diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..74f5825021a --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,36 @@ +{ + "name": "Java", + + "image": "mcr.microsoft.com/devcontainers/java:0-17", + + "features": { + "ghcr.io/devcontainers/features/java:1": { + "version": "none", + "installMaven": "true", + "installGradle": "false" + }, + "ghcr.io/devcontainers/features/docker-in-docker:2": {} + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "java -version", + + "customizations": { + "vscode": { + "extensions" : [ + "vscjava.vscode-java-pack", + "vscjava.vscode-maven", + "vscjava.vscode-java-debug", + "EditorConfig.EditorConfig", + "ms-azuretools.vscode-docker", + "antfu.vite", + "ms-kubernetes-tools.vscode-kubernetes-tools", + "github.vscode-pull-request-github" + ] + } + } + +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000000..4e23fc5467f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,286 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 120 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = true +ij_smart_tabs = false +ij_visual_guides = none +ij_wrap_on_typing = false +trim_trailing_whitespace = true + +[*.java] +indent_size = 2 +ij_continuation_indent_size = 4 +ij_java_align_consecutive_assignments = false +ij_java_align_consecutive_variable_declarations = false +ij_java_align_group_field_declarations = false +ij_java_align_multiline_annotation_parameters = false +ij_java_align_multiline_array_initializer_expression = false +ij_java_align_multiline_assignment = false +ij_java_align_multiline_binary_operation = false +ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_extends_list = false +ij_java_align_multiline_for = true +ij_java_align_multiline_method_parentheses = false +ij_java_align_multiline_parameters = true +ij_java_align_multiline_parameters_in_calls = false +ij_java_align_multiline_parenthesized_expression = false +ij_java_align_multiline_records = true +ij_java_align_multiline_resources = true +ij_java_align_multiline_ternary_operation = false +ij_java_align_multiline_text_blocks = false +ij_java_align_multiline_throws_list = false +ij_java_align_subsequent_simple_methods = false +ij_java_align_throws_keyword = false +ij_java_align_types_in_multi_catch = true +ij_java_annotation_parameter_wrap = off +ij_java_array_initializer_new_line_after_left_brace = false +ij_java_array_initializer_right_brace_on_new_line = false +ij_java_array_initializer_wrap = normal +ij_java_assert_statement_colon_on_next_line = false +ij_java_assert_statement_wrap = normal +ij_java_assignment_wrap = normal +ij_java_binary_operation_sign_on_next_line = false +ij_java_binary_operation_wrap = normal +ij_java_blank_lines_after_anonymous_class_header = 0 +ij_java_blank_lines_after_class_header = 0 +ij_java_blank_lines_after_imports = 1 +ij_java_blank_lines_after_package = 1 +ij_java_blank_lines_around_class = 1 +ij_java_blank_lines_around_field = 0 +ij_java_blank_lines_around_field_in_interface = 0 +ij_java_blank_lines_around_initializer = 1 +ij_java_blank_lines_around_method = 1 +ij_java_blank_lines_around_method_in_interface = 1 +ij_java_blank_lines_before_class_end = 0 +ij_java_blank_lines_before_imports = 1 +ij_java_blank_lines_before_method_body = 0 +ij_java_blank_lines_before_package = 1 +ij_java_block_brace_style = end_of_line +ij_java_block_comment_add_space = false +ij_java_block_comment_at_first_column = true +ij_java_builder_methods = none +ij_java_call_parameters_new_line_after_left_paren = false +ij_java_call_parameters_right_paren_on_new_line = false +ij_java_call_parameters_wrap = normal +ij_java_case_statement_on_separate_line = true +ij_java_catch_on_new_line = false +ij_java_class_annotation_wrap = split_into_lines +ij_java_class_brace_style = end_of_line +ij_java_class_count_to_use_import_on_demand = 999 +ij_java_class_names_in_javadoc = 1 +ij_java_do_not_indent_top_level_class_members = false +ij_java_do_not_wrap_after_single_annotation = false +ij_java_do_not_wrap_after_single_annotation_in_parameter = false +ij_java_do_while_brace_force = always +ij_java_doc_add_blank_line_after_description = true +ij_java_doc_add_blank_line_after_param_comments = false +ij_java_doc_add_blank_line_after_return = false +ij_java_doc_add_p_tag_on_empty_lines = true +ij_java_doc_align_exception_comments = true +ij_java_doc_align_param_comments = true +ij_java_doc_do_not_wrap_if_one_line = false +ij_java_doc_enable_formatting = true +ij_java_doc_enable_leading_asterisks = true +ij_java_doc_indent_on_continuation = false +ij_java_doc_keep_empty_lines = true +ij_java_doc_keep_empty_parameter_tag = true +ij_java_doc_keep_empty_return_tag = true +ij_java_doc_keep_empty_throws_tag = true +ij_java_doc_keep_invalid_tags = true +ij_java_doc_param_description_on_new_line = false +ij_java_doc_preserve_line_breaks = false +ij_java_doc_use_throws_not_exception_tag = true +ij_java_else_on_new_line = false +ij_java_entity_dd_suffix = EJB +ij_java_entity_eb_suffix = Bean +ij_java_entity_hi_suffix = Home +ij_java_entity_lhi_prefix = Local +ij_java_entity_lhi_suffix = Home +ij_java_entity_li_prefix = Local +ij_java_entity_pk_class = java.lang.String +ij_java_entity_vo_suffix = VO +ij_java_enum_constants_wrap = normal +ij_java_extends_keyword_wrap = normal +ij_java_extends_list_wrap = normal +ij_java_field_annotation_wrap = split_into_lines +ij_java_finally_on_new_line = false +ij_java_for_brace_force = always +ij_java_for_statement_new_line_after_left_paren = false +ij_java_for_statement_right_paren_on_new_line = false +ij_java_for_statement_wrap = normal +ij_java_generate_final_locals = false +ij_java_generate_final_parameters = false +ij_java_if_brace_force = always +ij_java_imports_layout = $*,|,* +ij_java_indent_case_from_switch = true +ij_java_insert_inner_class_imports = false +ij_java_insert_override_annotation = true +ij_java_keep_blank_lines_before_right_brace = 2 +ij_java_keep_blank_lines_between_package_declaration_and_header = 2 +ij_java_keep_blank_lines_in_code = 2 +ij_java_keep_blank_lines_in_declarations = 2 +ij_java_keep_builder_methods_indents = false +ij_java_keep_control_statement_in_one_line = true +ij_java_keep_first_column_comment = true +ij_java_keep_indents_on_empty_lines = false +ij_java_keep_line_breaks = true +ij_java_keep_multiple_expressions_in_one_line = false +ij_java_keep_simple_blocks_in_one_line = false +ij_java_keep_simple_classes_in_one_line = false +ij_java_keep_simple_lambdas_in_one_line = false +ij_java_keep_simple_methods_in_one_line = false +ij_java_label_indent_absolute = false +ij_java_label_indent_size = 0 +ij_java_lambda_brace_style = end_of_line +ij_java_layout_static_imports_separately = true +ij_java_line_comment_add_space = false +ij_java_line_comment_add_space_on_reformat = false +ij_java_line_comment_at_first_column = true +ij_java_message_dd_suffix = EJB +ij_java_message_eb_suffix = Bean +ij_java_method_annotation_wrap = split_into_lines +ij_java_method_brace_style = end_of_line +ij_java_method_call_chain_wrap = normal +ij_java_method_parameters_new_line_after_left_paren = false +ij_java_method_parameters_right_paren_on_new_line = false +ij_java_method_parameters_wrap = normal +ij_java_modifier_list_wrap = false +ij_java_multi_catch_types_wrap = normal +ij_java_names_count_to_use_import_on_demand = 999 +ij_java_new_line_after_lparen_in_annotation = false +ij_java_new_line_after_lparen_in_record_header = false +ij_java_parameter_annotation_wrap = normal +ij_java_parentheses_expression_new_line_after_left_paren = false +ij_java_parentheses_expression_right_paren_on_new_line = false +ij_java_place_assignment_sign_on_next_line = false +ij_java_prefer_longer_names = true +ij_java_prefer_parameters_wrap = false +ij_java_record_components_wrap = normal +ij_java_repeat_synchronized = true +ij_java_replace_instanceof_and_cast = false +ij_java_replace_null_check = true +ij_java_replace_sum_lambda_with_method_ref = true +ij_java_resource_list_new_line_after_left_paren = false +ij_java_resource_list_right_paren_on_new_line = false +ij_java_resource_list_wrap = normal +ij_java_rparen_on_new_line_in_annotation = false +ij_java_rparen_on_new_line_in_record_header = false +ij_java_session_dd_suffix = EJB +ij_java_session_eb_suffix = Bean +ij_java_session_hi_suffix = Home +ij_java_session_lhi_prefix = Local +ij_java_session_lhi_suffix = Home +ij_java_session_li_prefix = Local +ij_java_session_si_suffix = Service +ij_java_space_after_closing_angle_bracket_in_type_argument = false +ij_java_space_after_colon = true +ij_java_space_after_comma = true +ij_java_space_after_comma_in_type_arguments = true +ij_java_space_after_for_semicolon = true +ij_java_space_after_quest = true +ij_java_space_after_type_cast = true +ij_java_space_before_annotation_array_initializer_left_brace = false +ij_java_space_before_annotation_parameter_list = false +ij_java_space_before_array_initializer_left_brace = true +ij_java_space_before_catch_keyword = true +ij_java_space_before_catch_left_brace = true +ij_java_space_before_catch_parentheses = true +ij_java_space_before_class_left_brace = true +ij_java_space_before_colon = true +ij_java_space_before_colon_in_foreach = true +ij_java_space_before_comma = false +ij_java_space_before_do_left_brace = true +ij_java_space_before_else_keyword = true +ij_java_space_before_else_left_brace = true +ij_java_space_before_finally_keyword = true +ij_java_space_before_finally_left_brace = true +ij_java_space_before_for_left_brace = true +ij_java_space_before_for_parentheses = true +ij_java_space_before_for_semicolon = false +ij_java_space_before_if_left_brace = true +ij_java_space_before_if_parentheses = true +ij_java_space_before_method_call_parentheses = false +ij_java_space_before_method_left_brace = true +ij_java_space_before_method_parentheses = false +ij_java_space_before_opening_angle_bracket_in_type_parameter = false +ij_java_space_before_quest = true +ij_java_space_before_switch_left_brace = true +ij_java_space_before_switch_parentheses = true +ij_java_space_before_synchronized_left_brace = true +ij_java_space_before_synchronized_parentheses = true +ij_java_space_before_try_left_brace = true +ij_java_space_before_try_parentheses = true +ij_java_space_before_type_parameter_list = false +ij_java_space_before_while_keyword = true +ij_java_space_before_while_left_brace = true +ij_java_space_before_while_parentheses = true +ij_java_space_inside_one_line_enum_braces = false +ij_java_space_within_empty_array_initializer_braces = false +ij_java_space_within_empty_method_call_parentheses = false +ij_java_space_within_empty_method_parentheses = false +ij_java_spaces_around_additive_operators = true +ij_java_spaces_around_annotation_eq = true +ij_java_spaces_around_assignment_operators = true +ij_java_spaces_around_bitwise_operators = true +ij_java_spaces_around_equality_operators = true +ij_java_spaces_around_lambda_arrow = true +ij_java_spaces_around_logical_operators = true +ij_java_spaces_around_method_ref_dbl_colon = false +ij_java_spaces_around_multiplicative_operators = true +ij_java_spaces_around_relational_operators = true +ij_java_spaces_around_shift_operators = true +ij_java_spaces_around_type_bounds_in_type_parameters = true +ij_java_spaces_around_unary_operator = false +ij_java_spaces_within_angle_brackets = false +ij_java_spaces_within_annotation_parentheses = false +ij_java_spaces_within_array_initializer_braces = false +ij_java_spaces_within_braces = false +ij_java_spaces_within_brackets = false +ij_java_spaces_within_cast_parentheses = false +ij_java_spaces_within_catch_parentheses = false +ij_java_spaces_within_for_parentheses = false +ij_java_spaces_within_if_parentheses = false +ij_java_spaces_within_method_call_parentheses = false +ij_java_spaces_within_method_parentheses = false +ij_java_spaces_within_parentheses = false +ij_java_spaces_within_record_header = false +ij_java_spaces_within_switch_parentheses = false +ij_java_spaces_within_synchronized_parentheses = false +ij_java_spaces_within_try_parentheses = false +ij_java_spaces_within_while_parentheses = false +ij_java_special_else_if_treatment = true +ij_java_subclass_name_suffix = Impl +ij_java_ternary_operation_signs_on_next_line = false +ij_java_ternary_operation_wrap = normal +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = normal +ij_java_throws_list_wrap = normal +ij_java_use_external_annotations = false +ij_java_use_fq_class_names = false +ij_java_use_relative_indents = false +ij_java_use_single_class_imports = true +ij_java_variable_annotation_wrap = normal +ij_java_visibility = public +ij_java_while_brace_force = always +ij_java_while_on_new_line = false +ij_java_wrap_comments = false +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = false + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false + +[*.yaml] +indent_size = 2 +[*.yml] +indent_size = 2 + diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6a65f014c3f..cd94e7a297b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -14,5 +14,5 @@ # TESTS /kafka-ui-e2e-checks/ @provectus/kafka-qa -# HELM CHARTS -/charts/ @provectus/kafka-devops +# INFRA +/.github/workflows/ @provectus/kafka-devops diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 00000000000..4ec791ebb9c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,92 @@ +name: "\U0001F41E Bug report" +description: File a bug report +labels: ["status/triage", "type/bug"] +assignees: [] + +body: + - type: markdown + attributes: + value: | + Hi, thanks for raising the issue(-s), all contributions really matter! + Please, note that we'll close the issue without further explanation if you don't follow + this template and don't provide the information requested within this template. + + - type: checkboxes + id: terms + attributes: + label: Issue submitter TODO list + description: By you checking these checkboxes we can be sure you've done the essential things. + options: + - label: I've looked up my issue in [FAQ](https://docs.kafka-ui.provectus.io/faq/common-problems) + required: true + - label: I've searched for an already existing issues [here](https://github.com/provectus/kafka-ui/issues) + required: true + - label: I've tried running `master`-labeled docker image and the issue still persists there + required: true + - label: I'm running a supported version of the application which is listed [here](https://github.com/provectus/kafka-ui/blob/master/SECURITY.md) + required: true + + - type: textarea + attributes: + label: Describe the bug (actual behavior) + description: A clear and concise description of what the bug is. Use a list, if there is more than one problem + validations: + required: true + + - type: textarea + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen + validations: + required: false + + - type: textarea + attributes: + label: Your installation details + description: | + How do you run the app? Please provide as much info as possible: + 1. App version (commit hash in the top left corner of the UI) + 2. Helm chart version, if you use one + 3. Your application config. Please remove the sensitive info like passwords or API keys. + 4. Any IAAC configs + validations: + required: true + + - type: textarea + attributes: + label: Steps to reproduce + description: | + Please write down the order of the actions required to reproduce the issue. + For the advanced setups/complicated issue, we might need you to provide + a minimal [reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). + validations: + required: true + + - type: textarea + attributes: + label: Screenshots + description: | + If applicable, add screenshots to help explain your problem + validations: + required: false + + - type: textarea + attributes: + label: Logs + description: | + If applicable, *upload* screenshots to help explain your problem + validations: + required: false + + - type: textarea + attributes: + label: Additional context + description: | + Add any other context about the problem here. E.G.: + 1. Are there any alternative scenarios (different data/methods/configuration/setup) you have tried? + Were they successful or the same issue occurred? Please provide steps as well. + 2. Related issues (if there are any). + 3. Logs (if available) + 4. Is there any serious impact or behaviour on the end-user because of this issue, that can be overlooked? + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 6a2b8abec37..00000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -name: "\U0001F41E Bug report" -about: Create a bug report -title: '' -labels: status/triage, type/bug -assignees: '' - ---- - -**Describe the bug** - - - -**Set up** - - - -**Steps to Reproduce** -Steps to reproduce the behavior: - -1. - -**Expected behavior** - - -**Screenshots** - - - -**Additional context** - diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..ab1839eb161 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: false +contact_links: + - name: Report helm issue + url: https://github.com/provectus/kafka-ui-charts + about: Our helm charts are located in another repo. Please raise issues/PRs regarding charts in that repo. + - name: Official documentation + url: https://docs.kafka-ui.provectus.io/ + about: Before reaching out for support, please refer to our documentation. Read "FAQ" and "Common problems", also try using search there. + - name: Community Discord + url: https://discord.gg/4DWzD7pGE5 + about: Chat with other users, get some support or ask questions. + - name: GitHub Discussions + url: https://github.com/provectus/kafka-ui/discussions + about: An alternative place to ask questions or to get some support. diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 00000000000..e52c2b7ae99 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,66 @@ +name: "\U0001F680 Feature request" +description: Propose a new feature +labels: ["status/triage", "type/feature"] +assignees: [] + +body: + - type: markdown + attributes: + value: | + Hi, thanks for raising the issue(-s), all contributions really matter! + Please, note that we'll close the issue without further explanation if you don't follow + this template and don't provide the information requested within this template. + + - type: checkboxes + id: terms + attributes: + label: Issue submitter TODO list + description: By you checking these checkboxes we can be sure you've done the essential things. + options: + - label: I've searched for an already existing issues [here](https://github.com/provectus/kafka-ui/issues) + required: true + - label: I'm running a supported version of the application which is listed [here](https://github.com/provectus/kafka-ui/blob/master/SECURITY.md) and the feature is not present there + required: true + + - type: textarea + attributes: + label: Is your proposal related to a problem? + description: | + Provide a clear and concise description of what the problem is. + For example, "I'm always frustrated when..." + validations: + required: false + + - type: textarea + attributes: + label: Describe the feature you're interested in + description: | + Provide a clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + attributes: + label: Describe alternatives you've considered + description: | + Let us know about other solutions you've tried or researched. + validations: + required: false + + - type: input + attributes: + label: Version you're running + description: | + Please provide the app version you're currently running: + 1. App version (commit hash in the top left corner of the UI) + validations: + required: true + + - type: textarea + attributes: + label: Additional context + description: | + Is there anything else you can add about the proposal? + You might want to link to related issues here, if you haven't already. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 68bcf80782f..00000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -name: "\U0001F680 Feature request" -about: Propose a new feature -title: '' -labels: status/triage, type/feature -assignees: '' - ---- - -### Is your proposal related to a problem? - - - -### Describe the solution you'd like - - - -### Describe alternatives you've considered - - - -### Additional context - - - diff --git a/.github/ISSUE_TEMPLATE/k8s_whine.md b/.github/ISSUE_TEMPLATE/k8s_whine.md deleted file mode 100644 index 1d767005eb0..00000000000 --- a/.github/ISSUE_TEMPLATE/k8s_whine.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -name: "⎈ K8s/Helm problem report" -about: Report a problem with k8s/helm charts/etc -title: '' -labels: scope/k8s, status/triage -assignees: azatsafin, 5hin0bi - ---- - -**Describe the bug** - - - -**Set up** - - - -**Steps to Reproduce** -Steps to reproduce the behavior: - -1. - -**Expected behavior** - - -**Screenshots** - - - -**Additional context** - diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 75ea103c31e..7e8552962ab 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,8 +8,6 @@ updates: timezone: Europe/Moscow reviewers: - "Haarolean" - assignees: - - "Haarolean" labels: - "scope/backend" - "type/dependencies" @@ -99,8 +97,6 @@ updates: timezone: Europe/Moscow reviewers: - "Haarolean" - assignees: - - "Haarolean" labels: - "scope/infrastructure" - "type/dependencies" diff --git a/.github/release_drafter.yaml b/.github/release_drafter.yaml index 421697f2e70..36795355407 100644 --- a/.github/release_drafter.yaml +++ b/.github/release_drafter.yaml @@ -9,21 +9,33 @@ template: | exclude-labels: - 'scope/infrastructure' - 'scope/QA' + - 'scope/AQA' - 'type/dependencies' - 'type/chore' - 'type/documentation' - 'type/refactoring' categories: + - title: '🚩 Breaking Changes' + labels: + - 'impact/changelog' + - title: '⚙️Features' labels: - 'type/feature' + - title: '🪛Enhancements' labels: - 'type/enhancement' + - title: '🔨Bug Fixes' labels: - 'type/bug' + + - title: 'Security' + labels: + - 'type/security' + - title: '⎈ Helm/K8S Changes' labels: - 'scope/k8s' diff --git a/.github/workflows/aws_publisher.yaml b/.github/workflows/aws_publisher.yaml index 39468d4dde3..5ce2b587fb9 100644 --- a/.github/workflows/aws_publisher.yaml +++ b/.github/workflows/aws_publisher.yaml @@ -1,4 +1,4 @@ -name: AWS Marketplace Publisher +name: "Infra: Release: AWS Marketplace Publisher" on: workflow_dispatch: inputs: @@ -10,6 +10,11 @@ on: description: 'Version of KafkaUI' required: true default: '0.3.2' + PublishOnMarketplace: + description: 'If set to true, the request to update AWS Server product version will be raised' + required: true + default: false + type: boolean jobs: build-ami: @@ -19,14 +24,14 @@ jobs: - name: Clone infra repo run: | echo "Cloning repo..." - git clone https://kafka-ui-infra:${{ secrets.KAFKA_UI_INFRA_TOKEN }}@gitlab.provectus.com/provectus-internals/kafka-ui-infra.git --branch ${{ github.event.inputs.KafkaUIInfraBranch }} + git clone https://infra-tech:${{ secrets.INFRA_USER_ACCESS_TOKEN }}@github.com/provectus/kafka-ui-infra.git --branch ${{ github.event.inputs.KafkaUIInfraBranch }} echo "Cd to packer DIR..." cd kafka-ui-infra/ami echo "WORK_DIR=$(pwd)" >> $GITHUB_ENV echo "Packer will be triggered in this dir $WORK_DIR" - name: Configure AWS credentials for Kafka-UI account - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v3 with: aws-access-key-id: ${{ secrets.AWS_AMI_PUBLISH_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_AMI_PUBLISH_KEY_SECRET }} @@ -46,6 +51,27 @@ jobs: with: command: build arguments: "-color=false -on-error=abort -var=kafka_ui_release_version=${{ github.event.inputs.KafkaUIReleaseVersion }}" - target: kafka-ui-infra/ami/kafka-ui.pkr.hcl + target: kafka-ui.pkr.hcl + working_directory: ${{ env.WORK_DIR }} env: PACKER_LOG: 1 + + # add fresh AMI to AWS Marketplace + - name: Publish Artifact at Marketplace + if: ${{ github.event.inputs.PublishOnMarketplace == 'true' }} + env: + PRODUCT_ID: ${{ secrets.AWS_SERVER_PRODUCT_ID }} + RELEASE_VERSION: "${{ github.event.inputs.KafkaUIReleaseVersion }}" + RELEASE_NOTES: "https://github.com/provectus/kafka-ui/releases/tag/v${{ github.event.inputs.KafkaUIReleaseVersion }}" + MP_ROLE_ARN: ${{ secrets.AWS_MARKETPLACE_AMI_ACCESS_ROLE }} # https://docs.aws.amazon.com/marketplace/latest/userguide/ami-single-ami-products.html#single-ami-marketplace-ami-access + AMI_OS_VERSION: "amzn2-ami-kernel-5.10-hvm-*-x86_64-gp2" + run: | + set -x + pwd + ls -la kafka-ui-infra/ami + echo $WORK_DIR/manifest.json + export AMI_ID=$(jq -r '.builds[-1].artifact_id' kafka-ui-infra/ami/manifest.json | cut -d ":" -f2) + /bin/bash kafka-ui-infra/aws-marketplace/prepare_changeset.sh > changeset.json + aws marketplace-catalog start-change-set \ + --catalog "AWSMarketplace" \ + --change-set "$(cat changeset.json)" diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index aa9237618b5..7f62772832a 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -1,4 +1,4 @@ -name: backend +name: "Backend: PR/master build & test" on: push: branches: @@ -8,6 +8,9 @@ on: paths: - "kafka-ui-api/**" - "pom.xml" +permissions: + checks: write + pull-requests: write jobs: build-and-test: runs-on: ubuntu-latest @@ -16,17 +19,12 @@ jobs: with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} - - name: Cache local Maven repository - uses: actions/cache@v3 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - - name: Set up JDK 1.13 - uses: actions/setup-java@v1 + - name: Set up JDK + uses: actions/setup-java@v3 with: - java-version: 1.13 + java-version: '17' + distribution: 'zulu' + cache: 'maven' - name: Cache SonarCloud packages uses: actions/cache@v3 with: @@ -34,23 +32,25 @@ jobs: key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - name: Build and analyze pull request target - if: ${{ github.event_name == 'pull_request_target' }} + if: ${{ github.event_name == 'pull_request' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN_BACKEND }} + HEAD_REF: ${{ github.head_ref }} + BASE_REF: ${{ github.base_ref }} run: | - mvn versions:set -DnewVersion=${{ github.event.pull_request.head.sha }} - mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \ + ./mvnw -B -ntp versions:set -DnewVersion=${{ github.event.pull_request.head.sha }} + ./mvnw -B -V -ntp verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \ -Dsonar.projectKey=com.provectus:kafka-ui_backend \ -Dsonar.pullrequest.key=${{ github.event.pull_request.number }} \ - -Dsonar.pullrequest.branch=${{ github.head_ref }} \ - -Dsonar.pullrequest.base=${{ github.base_ref }} + -Dsonar.pullrequest.branch=$HEAD_REF \ + -Dsonar.pullrequest.base=$BASE_REF - name: Build and analyze push master if: ${{ github.event_name == 'push' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN_BACKEND }} run: | - mvn versions:set -DnewVersion=$GITHUB_SHA - mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \ + ./mvnw -B -ntp versions:set -DnewVersion=$GITHUB_SHA + ./mvnw -B -V -ntp verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \ -Dsonar.projectKey=com.provectus:kafka-ui_backend diff --git a/.github/workflows/block_merge.yml b/.github/workflows/block_merge.yml index ce98cc7e3a2..c689d45b0d7 100644 --- a/.github/workflows/block_merge.yml +++ b/.github/workflows/block_merge.yml @@ -1,4 +1,4 @@ -name: Pull Request Labels +name: "Infra: PR block merge" on: pull_request: types: [opened, labeled, unlabeled, synchronize] @@ -6,7 +6,7 @@ jobs: block_merge: runs-on: ubuntu-latest steps: - - uses: mheap/github-action-required-labels@v1 + - uses: mheap/github-action-required-labels@v5 with: mode: exactly count: 0 diff --git a/.github/workflows/branch-deploy.yml b/.github/workflows/branch-deploy.yml index 7d93385c307..2aa76126299 100644 --- a/.github/workflows/branch-deploy.yml +++ b/.github/workflows/branch-deploy.yml @@ -1,4 +1,4 @@ -name: DeployFromBranch +name: "Infra: Feature Testing: Init env" on: workflow_dispatch: @@ -10,40 +10,33 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: get branch name id: extract_branch run: | - hub pr checkout ${{ github.event.pull_request.number }} - branch_name=$(hub branch | grep "*" | sed -e 's/^\*//') - echo $branch_name - echo ::set-output name=branch::${branch_name} - tag=$(echo $branch_name | sed 's/\//-/g' | sed 's/\./-/g' | sed 's/\_/-/g' | sed -e 's/\(.*\)/\L\1/' | cut -c1-32 | sed -E 's/(^[^a-z0-9])*([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)([^a-z0-9]*)/\2/') - echo ::set-output name=tag::${tag} + tag='pr${{ github.event.pull_request.number }}' + echo "tag=${tag}" >> $GITHUB_OUTPUT env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Cache local Maven repository - uses: actions/cache@v3 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - - name: Set up JDK 1.13 - uses: actions/setup-java@v1 + - name: Set up JDK + uses: actions/setup-java@v3 with: - java-version: 1.13 + java-version: '17' + distribution: 'zulu' + cache: 'maven' - name: Build id: build run: | - mvn versions:set -DnewVersion=$GITHUB_SHA - mvn clean package -Pprod -DskipTests - export VERSION=$(mvn -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec) - echo "::set-output name=version::${VERSION}" + ./mvnw -B -ntp versions:set -DnewVersion=$GITHUB_SHA + ./mvnw -B -V -ntp clean package -Pprod -DskipTests + export VERSION=$(./mvnw -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec) + echo "version=${VERSION}" >> $GITHUB_OUTPUT - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Cache Docker layers uses: actions/cache@v3 with: @@ -52,7 +45,7 @@ jobs: restore-keys: | ${{ runner.os }}-buildx- - name: Configure AWS credentials for Kafka-UI account - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v3 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} @@ -62,7 +55,7 @@ jobs: uses: aws-actions/amazon-ecr-login@v1 - name: Build and push id: docker_build_and_push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4 with: builder: ${{ steps.buildx.outputs.name }} context: kafka-ui-api @@ -80,29 +73,33 @@ jobs: steps: - name: clone run: | - git clone https://kafka-ui-infra:${{ secrets.KAFKA_UI_INFRA_TOKEN }}@gitlab.provectus.com/provectus-internals/kafka-ui-infra.git + git clone https://infra-tech:${{ secrets.INFRA_USER_ACCESS_TOKEN }}@github.com/provectus/kafka-ui-infra.git --branch envs - name: create deployment run: | cd kafka-ui-infra/aws-infrastructure4eks/argocd/scripts echo "Branch:${{ needs.build.outputs.tag }}" ./kafka-ui-deployment-from-branch.sh ${{ needs.build.outputs.tag }} ${{ github.event.label.name }} ${{ secrets.FEATURE_TESTING_UI_PASSWORD }} - git config --global user.email "kafka-ui-infra@provectus.com" - git config --global user.name "kafka-ui-infra" + git config --global user.email "infra-tech@provectus.com" + git config --global user.name "infra-tech" git add ../kafka-ui-from-branch/ git commit -m "added env:${{ needs.build.outputs.deploy }}" && git push || true - - name: make comment with private deployment link + - name: update status check for private deployment if: ${{ github.event.label.name == 'status/feature_testing' }} - uses: peter-evans/create-or-update-comment@v2 + uses: Sibz/github-status-action@v1.1.6 with: - issue-number: ${{ github.event.pull_request.number }} - body: | - Custom deployment will be available at http://${{ needs.build.outputs.tag }}.internal.kafka-ui.provectus.io + authToken: ${{secrets.GITHUB_TOKEN}} + context: "Click Details button to open custom deployment page" + state: "success" + sha: ${{ github.event.pull_request.head.sha || github.sha }} + target_url: "http://${{ needs.build.outputs.tag }}.internal.kafka-ui.provectus.io" - - name: make comment with public deployment link + - name: update status check for public deployment if: ${{ github.event.label.name == 'status/feature_testing_public' }} - uses: peter-evans/create-or-update-comment@v2 + uses: Sibz/github-status-action@v1.1.6 with: - issue-number: ${{ github.event.pull_request.number }} - body: | - Custom deployment will be available at http://${{ needs.build.outputs.tag }}.kafka-ui.provectus.io in 5 minutes + authToken: ${{secrets.GITHUB_TOKEN}} + context: "Click Details button to open custom deployment page" + state: "success" + sha: ${{ github.event.pull_request.head.sha || github.sha }} + target_url: "http://${{ needs.build.outputs.tag }}.internal.kafka-ui.provectus.io" diff --git a/.github/workflows/branch-remove.yml b/.github/workflows/branch-remove.yml index 37b24239d52..d32e1d4edb6 100644 --- a/.github/workflows/branch-remove.yml +++ b/.github/workflows/branch-remove.yml @@ -1,40 +1,22 @@ -name: RemoveCustomDeployment +name: "Infra: Feature Testing: Destroy env" on: workflow_dispatch: pull_request: types: ['unlabeled', 'closed'] jobs: remove: - if: ${{ github.event.label.name == 'status/feature_testing' || github.event.label.name == 'status/feature_testing_public' }} runs-on: ubuntu-latest + if: ${{ (github.event.label.name == 'status/feature_testing' || github.event.label.name == 'status/feature_testing_public') || (github.event.action == 'closed' && (contains(github.event.pull_request.labels.*.name, 'status/feature_testing') || contains(github.event.pull_request.labels.*.name, 'status/feature_testing_public'))) }} steps: - uses: actions/checkout@v3 - - name: get branch name - id: extract_branch - run: | - hub pr checkout ${{ github.event.pull_request.number }} - branch_name=$(hub branch | grep "*" | sed -e 's/^\*//') - echo $branch_name - echo ::set-output name=branch::${branch_name} - tag=$(echo $branch_name | sed 's/\//-/g' | sed 's/\./-/g' | sed 's/\_/-/g' | cut -c1-32) - echo ::set-output name=tag::${tag} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: clone run: | - git clone https://kafka-ui-infra:${{ secrets.KAFKA_UI_INFRA_TOKEN }}@gitlab.provectus.com/provectus-internals/kafka-ui-infra.git + git clone https://infra-tech:${{ secrets.INFRA_USER_ACCESS_TOKEN }}@github.com/provectus/kafka-ui-infra.git --branch envs - name: remove env run: | cd kafka-ui-infra/aws-infrastructure4eks/argocd/scripts - echo "Branch:${{ steps.extract_branch.outputs.tag }}" - ./delete-env.sh ${{ steps.extract_branch.outputs.tag }} - git config --global user.email "kafka-ui-infra@provectus.com" - git config --global user.name "kafka-ui-infra" + ./delete-env.sh pr${{ github.event.pull_request.number }} || true + git config --global user.email "infra-tech@provectus.com" + git config --global user.name "infra-tech" git add ../kafka-ui-from-branch/ git commit -m "removed env:${{ needs.build.outputs.deploy }}" && git push || true - - name: make comment with deployment link - uses: peter-evans/create-or-update-comment@v2 - with: - issue-number: ${{ github.event.pull_request.number }} - body: | - Custom deployment removed diff --git a/.github/workflows/build-public-image.yml b/.github/workflows/build-public-image.yml new file mode 100644 index 00000000000..5f6c46e25eb --- /dev/null +++ b/.github/workflows/build-public-image.yml @@ -0,0 +1,74 @@ +name: "Infra: Image Testing: Deploy" +on: + workflow_dispatch: + pull_request: + types: ['labeled'] +jobs: + build: + if: ${{ github.event.label.name == 'status/image_testing' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: get branch name + id: extract_branch + run: | + tag='${{ github.event.pull_request.number }}' + echo "tag=${tag}" >> $GITHUB_OUTPUT + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'zulu' + cache: 'maven' + - name: Build + id: build + run: | + ./mvnw -B -ntp versions:set -DnewVersion=$GITHUB_SHA + ./mvnw -B -V -ntp clean package -Pprod -DskipTests + export VERSION=$(./mvnw -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec) + echo "version=${VERSION}" >> $GITHUB_OUTPUT + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + - name: Configure AWS credentials for Kafka-UI account + uses: aws-actions/configure-aws-credentials@v3 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + with: + registry-type: 'public' + - name: Build and push + id: docker_build_and_push + uses: docker/build-push-action@v4 + with: + builder: ${{ steps.buildx.outputs.name }} + context: kafka-ui-api + push: true + tags: public.ecr.aws/provectus/kafka-ui-custom-build:${{ steps.extract_branch.outputs.tag }} + build-args: | + JAR_FILE=kafka-ui-api-${{ steps.build.outputs.version }}.jar + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache + - name: make comment with private deployment link + uses: peter-evans/create-or-update-comment@v3 + with: + issue-number: ${{ github.event.pull_request.number }} + body: | + Image published at public.ecr.aws/provectus/kafka-ui-custom-build:${{ steps.extract_branch.outputs.tag }} + outputs: + tag: ${{ steps.extract_branch.outputs.tag }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8eae96d5fb6..c50da89ae86 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,6 +20,8 @@ on: paths: - 'kafka-ui-contract/**' - 'kafka-ui-react-app/**' + - 'kafka-ui-api/**' + - 'kafka-ui-serde-api/**' schedule: - cron: '39 15 * * 6' @@ -31,7 +33,7 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'javascript' ] + language: [ 'javascript', 'java' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed @@ -42,7 +44,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,10 +52,17 @@ jobs: # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'zulu' + cache: 'maven' + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -67,4 +76,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/cve.yaml b/.github/workflows/cve.yaml index ab6df3a049d..4b38fa2465c 100644 --- a/.github/workflows/cve.yaml +++ b/.github/workflows/cve.yaml @@ -10,32 +10,26 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Cache local Maven repository - uses: actions/cache@v3 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - - - name: Set up JDK 1.13 - uses: actions/setup-java@v1 + - name: Set up JDK + uses: actions/setup-java@v3 with: - java-version: 1.13 + java-version: '17' + distribution: 'zulu' + cache: 'maven' - name: Build project id: build run: | - mvn versions:set -DnewVersion=$GITHUB_SHA - mvn clean package -DskipTests - export VERSION=$(mvn -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec) - echo "::set-output name=version::${VERSION}" + ./mvnw -B -ntp versions:set -DnewVersion=$GITHUB_SHA + ./mvnw -B -V -ntp clean package -DskipTests + export VERSION=$(./mvnw -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec) + echo "version=${VERSION}" >> $GITHUB_OUTPUT - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Cache Docker layers uses: actions/cache@v3 @@ -46,7 +40,7 @@ jobs: ${{ runner.os }}-buildx- - name: Build docker image - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4 with: builder: ${{ steps.buildx.outputs.name }} context: kafka-ui-api @@ -61,7 +55,7 @@ jobs: cache-to: type=local,dest=/tmp/.buildx-cache - name: Run CVE checks - uses: aquasecurity/trivy-action@0.3.0 + uses: aquasecurity/trivy-action@0.12.0 with: image-ref: "provectuslabs/kafka-ui:${{ steps.build.outputs.version }}" format: "table" diff --git a/.github/workflows/delete-public-image.yml b/.github/workflows/delete-public-image.yml new file mode 100644 index 00000000000..45b4e8f7f37 --- /dev/null +++ b/.github/workflows/delete-public-image.yml @@ -0,0 +1,34 @@ +name: "Infra: Image Testing: Delete" +on: + workflow_dispatch: + pull_request: + types: ['unlabeled', 'closed'] +jobs: + remove: + if: ${{ github.event.label.name == 'status/image_testing' || ( github.event.action == 'closed' && (contains(github.event.pull_request.labels, 'status/image_testing'))) }} + runs-on: ubuntu-latest + steps: + - name: get branch name + id: extract_branch + run: | + echo + tag='${{ github.event.pull_request.number }}' + echo "tag=${tag}" >> $GITHUB_OUTPUT + - name: Configure AWS credentials for Kafka-UI account + uses: aws-actions/configure-aws-credentials@v3 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + with: + registry-type: 'public' + - name: Remove from ECR + id: remove_from_ecr + run: | + aws ecr-public batch-delete-image \ + --repository-name kafka-ui-custom-build \ + --image-ids imageTag=${{ steps.extract_branch.outputs.tag }} \ + --region us-east-1 diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml index 3c1211b7b36..a0726c204a3 100644 --- a/.github/workflows/documentation.yaml +++ b/.github/workflows/documentation.yaml @@ -1,4 +1,4 @@ -name: Documentation +name: "Infra: Docs: URL linter" on: pull_request: types: @@ -15,9 +15,9 @@ jobs: steps: - uses: actions/checkout@v3 - name: Check URLs in files - uses: urlstechie/urlchecker-action@0.2.31 + uses: urlstechie/urlchecker-action@0.0.34 with: exclude_patterns: localhost,127.0.,192.168. - exclude_urls: https://api.server,https://graph.microsoft.com/User.Read,https://dev-a63ggcut.auth0.com/ + exclude_urls: https://api.server,https://graph.microsoft.com/User.Read,https://dev-a63ggcut.auth0.com/,http://main-schema-registry:8081,http://schema-registry:8081,http://another-yet-schema-registry:8081,http://another-schema-registry:8081 print_all: false file_types: .md diff --git a/.github/workflows/e2e-automation.yml b/.github/workflows/e2e-automation.yml new file mode 100644 index 00000000000..b3bb2f266fc --- /dev/null +++ b/.github/workflows/e2e-automation.yml @@ -0,0 +1,88 @@ +name: "E2E: Automation suite" +on: + workflow_dispatch: + inputs: + test_suite: + description: 'Select test suite to run' + default: 'regression' + required: true + type: choice + options: + - regression + - sanity + - smoke + qase_token: + description: 'Set Qase token to enable integration' + required: false + type: string + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.sha }} + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v3 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: eu-central-1 + - name: Set up environment + id: set_env_values + run: | + cat "./kafka-ui-e2e-checks/.env.ci" >> "./kafka-ui-e2e-checks/.env" + - name: Pull with Docker + id: pull_chrome + run: | + docker pull selenoid/vnc_chrome:103.0 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'zulu' + cache: 'maven' + - name: Build with Maven + id: build_app + run: | + ./mvnw -B -ntp versions:set -DnewVersion=${{ github.sha }} + ./mvnw -B -V -ntp clean install -Pprod -Dmaven.test.skip=true ${{ github.event.inputs.extraMavenOptions }} + - name: Compose with Docker + id: compose_app + # use the following command until #819 will be fixed + run: | + docker-compose -f kafka-ui-e2e-checks/docker/selenoid-git.yaml up -d + docker-compose -f ./documentation/compose/e2e-tests.yaml up -d + - name: Run test suite + run: | + ./mvnw -B -ntp versions:set -DnewVersion=${{ github.sha }} + ./mvnw -B -V -ntp -DQASEIO_API_TOKEN=${{ github.event.inputs.qase_token }} -Dsurefire.suiteXmlFiles='src/test/resources/${{ github.event.inputs.test_suite }}.xml' -Dsuite=${{ github.event.inputs.test_suite }} -f 'kafka-ui-e2e-checks' test -Pprod + - name: Generate Allure report + uses: simple-elf/allure-report-action@master + if: always() + id: allure-report + with: + allure_results: ./kafka-ui-e2e-checks/allure-results + gh_pages: allure-results + allure_report: allure-report + subfolder: allure-results + report_url: "http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com" + - uses: jakejarvis/s3-sync-action@master + if: always() + env: + AWS_S3_BUCKET: 'kafkaui-allure-reports' + AWS_REGION: 'eu-central-1' + SOURCE_DIR: 'allure-history/allure-results' + - name: Deploy report to Amazon S3 + if: always() + uses: Sibz/github-status-action@v1.1.6 + with: + authToken: ${{secrets.GITHUB_TOKEN}} + context: "Click Details button to open Allure report" + state: "success" + sha: ${{ github.sha }} + target_url: http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com/${{ github.run_number }} + - name: Dump Docker logs on failure + if: failure() + uses: jwalton/gh-docker-logs@v2.2.1 diff --git a/.github/workflows/e2e-checks.yaml b/.github/workflows/e2e-checks.yaml index 574aa922adf..e62cd724a8f 100644 --- a/.github/workflows/e2e-checks.yaml +++ b/.github/workflows/e2e-checks.yaml @@ -1,13 +1,15 @@ -name: e2e-checks +name: "E2E: PR healthcheck" on: pull_request_target: - types: ["opened", "edited", "reopened", "synchronize"] + types: [ "opened", "edited", "reopened", "synchronize" ] paths: - "kafka-ui-api/**" - "kafka-ui-contract/**" - "kafka-ui-react-app/**" - "kafka-ui-e2e-checks/**" - "pom.xml" +permissions: + statuses: write jobs: build-and-test: runs-on: ubuntu-latest @@ -15,39 +17,41 @@ jobs: - uses: actions/checkout@v3 with: ref: ${{ github.event.pull_request.head.sha }} - - name: Cache local Maven repository - uses: actions/cache@v3 + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v3 with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - - name: Set the values + aws-access-key-id: ${{ secrets.S3_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.S3_AWS_SECRET_ACCESS_KEY }} + aws-region: eu-central-1 + - name: Set up environment id: set_env_values run: | cat "./kafka-ui-e2e-checks/.env.ci" >> "./kafka-ui-e2e-checks/.env" - - name: pull docker - id: pull_selenoid + - name: Pull with Docker + id: pull_chrome run: | - docker pull selenoid/vnc:chrome_86.0 - - name: Set up JDK 1.13 - uses: actions/setup-java@v1 + docker pull selenoid/vnc_chrome:103.0 + - name: Set up JDK + uses: actions/setup-java@v3 with: - java-version: 1.13 + java-version: '17' + distribution: 'zulu' + cache: 'maven' - name: Build with Maven id: build_app run: | - mvn versions:set -DnewVersion=${{ github.event.pull_request.head.sha }} - mvn clean package -DskipTests ${{ github.event.inputs.extraMavenOptions }} - - name: compose app + ./mvnw -B -ntp versions:set -DnewVersion=${{ github.event.pull_request.head.sha }} + ./mvnw -B -V -ntp clean install -Pprod -Dmaven.test.skip=true ${{ github.event.inputs.extraMavenOptions }} + - name: Compose with Docker id: compose_app # use the following command until #819 will be fixed run: | - docker-compose -f ./documentation/compose/kafka-ui-connectors.yaml up -d - - name: e2e run + docker-compose -f kafka-ui-e2e-checks/docker/selenoid-git.yaml up -d + docker-compose -f ./documentation/compose/e2e-tests.yaml up -d && until [ "$(docker exec kafka-ui wget --spider --server-response http://localhost:8080/actuator/health 2>&1 | grep -c 'HTTP/1.1 200 OK')" == "1" ]; do echo "Waiting for kafka-ui ..." && sleep 1; done + - name: Run test suite run: | - mvn versions:set -DnewVersion=${{ github.event.pull_request.head.sha }} - mvn -pl '!kafka-ui-api' test -Pprod + ./mvnw -B -ntp versions:set -DnewVersion=${{ github.event.pull_request.head.sha }} + ./mvnw -B -V -ntp -Dsurefire.suiteXmlFiles='src/test/resources/smoke.xml' -f 'kafka-ui-e2e-checks' test -Pprod - name: Generate allure report uses: simple-elf/allure-report-action@master if: always() @@ -57,23 +61,22 @@ jobs: gh_pages: allure-results allure_report: allure-report subfolder: allure-results - - name: Deploy allure report to Github Pages + report_url: "http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com" + - uses: jakejarvis/s3-sync-action@master if: always() - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: allure-history - publish_branch: gh-pages - destination_dir: ./allure - - name: Post the link to allure report + env: + AWS_S3_BUCKET: 'kafkaui-allure-reports' + AWS_REGION: 'eu-central-1' + SOURCE_DIR: 'allure-history/allure-results' + - name: Deploy report to Amazon S3 if: always() uses: Sibz/github-status-action@v1.1.6 with: authToken: ${{secrets.GITHUB_TOKEN}} - context: "Test report" + context: "Click Details button to open Allure report" state: "success" sha: ${{ github.event.pull_request.head.sha || github.sha }} - target_url: https://${{ github.repository_owner }}.github.io/kafka-ui/allure/allure-results/${{ github.run_number }} + target_url: http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com/${{ github.run_number }} - name: Dump docker logs on failure if: failure() - uses: jwalton/gh-docker-logs@v2.2.0 + uses: jwalton/gh-docker-logs@v2.2.1 diff --git a/.github/workflows/e2e-manual.yml b/.github/workflows/e2e-manual.yml new file mode 100644 index 00000000000..31cd3bdf642 --- /dev/null +++ b/.github/workflows/e2e-manual.yml @@ -0,0 +1,43 @@ +name: "E2E: Manual suite" +on: + workflow_dispatch: + inputs: + test_suite: + description: 'Select test suite to run' + default: 'manual' + required: true + type: choice + options: + - manual + - qase + qase_token: + description: 'Set Qase token to enable integration' + required: true + type: string + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.sha }} + - name: Set up environment + id: set_env_values + run: | + cat "./kafka-ui-e2e-checks/.env.ci" >> "./kafka-ui-e2e-checks/.env" + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'zulu' + cache: 'maven' + - name: Build with Maven + id: build_app + run: | + ./mvnw -B -ntp versions:set -DnewVersion=${{ github.sha }} + ./mvnw -B -V -ntp clean install -Pprod -Dmaven.test.skip=true ${{ github.event.inputs.extraMavenOptions }} + - name: Run test suite + run: | + ./mvnw -B -ntp versions:set -DnewVersion=${{ github.sha }} + ./mvnw -B -V -ntp -DQASEIO_API_TOKEN=${{ github.event.inputs.qase_token }} -Dsurefire.suiteXmlFiles='src/test/resources/${{ github.event.inputs.test_suite }}.xml' -Dsuite=${{ github.event.inputs.test_suite }} -f 'kafka-ui-e2e-checks' test -Pprod diff --git a/.github/workflows/e2e-weekly.yml b/.github/workflows/e2e-weekly.yml new file mode 100644 index 00000000000..439d8037649 --- /dev/null +++ b/.github/workflows/e2e-weekly.yml @@ -0,0 +1,75 @@ +name: "E2E: Weekly suite" +on: + schedule: + - cron: '0 1 * * 1' + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.sha }} + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v3 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: eu-central-1 + - name: Set up environment + id: set_env_values + run: | + cat "./kafka-ui-e2e-checks/.env.ci" >> "./kafka-ui-e2e-checks/.env" + - name: Pull with Docker + id: pull_chrome + run: | + docker pull selenoid/vnc_chrome:103.0 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'zulu' + cache: 'maven' + - name: Build with Maven + id: build_app + run: | + ./mvnw -B -ntp versions:set -DnewVersion=${{ github.sha }} + ./mvnw -B -V -ntp clean install -Pprod -Dmaven.test.skip=true ${{ github.event.inputs.extraMavenOptions }} + - name: Compose with Docker + id: compose_app + # use the following command until #819 will be fixed + run: | + docker-compose -f kafka-ui-e2e-checks/docker/selenoid-git.yaml up -d + docker-compose -f ./documentation/compose/e2e-tests.yaml up -d + - name: Run test suite + run: | + ./mvnw -B -ntp versions:set -DnewVersion=${{ github.sha }} + ./mvnw -B -V -ntp -DQASEIO_API_TOKEN=${{ secrets.QASEIO_API_TOKEN }} -Dsurefire.suiteXmlFiles='src/test/resources/sanity.xml' -Dsuite=weekly -f 'kafka-ui-e2e-checks' test -Pprod + - name: Generate Allure report + uses: simple-elf/allure-report-action@master + if: always() + id: allure-report + with: + allure_results: ./kafka-ui-e2e-checks/allure-results + gh_pages: allure-results + allure_report: allure-report + subfolder: allure-results + report_url: "http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com" + - uses: jakejarvis/s3-sync-action@master + if: always() + env: + AWS_S3_BUCKET: 'kafkaui-allure-reports' + AWS_REGION: 'eu-central-1' + SOURCE_DIR: 'allure-history/allure-results' + - name: Deploy report to Amazon S3 + if: always() + uses: Sibz/github-status-action@v1.1.6 + with: + authToken: ${{secrets.GITHUB_TOKEN}} + context: "Click Details button to open Allure report" + state: "success" + sha: ${{ github.sha }} + target_url: http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com/${{ github.run_number }} + - name: Dump Docker logs on failure + if: failure() + uses: jwalton/gh-docker-logs@v2.2.1 diff --git a/.github/workflows/frontend.yaml b/.github/workflows/frontend.yaml index ee13432afa9..9d7300448c9 100644 --- a/.github/workflows/frontend.yaml +++ b/.github/workflows/frontend.yaml @@ -1,4 +1,4 @@ -name: frontend +name: "Frontend: PR/master build & test" on: push: branches: @@ -8,6 +8,9 @@ on: paths: - "kafka-ui-contract/**" - "kafka-ui-react-app/**" +permissions: + checks: write + pull-requests: write jobs: build-and-test: env: @@ -20,35 +23,33 @@ jobs: # Disabling shallow clone is recommended for improving relevancy of reporting fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} - - name: Use Node.js - uses: actions/setup-node@v3.1.1 + - uses: pnpm/action-setup@v2.4.0 with: - node-version: "14" - - name: Cache node dependency - uses: actions/cache@v3 + version: 8.6.12 + - name: Install node + uses: actions/setup-node@v3.8.1 with: - path: kafka-ui-react-app/node_modules - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- + node-version: "18.17.1" + cache: "pnpm" + cache-dependency-path: "./kafka-ui-react-app/pnpm-lock.yaml" - name: Install Node dependencies run: | cd kafka-ui-react-app/ - npm install + pnpm install --frozen-lockfile - name: Generate sources run: | cd kafka-ui-react-app/ - npm run gen:sources + pnpm gen:sources - name: Linter run: | cd kafka-ui-react-app/ - npm run lint:CI + pnpm lint:CI - name: Tests run: | cd kafka-ui-react-app/ - npm run test:CI + pnpm test:CI - name: SonarCloud Scan - uses: workshur/sonarcloud-github-action@improved_basedir + uses: sonarsource/sonarcloud-github-action@master with: projectBaseDir: ./kafka-ui-react-app args: -Dsonar.pullrequest.key=${{ github.event.pull_request.number }} -Dsonar.pullrequest.branch=${{ github.head_ref }} -Dsonar.pullrequest.base=${{ github.base_ref }} diff --git a/.github/workflows/helm.yaml b/.github/workflows/helm.yaml deleted file mode 100644 index 664a15e8a73..00000000000 --- a/.github/workflows/helm.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: Helm -on: - pull_request: - types: [ 'opened', 'edited', 'reopened', 'synchronize' ] - paths: - - "charts/**" - - schedule: - # * is a special character in YAML so you have to quote this string - - cron: '0 8 * * 3' - -jobs: - build-and-test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Helm tool installer - uses: Azure/setup-helm@v1 - - name: Setup Kubeval - uses: lra/setup-kubeval@v1.0.1 - - name: Run kubeval - shell: bash - run: | - sed -i "s@enabled: false@enabled: true@g" charts/kafka-ui/values.yaml - K8S_VERSIONS=$(git ls-remote --refs --tags https://github.com/kubernetes/kubernetes.git | cut -d/ -f3 | grep -e '^v1\.[0-9]\{2\}\.[0]\{1,2\}$' | grep -v -e '^v1\.1[0-8]\{1\}' | cut -c2-) - echo "NEXT K8S VERSIONS ARE GOING TO BE TESTED: $K8S_VERSIONS" - echo "" - for version in $K8S_VERSIONS - do - echo $version; - helm template charts/kafka-ui -f charts/kafka-ui/values.yaml | kubeval --additional-schema-locations https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master --strict -v $version; - done diff --git a/.github/workflows/master.yaml b/.github/workflows/master.yaml index 1d1aa1f130b..d751e500210 100644 --- a/.github/workflows/master.yaml +++ b/.github/workflows/master.yaml @@ -1,4 +1,4 @@ -name: Master +name: "Master: Build & deploy" on: workflow_dispatch: push: @@ -9,37 +9,33 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - - name: Cache local Maven repository - uses: actions/cache@v3 with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- + ref: ${{ github.event.pull_request.head.sha }} - - name: Set up JDK 1.13 - uses: actions/setup-java@v1 + - name: Set up JDK + uses: actions/setup-java@v3 with: - java-version: 1.13 + java-version: '17' + distribution: 'zulu' + cache: 'maven' - name: Build id: build run: | - mvn versions:set -DnewVersion=$GITHUB_SHA - mvn clean package -Pprod -DskipTests - export VERSION=$(mvn -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec) - echo "::set-output name=version::${VERSION}" + ./mvnw -B -ntp versions:set -DnewVersion=$GITHUB_SHA + ./mvnw -V -B -ntp clean package -Pprod -DskipTests + export VERSION=$(./mvnw -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec) + echo "version=${VERSION}" >> $GITHUB_OUTPUT ################# # # # Docker images # # # ################# - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Cache Docker layers uses: actions/cache@v3 @@ -50,18 +46,19 @@ jobs: ${{ runner.os }}-buildx- - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build_and_push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4 with: builder: ${{ steps.buildx.outputs.name }} context: kafka-ui-api platforms: linux/amd64,linux/arm64 + provenance: false push: true tags: | provectuslabs/kafka-ui:${{ steps.build.outputs.version }} @@ -77,11 +74,11 @@ jobs: ################################# - name: update-master-deployment run: | - git clone https://kafka-ui-infra:${{ secrets.KAFKA_UI_INFRA_TOKEN }}@gitlab.provectus.com/provectus-internals/kafka-ui-infra.git --branch master + git clone https://infra-tech:${{ secrets.INFRA_USER_ACCESS_TOKEN }}@github.com/provectus/kafka-ui-infra.git --branch master cd kafka-ui-infra/aws-infrastructure4eks/argocd/scripts echo "Image digest is:${{ steps.docker_build_and_push.outputs.digest }}" ./kafka-ui-update-master-digest.sh ${{ steps.docker_build_and_push.outputs.digest }} - git config --global user.email "kafka-ui-infra@provectus.com" - git config --global user.name "kafka-ui-infra" + git config --global user.email "infra-tech@provectus.com" + git config --global user.name "infra-tech" git add ../kafka-ui/* git commit -m "updated master image digest: ${{ steps.docker_build_and_push.outputs.digest }}" && git push diff --git a/.github/workflows/pr-checks.yaml b/.github/workflows/pr-checks.yaml index 74ff75b833d..ce7dd17ae40 100644 --- a/.github/workflows/pr-checks.yaml +++ b/.github/workflows/pr-checks.yaml @@ -1,13 +1,14 @@ -name: "PR Checklist checked" +name: "PR: Checklist linter" on: pull_request_target: types: [opened, edited, synchronize, reopened] - +permissions: + checks: write jobs: task-check: runs-on: ubuntu-latest steps: - - uses: kentaro-m/task-completed-checker-action@v0.1.0 + - uses: kentaro-m/task-completed-checker-action@v0.1.2 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" - uses: dekinderfiets/pr-description-enforcer@0.0.1 diff --git a/.github/workflows/release-serde-api.yaml b/.github/workflows/release-serde-api.yaml new file mode 100644 index 00000000000..44e5babc7e9 --- /dev/null +++ b/.github/workflows/release-serde-api.yaml @@ -0,0 +1,30 @@ +name: "Infra: Release: Serde API" +on: workflow_dispatch + +jobs: + release-serde-api: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - run: | + git config user.name github-actions + git config user.email github-actions@github.com + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: "17" + distribution: "zulu" + cache: "maven" + + - id: install-secret-key + name: Install GPG secret key + run: | + cat <(echo -e "${{ secrets.GPG_PRIVATE_KEY }}") | gpg --batch --import + + - name: Publish to Maven Central + run: | + mvn source:jar javadoc:jar package gpg:sign -Dgpg.passphrase=${{ secrets.GPG_PASSPHRASE }} -Dserver.username=${{ secrets.NEXUS_USERNAME }} -Dserver.password=${{ secrets.NEXUS_PASSWORD }} nexus-staging:deploy -pl kafka-ui-serde-api -s settings.xml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7a21d3e804d..4c2837f1af0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,4 +1,4 @@ -name: Release +name: "Infra: Release" on: release: types: [published] @@ -12,34 +12,29 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} - run: | git config user.name github-actions git config user.email github-actions@github.com - - name: Cache local Maven repository - uses: actions/cache@v3 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - - - name: Set up JDK 1.13 - uses: actions/setup-java@v1 + - name: Set up JDK + uses: actions/setup-java@v3 with: - java-version: 1.13 + java-version: '17' + distribution: 'zulu' + cache: 'maven' - name: Build with Maven id: build run: | - mvn versions:set -DnewVersion=${{ github.event.release.tag_name }} - mvn clean package -Pprod -DskipTests - export VERSION=$(mvn -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec) - echo ::set-output name=version::${VERSION} + ./mvnw -B -ntp versions:set -DnewVersion=${{ github.event.release.tag_name }} + ./mvnw -B -V -ntp clean package -Pprod -DskipTests + export VERSION=$(./mvnw -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec) + echo "version=${VERSION}" >> $GITHUB_OUTPUT - name: Upload files to a GitHub release - uses: svenstaro/upload-release-action@2.2.1 + uses: svenstaro/upload-release-action@2.7.0 with: repo_token: ${{ secrets.GITHUB_TOKEN }} file: kafka-ui-api/target/kafka-ui-api-${{ steps.build.outputs.version }}.jar @@ -56,10 +51,10 @@ jobs: # # ################# - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Cache Docker layers uses: actions/cache@v3 @@ -70,18 +65,19 @@ jobs: ${{ runner.os }}-buildx- - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build_and_push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4 with: builder: ${{ steps.buildx.outputs.name }} context: kafka-ui-api platforms: linux/amd64,linux/arm64 + provenance: false push: true tags: | provectuslabs/kafka-ui:${{ steps.build.outputs.version }} @@ -95,30 +91,10 @@ jobs: runs-on: ubuntu-latest needs: release steps: - - uses: actions/checkout@v3 + - name: Repository Dispatch + uses: peter-evans/repository-dispatch@v2 with: - fetch-depth: 1 - - - run: | - git config user.name github-actions - git config user.email github-actions@github.com - - - uses: azure/setup-helm@v1 - - - name: update chart version - run: | - export version=${{needs.release.outputs.version}} - sed -i "s/version:.*/version: ${version}/" charts/kafka-ui/Chart.yaml - sed -i "s/appVersion:.*/appVersion: ${version}/" charts/kafka-ui/Chart.yaml - - - name: add chart - run: | - export VERSION=${{needs.release.outputs.version}} - MSG=$(helm package --app-version ${VERSION} charts/kafka-ui) - git fetch origin - git stash - git checkout -b gh-pages origin/gh-pages - helm repo index . - git add -f ${MSG##*/} index.yaml - git commit -m "release ${VERSION}" - git push + token: ${{ secrets.CHARTS_ACTIONS_TOKEN }} + repository: provectus/kafka-ui-charts + event-type: prepare-helm-release + client-payload: '{"appversion": "${{ needs.release.outputs.version }}"}' diff --git a/.github/workflows/release_drafter.yml b/.github/workflows/release_drafter.yml index 742254b942e..d313edac3c4 100644 --- a/.github/workflows/release_drafter.yml +++ b/.github/workflows/release_drafter.yml @@ -1,19 +1,34 @@ -name: Release Drafter +name: "Infra: Release Drafter run" on: push: - # branches to consider in the event; optional, defaults to all branches: - master workflow_dispatch: + inputs: + version: + description: 'Release version' + required: false + branch: + description: 'Target branch' + required: false + default: 'master' + +permissions: + contents: read jobs: update_release_draft: runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write steps: - uses: release-drafter/release-drafter@v5 with: config-name: release_drafter.yaml disable-autolabeler: true + version: ${{ github.event.inputs.version }} + commitish: ${{ github.event.inputs.branch }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/separate_env_public_create.yml b/.github/workflows/separate_env_public_create.yml index eb59e55f7ad..cac2d444ecd 100644 --- a/.github/workflows/separate_env_public_create.yml +++ b/.github/workflows/separate_env_public_create.yml @@ -1,4 +1,4 @@ -name: Separate environment create +name: "Infra: Feature Testing Public: Init env" on: workflow_dispatch: inputs: @@ -8,19 +8,82 @@ on: default: 'demo' jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: get branch name + id: extract_branch + run: | + tag="${{ github.event.inputs.ENV_NAME }}-$(date '+%F-%H-%M-%S')" + echo "tag=${tag}" >> $GITHUB_OUTPUT + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'zulu' + cache: 'maven' + - name: Build + id: build + run: | + ./mvnw -B -ntp versions:set -DnewVersion=$GITHUB_SHA + ./mvnw -B -V -ntp clean package -Pprod -DskipTests + export VERSION=$(./mvnw -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec) + echo "version=${VERSION}" >> $GITHUB_OUTPUT + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + - name: Configure AWS credentials for Kafka-UI account + uses: aws-actions/configure-aws-credentials@v3 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: eu-central-1 + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + - name: Build and push + id: docker_build_and_push + uses: docker/build-push-action@v4 + with: + builder: ${{ steps.buildx.outputs.name }} + context: kafka-ui-api + push: true + tags: 297478128798.dkr.ecr.eu-central-1.amazonaws.com/kafka-ui:${{ steps.extract_branch.outputs.tag }} + build-args: | + JAR_FILE=kafka-ui-api-${{ steps.build.outputs.version }}.jar + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache + outputs: + tag: ${{ steps.extract_branch.outputs.tag }} + separate-env-create: runs-on: ubuntu-latest + needs: build steps: - name: clone run: | - git clone https://kafka-ui-infra:${{ secrets.KAFKA_UI_INFRA_TOKEN }}@gitlab.provectus.com/provectus-internals/kafka-ui-infra.git + git clone https://infra-tech:${{ secrets.INFRA_USER_ACCESS_TOKEN }}@github.com/provectus/kafka-ui-infra.git --branch envs - name: separate env create run: | cd kafka-ui-infra/aws-infrastructure4eks/argocd/scripts - bash separate_env_create.sh ${{ github.event.inputs.ENV_NAME }} ${{ secrets.FEATURE_TESTING_UI_PASSWORD }} - git config --global user.email "kafka-ui-infra@provectus.com" - git config --global user.name "kafka-ui-infra" + bash separate_env_create.sh ${{ github.event.inputs.ENV_NAME }} ${{ secrets.FEATURE_TESTING_UI_PASSWORD }} ${{ needs.build.outputs.tag }} + git config --global user.email "infra-tech@provectus.com" + git config --global user.name "infra-tech" git add -A git commit -m "separate env added: ${{ github.event.inputs.ENV_NAME }}" && git push || true diff --git a/.github/workflows/separate_env_public_remove.yml b/.github/workflows/separate_env_public_remove.yml index 19084801377..145be002c9f 100644 --- a/.github/workflows/separate_env_public_remove.yml +++ b/.github/workflows/separate_env_public_remove.yml @@ -1,4 +1,4 @@ -name: Separate environment remove +name: "Infra: Feature Testing Public: Destroy env" on: workflow_dispatch: inputs: @@ -13,12 +13,12 @@ jobs: steps: - name: clone run: | - git clone https://kafka-ui-infra:${{ secrets.KAFKA_UI_INFRA_TOKEN }}@gitlab.provectus.com/provectus-internals/kafka-ui-infra.git + git clone https://infra-tech:${{ secrets.INFRA_USER_ACCESS_TOKEN }}@github.com/provectus/kafka-ui-infra.git --branch envs - name: separate environment remove run: | cd kafka-ui-infra/aws-infrastructure4eks/argocd/scripts bash separate_env_remove.sh ${{ github.event.inputs.ENV_NAME }} - git config --global user.email "kafka-ui-infra@provectus.com" - git config --global user.name "kafka-ui-infra" + git config --global user.email "infra-tech@provectus.com" + git config --global user.name "infra-tech" git add -A git commit -m "separate env removed: ${{ github.event.inputs.ENV_NAME }}" && git push || true diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 5e9ac844fb0..cb9870c5208 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -1,4 +1,4 @@ -name: 'Close stale issues' +name: 'Infra: Close stale issues' on: schedule: - cron: '30 1 * * *' @@ -7,7 +7,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v5 + - uses: actions/stale@v8 with: days-before-issue-stale: 7 days-before-issue-close: 3 diff --git a/.github/workflows/terraform-deploy.yml b/.github/workflows/terraform-deploy.yml index db2f651e038..e42d52b11a1 100644 --- a/.github/workflows/terraform-deploy.yml +++ b/.github/workflows/terraform-deploy.yml @@ -1,4 +1,4 @@ -name: terraform_deploy +name: "Infra: Terraform deploy" on: workflow_dispatch: inputs: @@ -26,18 +26,14 @@ jobs: echo "Terraform will be triggered in this dir $TF_DIR" - name: Configure AWS credentials for Kafka-UI account - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v3 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: eu-central-1 - name: Terraform Install - uses: hashicorp/setup-terraform@v1 - - - name: Terraform format - id: fmt - run: cd $TF_DIR && terraform fmt -check + uses: hashicorp/setup-terraform@v2 - name: Terraform init id: init diff --git a/.github/workflows/triage_issues.yml b/.github/workflows/triage_issues.yml index 8009a2a64bd..344ba5c118d 100644 --- a/.github/workflows/triage_issues.yml +++ b/.github/workflows/triage_issues.yml @@ -1,4 +1,4 @@ -name: Add triage label to new issues +name: "Infra: Triage: Apply triage label for issues" on: issues: types: diff --git a/.github/workflows/triage_prs.yml b/.github/workflows/triage_prs.yml index 90d76936036..6906cd8a8a5 100644 --- a/.github/workflows/triage_prs.yml +++ b/.github/workflows/triage_prs.yml @@ -1,4 +1,4 @@ -name: Add triage label to new PRs +name: "Infra: Triage: Apply triage label for PRs" on: pull_request: types: diff --git a/.github/workflows/welcome-first-time-contributors.yml b/.github/workflows/welcome-first-time-contributors.yml index b0258c9235b..1ac861055cc 100644 --- a/.github/workflows/welcome-first-time-contributors.yml +++ b/.github/workflows/welcome-first-time-contributors.yml @@ -7,7 +7,9 @@ on: issues: types: - opened - +permissions: + issues: write + pull-requests: write jobs: welcome: runs-on: ubuntu-latest diff --git a/.github/workflows/workflow_linter.yaml b/.github/workflows/workflow_linter.yaml index b4af45d57ef..df9983a5301 100644 --- a/.github/workflows/workflow_linter.yaml +++ b/.github/workflows/workflow_linter.yaml @@ -1,4 +1,4 @@ -name: "Workflow linter" +name: "Infra: Workflow linter" on: pull_request: types: diff --git a/.gitignore b/.gitignore index 55b770349f8..a12e7753760 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ build/ .vscode/ /kafka-ui-api/app/node +### SDKMAN ### +.sdkmanrc + .DS_Store *.code-workspace diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java deleted file mode 100644 index e76d1f3241d..00000000000 --- a/.mvn/wrapper/MavenWrapperDownloader.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2007-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import java.net.*; -import java.io.*; -import java.nio.channels.*; -import java.util.Properties; - -public class MavenWrapperDownloader { - - private static final String WRAPPER_VERSION = "0.5.6"; - /** - * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. - */ - private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" - + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; - - /** - * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to - * use instead of the default one. - */ - private static final String MAVEN_WRAPPER_PROPERTIES_PATH = - ".mvn/wrapper/maven-wrapper.properties"; - - /** - * Path where the maven-wrapper.jar will be saved to. - */ - private static final String MAVEN_WRAPPER_JAR_PATH = - ".mvn/wrapper/maven-wrapper.jar"; - - /** - * Name of the property which should be used to override the default download url for the wrapper. - */ - private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; - - public static void main(String args[]) { - System.out.println("- Downloader started"); - File baseDirectory = new File(args[0]); - System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); - - // If the maven-wrapper.properties exists, read it and check if it contains a custom - // wrapperUrl parameter. - File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); - String url = DEFAULT_DOWNLOAD_URL; - if(mavenWrapperPropertyFile.exists()) { - FileInputStream mavenWrapperPropertyFileInputStream = null; - try { - mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); - Properties mavenWrapperProperties = new Properties(); - mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); - url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); - } catch (IOException e) { - System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); - } finally { - try { - if(mavenWrapperPropertyFileInputStream != null) { - mavenWrapperPropertyFileInputStream.close(); - } - } catch (IOException e) { - // Ignore ... - } - } - } - System.out.println("- Downloading from: " + url); - - File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); - if(!outputFile.getParentFile().exists()) { - if(!outputFile.getParentFile().mkdirs()) { - System.out.println( - "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); - } - } - System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); - try { - downloadFileFromURL(url, outputFile); - System.out.println("Done"); - System.exit(0); - } catch (Throwable e) { - System.out.println("- Error downloading"); - e.printStackTrace(); - System.exit(1); - } - } - - private static void downloadFileFromURL(String urlString, File destination) throws Exception { - if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { - String username = System.getenv("MVNW_USERNAME"); - char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); - Authenticator.setDefault(new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(username, password); - } - }); - } - URL website = new URL(urlString); - ReadableByteChannel rbc; - rbc = Channels.newChannel(website.openStream()); - FileOutputStream fos = new FileOutputStream(destination); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - fos.close(); - rbc.close(); - } - -} diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar index 2cc7d4a55c0..bf82ff01c6c 100644 Binary files a/.mvn/wrapper/maven-wrapper.jar and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 642d572ce90..dc3affce3dd 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,2 +1,18 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c01826ba493..ab17417cf97 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,5 @@ +This guide is an exact copy of the same documented located [in our official docs](https://docs.kafka-ui.provectus.io/development/contributing). If there are any differences between the documents, the one located in our official docs should prevail. + This guide aims to walk you through the process of working on issues and Pull Requests (PRs). Bear in mind that you will not be able to complete some steps on your own if you do not have a “write” permission. Feel free to reach out to the maintainers to help you unlock these activities. @@ -20,7 +22,7 @@ You also need to consider labels. You can sort the issues by scope labels, such ## Grabbing the issue There is a bunch of criteria that make an issue feasible for development.
-The implementation of any features and/or their enhancements should be reasonable, must be backed by justified requirements (demanded by the community, [roadmap](documentation/project/ROADMAP.md) plans, etc.). The final decision is left for the maintainers' discretion. +The implementation of any features and/or their enhancements should be reasonable, must be backed by justified requirements (demanded by the community, [roadmap](https://docs.kafka-ui.provectus.io/project/roadmap) plans, etc.). The final decision is left for the maintainers' discretion. All bugs should be confirmed as such (i.e. the behavior is unintended). @@ -39,7 +41,7 @@ To keep the status of the issue clear to everyone, please keep the card's status ## Setting up a local development environment -Please refer to [this guide](documentation/project/contributing/README.md). +Please refer to [this guide](https://docs.kafka-ui.provectus.io/development/contributing). # Pull Requests @@ -78,6 +80,7 @@ When creating a PR please do the following: 4. If the PR does not close any of the issues, the PR itself might need to have a milestone set. Reach out to the maintainers to consult. 5. Assign the PR to yourself. A PR assignee is someone whose goal is to get the PR merged. 6. Add reviewers. As a rule, reviewers' suggestions are pretty good; please use them. +7. Upon merging the PR, please use a meaningful commit message, task name should be fine in this case. ### Pull Request checklist diff --git a/README.md b/README.md index 765239512ac..f6a16c862c3 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,37 @@ ![UI for Apache Kafka logo](documentation/images/kafka-ui-logo.png) UI for Apache Kafka  ------------------ #### Versatile, fast and lightweight web UI for managing Apache Kafka® clusters. Built by developers, for developers. +
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/provectus/kafka-ui/blob/master/LICENSE) ![UI for Apache Kafka Price Free](documentation/images/free-open-source.svg) [![Release version](https://img.shields.io/github/v/release/provectus/kafka-ui)](https://github.com/provectus/kafka-ui/releases) [![Chat with us](https://img.shields.io/discord/897805035122077716)](https://discord.gg/4DWzD7pGE5) +[![Docker pulls](https://img.shields.io/docker/pulls/provectuslabs/kafka-ui)](https://hub.docker.com/r/provectuslabs/kafka-ui) -### DISCLAIMER -UI for Apache Kafka is a free, open-source tool that is curated by Provectus, and is built and supported by the open-source community. The tool will remain free and open-source in the future. Provectus does not plan to add any paid features or subscription plans so that everyone can have a better experience observing their data. UI for Apache Kafka is a part of the [Provectus NextGen Data Platform](https://provectus.com/nextgen-data-platform/). Check it out for more details! +

+ DOCS • + QUICK START • + COMMUNITY DISCORD +
+ AWS Marketplace • + ProductHunt +

+ +

+ +

-#### UI for Apache Kafka is a free, open-source web UI to monitor and manage Apache Kafka clusters. +#### UI for Apache Kafka is a free, open-source web UI to monitor and manage Apache Kafka clusters. -UI for Apache Kafka is a simple tool that makes your data flows observable, helps find and troubleshoot issues faster and deliver optimal performance. Its lightweight dashboard makes it easy to track key metrics of your Kafka clusters - Brokers, Topics, Partitions, Production, and Consumption. +UI for Apache Kafka is a simple tool that makes your data flows observable, helps find and troubleshoot issues faster and deliver optimal performance. Its lightweight dashboard makes it easy to track key metrics of your Kafka clusters - Brokers, Topics, Partitions, Production, and Consumption. -Set up UI for Apache Kafka with just a couple of easy commands to visualize your Kafka data in a comprehensible way. You can run the tool locally or in -the cloud. +### DISCLAIMER +UI for Apache Kafka is a free tool built and supported by the open-source community. Curated by Provectus, it will remain free and open-source, without any paid features or subscription plans to be added in the future. +Looking for the help of Kafka experts? Provectus can help you design, build, deploy, and manage Apache Kafka clusters and streaming applications. Discover [Professional Services for Apache Kafka](https://provectus.com/professional-services-apache-kafka/), to unlock the full potential of Kafka in your enterprise! + +Set up UI for Apache Kafka with just a couple of easy commands to visualize your Kafka data in a comprehensible way. You can run the tool locally or in +the cloud. ![Interface](documentation/images/Interface.gif) @@ -27,21 +43,24 @@ the cloud. * **View Consumer Groups** — view per-partition parked offsets, combined and per-partition lag * **Browse Messages** — browse messages with JSON, plain text, and Avro encoding * **Dynamic Topic Configuration** — create and configure new topics with dynamic configuration -* **Configurable Authentification** — secure your installation with optional Github/Gitlab/Google OAuth 2.0 - +* **Configurable Authentification** — [secure](https://docs.kafka-ui.provectus.io/configuration/authentication) your installation with optional Github/Gitlab/Google OAuth 2.0 +* **Custom serialization/deserialization plugins** - [use](https://docs.kafka-ui.provectus.io/configuration/serialization-serde) a ready-to-go serde for your data like AWS Glue or Smile, or code your own! +* **Role based access control** - [manage permissions](https://docs.kafka-ui.provectus.io/configuration/rbac-role-based-access-control) to access the UI with granular precision +* **Data masking** - [obfuscate](https://docs.kafka-ui.provectus.io/configuration/data-masking) sensitive data in topic messages + # The Interface UI for Apache Kafka wraps major functions of Apache Kafka with an intuitive user interface. ![Interface](documentation/images/Interface.gif) ## Topics -UI for Apache Kafka makes it easy for you to create topics in your browser by several clicks, +UI for Apache Kafka makes it easy for you to create topics in your browser by several clicks, pasting your own parameters, and viewing topics in the list. ![Create Topic](documentation/images/Create_topic_kafka-ui.gif) It's possible to jump from connectors view to corresponding topics and from a topic to consumers (back and forth) for more convenient navigation. -connectors, overview topic settings. +connectors, overview topic settings. ![Connector_Topic_Consumer](documentation/images/Connector_Topic_Consumer.gif) @@ -55,123 +74,68 @@ There are 3 supported types of schemas: Avro®, JSON Schema, and Protobuf schema ![Create Schema Registry](documentation/images/Create_schema.gif) -Before producing avro-encoded messages, you have to add an avro schema for the topic in Schema Registry. Now all these steps are easy to do +Before producing avro/protobuf encoded messages, you have to add a schema for the topic in Schema Registry. Now all these steps are easy to do with a few clicks in a user-friendly interface. ![Avro Schema Topic](documentation/images/Schema_Topic.gif) # Getting Started -To run UI for Apache Kafka, you can use a pre-built Docker image or build it locally. +To run UI for Apache Kafka, you can use either a pre-built Docker image or build it (or a jar file) yourself. -## Configuration +## Quick start (Demo run) -We have plenty of [docker-compose files](documentation/compose/DOCKER_COMPOSE.md) as examples. They're built for various configuration stacks. - -# Guides - -- [SSO configuration](documentation/guides/SSO.md) -- [AWS IAM configuration](documentation/guides/AWS_IAM.md) -- [Docker-compose files](documentation/compose/DOCKER_COMPOSE.md) -- [Connection to a secure broker](documentation/compose/SECURE_BROKER.md) - -### Configuration File -Example of how to configure clusters in the [application-local.yml](https://github.com/provectus/kafka-ui/blob/master/kafka-ui-api/src/main/resources/application-local.yml) configuration file: +``` +docker run -it -p 8080:8080 -e DYNAMIC_CONFIG_ENABLED=true provectuslabs/kafka-ui +``` +Then access the web UI at [http://localhost:8080](http://localhost:8080) -```sh -kafka: - clusters: - - - name: local - bootstrapServers: localhost:29091 - schemaRegistry: http://localhost:8085 - schemaRegistryAuth: - username: username - password: password -# schemaNameTemplate: "%s-value" - jmxPort: 9997 - - -``` +The command is sufficient to try things out. When you're done trying things out, you can proceed with a [persistent installation](https://docs.kafka-ui.provectus.io/quick-start/persistent-start) -* `name`: cluster name -* `bootstrapServers`: where to connect -* `schemaRegistry`: schemaRegistry's address -* `schemaRegistryAuth.username`: schemaRegistry's basic authentication username -* `schemaRegistryAuth.password`: schemaRegistry's basic authentication password -* `schemaNameTemplate`: how keys are saved to schemaRegistry -* `jmxPort`: open jmxPosrts of a broker -* `readOnly`: enable read only mode +## Persistent installation -Configure as many clusters as you need by adding their configs below separated with `-`. +``` +services: + kafka-ui: + container_name: kafka-ui + image: provectuslabs/kafka-ui:latest + ports: + - 8080:8080 + environment: + DYNAMIC_CONFIG_ENABLED: 'true' + volumes: + - ~/kui/config.yml:/etc/kafkaui/dynamic_config.yaml +``` -## Running a Docker Image -The official Docker image for UI for Apache Kafka is hosted here: [hub.docker.com/r/provectuslabs/kafka-ui](https://hub.docker.com/r/provectuslabs/kafka-ui). +Please refer to our [configuration](https://docs.kafka-ui.provectus.io/configuration/quick-start) page to proceed with further app configuration. -Launch Docker container in the background: -```sh +## Some useful configuration related links -docker run -p 8080:8080 \ - -e KAFKA_CLUSTERS_0_NAME=local \ - -e KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=kafka:9092 \ - -d provectuslabs/kafka-ui:latest +[Web UI Cluster Configuration Wizard](https://docs.kafka-ui.provectus.io/configuration/configuration-wizard) -``` -Then access the web UI at [http://localhost:8080](http://localhost:8080). -Further configuration with environment variables - [see environment variables](#env_variables) - -### Docker Compose +[Configuration file explanation](https://docs.kafka-ui.provectus.io/configuration/configuration-file) -If you prefer to use `docker-compose` please refer to the [documentation](docker-compose.md). +[Docker Compose examples](https://docs.kafka-ui.provectus.io/configuration/compose-examples) +[Misc configuration properties](https://docs.kafka-ui.provectus.io/configuration/misc-configuration-properties) -## Building With Docker +## Helm charts -### Prerequisites +[Quick start](https://docs.kafka-ui.provectus.io/configuration/helm-charts/quick-start) -Check [software-required.md](documentation/project/contributing/software-required.md) +## Building from sources -### Building +[Quick start](https://docs.kafka-ui.provectus.io/development/building/prerequisites) with building -Check [building.md](documentation/project/contributing/building.md) +## Liveliness and readiness probes +Liveliness and readiness endpoint is at `/actuator/health`.
+Info endpoint (build info) is located at `/actuator/info`. -### Running +# Configuration options -Check [running.md](documentation/project/contributing/running.md) +All of the environment variables/config properties could be found [here](https://docs.kafka-ui.provectus.io/configuration/misc-configuration-properties). -## Liveliness and readiness probes -Liveliness and readiness endpoint is at `/actuator/health`. -Info endpoint (build info) is located at `/actuator/info`. +# Contributing -## Environment Variables - -Alternatively, each variable of the .yml file can be set with an environment variable. -For example, if you want to use an environment variable to set the `name` parameter, you can write it like this: `KAFKA_CLUSTERS_2_NAME` - -|Name |Description -|-----------------------|------------------------------- -|`SERVER_SERVLET_CONTEXT_PATH` | URI basePath -|`LOGGING_LEVEL_ROOT` | Setting log level (trace, debug, info, warn, error). Default: info -|`LOGGING_LEVEL_COM_PROVECTUS` |Setting log level (trace, debug, info, warn, error). Default: debug -|`SERVER_PORT` |Port for the embedded server. Default: `8080` -|`KAFKA_ADMIN-CLIENT-TIMEOUT` | Kafka API timeout in ms. Default: `30000` -|`KAFKA_CLUSTERS_0_NAME` | Cluster name -|`KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS` |Address where to connect -|`KAFKA_CLUSTERS_0_KSQLDBSERVER` | KSQL DB server address -|`KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL` |Security protocol to connect to the brokers. For SSL connection use "SSL", for plaintext connection don't set this environment variable -|`KAFKA_CLUSTERS_0_SCHEMAREGISTRY` |SchemaRegistry's address -|`KAFKA_CLUSTERS_0_SCHEMAREGISTRYAUTH_USERNAME` |SchemaRegistry's basic authentication username -|`KAFKA_CLUSTERS_0_SCHEMAREGISTRYAUTH_PASSWORD` |SchemaRegistry's basic authentication password -|`KAFKA_CLUSTERS_0_SCHEMANAMETEMPLATE` |How keys are saved to schemaRegistry -|`KAFKA_CLUSTERS_0_JMXPORT` |Open jmxPosrts of a broker -|`KAFKA_CLUSTERS_0_READONLY` |Enable read-only mode. Default: false -|`KAFKA_CLUSTERS_0_DISABLELOGDIRSCOLLECTION` |Disable collecting segments information. It should be true for confluent cloud. Default: false -|`KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME` |Given name for the Kafka Connect cluster -|`KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS` |Address of the Kafka Connect service endpoint -|`KAFKA_CLUSTERS_0_KAFKACONNECT_0_USERNAME`| Kafka Connect cluster's basic authentication username -|`KAFKA_CLUSTERS_0_KAFKACONNECT_0_PASSWORD`| Kafka Connect cluster's basic authentication password -|`KAFKA_CLUSTERS_0_JMXSSL` |Enable SSL for JMX? `true` or `false`. For advanced setup, see `kafka-ui-jmx-secured.yml` -|`KAFKA_CLUSTERS_0_JMXUSERNAME` |Username for JMX authentication -|`KAFKA_CLUSTERS_0_JMXPASSWORD` |Password for JMX authentication -|`TOPIC_RECREATE_DELAY_SECONDS` |Time delay between topic deletion and topic creation attempts for topic recreate functionality. Default: 1 -|`TOPIC_RECREATE_MAXRETRIES` |Number of attempts of topic creation after topic deletion for topic recreate functionality. Default: 15 +Please refer to [contributing guide](https://docs.kafka-ui.provectus.io/development/contributing), we'll guide you from there. diff --git a/SECURITY.md b/SECURITY.md index 26e7552b42b..318166dd606 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,10 @@ Following versions of the project are currently being supported with security up | Version | Supported | | ------- | ------------------ | -| 0.4.x | :white_check_mark: | +| 0.7.x | :white_check_mark: | +| 0.6.x | :x: | +| 0.5.x | :x: | +| 0.4.x | :x: | | 0.3.x | :x: | | 0.2.x | :x: | | 0.1.x | :x: | diff --git a/charts/kafka-ui/.helmignore b/charts/kafka-ui/.helmignore deleted file mode 100644 index 7a93969f5a0..00000000000 --- a/charts/kafka-ui/.helmignore +++ /dev/null @@ -1,25 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ -example/ -README.md diff --git a/charts/kafka-ui/Chart.yaml b/charts/kafka-ui/Chart.yaml deleted file mode 100644 index f1be768f337..00000000000 --- a/charts/kafka-ui/Chart.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v2 -name: kafka-ui -description: A Helm chart for kafka-UI -type: application -version: 0.0.3 -appVersion: latest -icon: https://github.com/provectus/kafka-ui/raw/master/images/kafka-ui-logo.png diff --git a/charts/kafka-ui/README.md b/charts/kafka-ui/README.md deleted file mode 100644 index eac7aa04fd2..00000000000 --- a/charts/kafka-ui/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Kafka-UI Helm Chart - -## Configuration - -Most of the Helm charts parameters are common, follow table describe unique parameters related to application configuration. - -### Kafka-UI parameters - -| Parameter| Description| Default| -|---|---|---| -| `existingConfigMap`| Name of the existing ConfigMap with Kafka-UI environment variables | `nil`| -| `existingSecret`| Name of the existing Secret with Kafka-UI environment variables| `nil`| -| `envs.secret`| Set of the sensitive environment variables to pass to Kafka-UI | `{}`| -| `envs.config`| Set of the environment variables to pass to Kafka-UI | `{}`| -| `networkPolicy.enabled` | Enable network policies | `false`| -| `networkPolicy.egressRules.customRules` | Custom network egress policy rules | `[]`| -| `networkPolicy.ingressRules.customRules` | Custom network ingress policy rules | `[]`| -| `podLabels` | Extra labels for Kafka-UI pod | `{}`| - -## Example - -To install Kafka-UI need to execute follow: -``` bash -helm repo add kafka-ui https://provectus.github.io/kafka-ui -helm install kafka-ui kafka-ui/kafka-ui --set envs.config.KAFKA_CLUSTERS_0_NAME=local --set envs.config.KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=kafka:9092 -``` -To connect to Kafka-UI web application need to execute: -``` bash -kubectl port-forward svc/kafka-ui 8080:80 -``` -Open the `http://127.0.0.1:8080` on the browser to access Kafka-UI. diff --git a/charts/kafka-ui/index.yaml b/charts/kafka-ui/index.yaml deleted file mode 100644 index 872807193db..00000000000 --- a/charts/kafka-ui/index.yaml +++ /dev/null @@ -1,3 +0,0 @@ -apiVersion: v1 -entries: {} -generated: "2021-11-11T12:26:08.479581+03:00" diff --git a/charts/kafka-ui/templates/NOTES.txt b/charts/kafka-ui/templates/NOTES.txt deleted file mode 100644 index 94e8d394344..00000000000 --- a/charts/kafka-ui/templates/NOTES.txt +++ /dev/null @@ -1,21 +0,0 @@ -1. Get the application URL by running these commands: -{{- if .Values.ingress.enabled }} -{{- range $host := .Values.ingress.hosts }} - {{- range .paths }} - http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} - {{- end }} -{{- end }} -{{- else if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "kafka-ui.fullname" . }}) - export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") - echo http://$NODE_IP:$NODE_PORT -{{- else if contains "LoadBalancer" .Values.service.type }} - NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "kafka-ui.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "kafka-ui.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") - echo http://$SERVICE_IP:{{ .Values.service.port }} -{{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "kafka-ui.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") - echo "Visit http://127.0.0.1:8080 to use your application" - kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:8080 -{{- end }} diff --git a/charts/kafka-ui/templates/_helpers.tpl b/charts/kafka-ui/templates/_helpers.tpl deleted file mode 100644 index 076c4886f80..00000000000 --- a/charts/kafka-ui/templates/_helpers.tpl +++ /dev/null @@ -1,63 +0,0 @@ -{{/* vim: set filetype=mustache: */}} -{{/* -Expand the name of the chart. -*/}} -{{- define "kafka-ui.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "kafka-ui.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "kafka-ui.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "kafka-ui.labels" -}} -helm.sh/chart: {{ include "kafka-ui.chart" . }} -{{ include "kafka-ui.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "kafka-ui.selectorLabels" -}} -app.kubernetes.io/name: {{ include "kafka-ui.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define "kafka-ui.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "kafka-ui.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} diff --git a/charts/kafka-ui/templates/configmap.yaml b/charts/kafka-ui/templates/configmap.yaml deleted file mode 100644 index c802e575222..00000000000 --- a/charts/kafka-ui/templates/configmap.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ include "kafka-ui.fullname" . }} - labels: - {{- include "kafka-ui.labels" . | nindent 4 }} -data: - {{- toYaml .Values.envs.config | nindent 2 }} \ No newline at end of file diff --git a/charts/kafka-ui/templates/deployment.yaml b/charts/kafka-ui/templates/deployment.yaml deleted file mode 100644 index 630d93c8329..00000000000 --- a/charts/kafka-ui/templates/deployment.yaml +++ /dev/null @@ -1,103 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "kafka-ui.fullname" . }} - labels: - {{- include "kafka-ui.labels" . | nindent 4 }} -spec: -{{- if not .Values.autoscaling.enabled }} - replicas: {{ .Values.replicaCount }} -{{- end }} - selector: - matchLabels: - {{- include "kafka-ui.selectorLabels" . | nindent 6 }} - template: - metadata: - annotations: - {{- with .Values.podAnnotations }} - {{- toYaml . | nindent 8 }} - {{- end }} - checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} - checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} - labels: - {{- include "kafka-ui.selectorLabels" . | nindent 8 }} - {{- if .Values.podLabels }} - {{- toYaml .Values.podLabels | nindent 8 }} - {{- end }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.initContainers }} - initContainers: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "kafka-ui.serviceAccountName" . }} - securityContext: - {{- toYaml .Values.podSecurityContext | nindent 8 }} - containers: - - name: {{ .Chart.Name }} - securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} - image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - {{- with .Values.env }} - env: - {{- toYaml . | nindent 12 }} - {{- end }} - envFrom: - {{- if .Values.existingConfigMap }} - - configMapRef: - name: {{ .Values.existingConfigMap }} - {{- end }} - - configMapRef: - name: {{ include "kafka-ui.fullname" . }} - {{- if .Values.existingSecret }} - - secretRef: - name: {{ .Values.existingSecret }} - {{- end }} - - secretRef: - name: {{ include "kafka-ui.fullname" . }} - ports: - - name: http - containerPort: 8080 - protocol: TCP - livenessProbe: - httpGet: - {{- $contextPath := .Values.envs.config.SERVER_SERVLET_CONTEXT_PATH | default "" | printf "%s/actuator/health" | urlParse }} - path: {{ get $contextPath "path" }} - port: http - initialDelaySeconds: 60 - periodSeconds: 30 - timeoutSeconds: 10 - readinessProbe: - httpGet: - {{- $contextPath := .Values.envs.config.SERVER_SERVLET_CONTEXT_PATH | default "" | printf "%s/actuator/health" | urlParse }} - path: {{ get $contextPath "path" }} - port: http - initialDelaySeconds: 60 - periodSeconds: 30 - timeoutSeconds: 10 - resources: - {{- toYaml .Values.resources | nindent 12 }} - {{- with .Values.volumeMounts }} - volumeMounts: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.volumes }} - volumes: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/charts/kafka-ui/templates/hpa.yaml b/charts/kafka-ui/templates/hpa.yaml deleted file mode 100644 index 1509ef3f010..00000000000 --- a/charts/kafka-ui/templates/hpa.yaml +++ /dev/null @@ -1,28 +0,0 @@ -{{- if .Values.autoscaling.enabled }} -apiVersion: autoscaling/v2beta1 -kind: HorizontalPodAutoscaler -metadata: - name: {{ include "kafka-ui.fullname" . }} - labels: - {{- include "kafka-ui.labels" . | nindent 4 }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include "kafka-ui.fullname" . }} - minReplicas: {{ .Values.autoscaling.minReplicas }} - maxReplicas: {{ .Values.autoscaling.maxReplicas }} - metrics: - {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} - - type: Resource - resource: - name: cpu - targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} - {{- end }} - {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} - - type: Resource - resource: - name: memory - targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} - {{- end }} -{{- end }} diff --git a/charts/kafka-ui/templates/ingress.yaml b/charts/kafka-ui/templates/ingress.yaml deleted file mode 100644 index 8631ea0130c..00000000000 --- a/charts/kafka-ui/templates/ingress.yaml +++ /dev/null @@ -1,87 +0,0 @@ -{{- if .Values.ingress.enabled -}} -{{- $fullName := include "kafka-ui.fullname" . -}} -{{- $svcPort := .Values.service.port -}} -{{- if $.Capabilities.APIVersions.Has "networking.k8s.io/v1" }} -apiVersion: networking.k8s.io/v1 -{{- else if $.Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" }} -apiVersion: networking.k8s.io/v1beta1 -{{- else }} -apiVersion: extensions/v1beta1 -{{- end }} -kind: Ingress -metadata: - name: {{ $fullName }} - labels: - {{- include "kafka-ui.labels" . | nindent 4 }} - {{- with .Values.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - {{- if .Values.ingress.tls.enabled }} - tls: - - hosts: - - {{ tpl .Values.ingress.host . }} - secretName: {{ .Values.ingress.tls.secretName }} - {{- end }} - {{- if .Values.ingress.ingressClassName }} - ingressClassName: {{ .Values.ingress.ingressClassName }} - {{- end }} - rules: - - http: - paths: -{{- if $.Capabilities.APIVersions.Has "networking.k8s.io/v1" -}} - {{- range .Values.ingress.precedingPaths }} - - path: {{ .path }} - pathType: Prefix - backend: - service: - name: {{ .serviceName }} - port: - number: {{ .servicePort }} - {{- end }} - - backend: - service: - name: {{ $fullName }} - port: - number: {{ $svcPort }} - pathType: Prefix -{{- if .Values.ingress.path }} - path: {{ .Values.ingress.path }} -{{- end }} - {{- range .Values.ingress.succeedingPaths }} - - path: {{ .path }} - pathType: Prefix - backend: - service: - name: {{ .serviceName }} - port: - number: {{ .servicePort }} - {{- end }} -{{- if tpl .Values.ingress.host . }} - host: {{tpl .Values.ingress.host . }} -{{- end }} -{{- else -}} - {{- range .Values.ingress.precedingPaths }} - - path: {{ .path }} - backend: - serviceName: {{ .serviceName }} - servicePort: {{ .servicePort }} - {{- end }} - - backend: - serviceName: {{ $fullName }} - servicePort: {{ $svcPort }} -{{- if .Values.ingress.path }} - path: {{ .Values.ingress.path }} -{{- end }} - {{- range .Values.ingress.succeedingPaths }} - - path: {{ .path }} - backend: - serviceName: {{ .serviceName }} - servicePort: {{ .servicePort }} - {{- end }} -{{- if tpl .Values.ingress.host . }} - host: {{ tpl .Values.ingress.host . }} -{{- end }} -{{- end }} -{{- end }} diff --git a/charts/kafka-ui/templates/networkpolicy-egress.yaml b/charts/kafka-ui/templates/networkpolicy-egress.yaml deleted file mode 100644 index 4f582802712..00000000000 --- a/charts/kafka-ui/templates/networkpolicy-egress.yaml +++ /dev/null @@ -1,18 +0,0 @@ -{{- if and .Values.networkPolicy.enabled .Values.networkPolicy.egressRules.customRules }} -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: {{ printf "%s-egress" (include "kafka-ui.fullname" .) }} - labels: - {{- include "kafka-ui.labels" . | nindent 4 }} -spec: - podSelector: - matchLabels: - {{- include "kafka-ui.selectorLabels" . | nindent 6 }} - policyTypes: - - Egress - egress: - {{- if .Values.networkPolicy.egressRules.customRules }} - {{- toYaml .Values.networkPolicy.egressRules.customRules | nindent 4 }} - {{- end }} -{{- end }} diff --git a/charts/kafka-ui/templates/networkpolicy-ingress.yaml b/charts/kafka-ui/templates/networkpolicy-ingress.yaml deleted file mode 100644 index 74988676b52..00000000000 --- a/charts/kafka-ui/templates/networkpolicy-ingress.yaml +++ /dev/null @@ -1,18 +0,0 @@ -{{- if and .Values.networkPolicy.enabled .Values.networkPolicy.ingressRules.customRules }} -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: {{ printf "%s-ingress" (include "kafka-ui.fullname" .) }} - labels: - {{- include "kafka-ui.labels" . | nindent 4 }} -spec: - podSelector: - matchLabels: - {{- include "kafka-ui.selectorLabels" . | nindent 6 }} - policyTypes: - - Ingress - ingress: - {{- if .Values.networkPolicy.ingressRules.customRules }} - {{- toYaml .Values.networkPolicy.ingressRules.customRules | nindent 4 }} - {{- end }} -{{- end }} diff --git a/charts/kafka-ui/templates/secret.yaml b/charts/kafka-ui/templates/secret.yaml deleted file mode 100644 index a2ebf0fdba8..00000000000 --- a/charts/kafka-ui/templates/secret.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "kafka-ui.fullname" . }} - labels: - {{- include "kafka-ui.labels" . | nindent 4 }} -type: Opaque -data: - {{- toYaml .Values.envs.secret | nindent 2 }} \ No newline at end of file diff --git a/charts/kafka-ui/templates/service.yaml b/charts/kafka-ui/templates/service.yaml deleted file mode 100644 index 5801135c4c7..00000000000 --- a/charts/kafka-ui/templates/service.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "kafka-ui.fullname" . }} - labels: - {{- include "kafka-ui.labels" . | nindent 4 }} -{{- if .Values.service.annotations }} - annotations: -{{ toYaml .Values.service.annotations | nindent 4 }} -{{- end }} -spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.port }} - targetPort: http - protocol: TCP - name: http - {{- if (and (eq .Values.service.type "NodePort") .Values.service.nodePort) }} - nodePort: {{ .Values.service.nodePort }} - {{- end }} - selector: - {{- include "kafka-ui.selectorLabels" . | nindent 4 }} diff --git a/charts/kafka-ui/templates/serviceaccount.yaml b/charts/kafka-ui/templates/serviceaccount.yaml deleted file mode 100644 index b89551c833e..00000000000 --- a/charts/kafka-ui/templates/serviceaccount.yaml +++ /dev/null @@ -1,12 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "kafka-ui.serviceAccountName" . }} - labels: - {{- include "kafka-ui.labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -{{- end }} diff --git a/charts/kafka-ui/values.yaml b/charts/kafka-ui/values.yaml deleted file mode 100644 index 33dcfb0b300..00000000000 --- a/charts/kafka-ui/values.yaml +++ /dev/null @@ -1,127 +0,0 @@ -replicaCount: 1 - -image: - registry: docker.io - repository: provectuslabs/kafka-ui - pullPolicy: IfNotPresent - # Overrides the image tag whose default is the chart appVersion. - tag: "" - -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" - -serviceAccount: - # Specifies whether a service account should be created - create: true - # Annotations to add to the service account - annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template - name: "" - -existingConfigMap: "" -existingSecret: "" -envs: - secret: {} - config: {} - -networkPolicy: - enabled: false - egressRules: - ## Additional custom egress rules - ## e.g: - ## customRules: - ## - to: - ## - namespaceSelector: - ## matchLabels: - ## label: example - customRules: [] - ingressRules: - ## Additional custom ingress rules - ## e.g: - ## customRules: - ## - from: - ## - namespaceSelector: - ## matchLabels: - ## label: example - customRules: [] - -podAnnotations: {} -podLabels: {} - -podSecurityContext: {} - # fsGroup: 2000 - -securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - -service: - type: ClusterIP - port: 80 - # if you want to force a specific nodePort. Must be use with service.type=NodePort - # nodePort: - -# Ingress configuration -ingress: - # Enable ingress resource - enabled: false - - # Annotations for the Ingress - annotations: {} - - # ingressClassName for the Ingress - ingressClassName: "" - - # The path for the Ingress - path: "" - - # The hostname for the Ingress - host: "" - - # configs for Ingress TLS - tls: - # Enable TLS termination for the Ingress - enabled: false - # the name of a pre-created Secret containing a TLS private key and certificate - secretName: "" - - # HTTP paths to add to the Ingress before the default path - precedingPaths: [] - - # Http paths to add to the Ingress after the default path - succeedingPaths: [] - -resources: {} - # limits: - # cpu: 200m - # memory: 512Mi - # requests: - # cpu: 200m - # memory: 256Mi - -autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 100 - targetCPUUtilizationPercentage: 80 - # targetMemoryUtilizationPercentage: 80 - -nodeSelector: {} - -tolerations: [] - -affinity: {} - -env: {} - -initContainers: {} - -volumeMounts: {} - -volumes: {} diff --git a/docker-compose.md b/docker-compose.md deleted file mode 100644 index d3912c67151..00000000000 --- a/docker-compose.md +++ /dev/null @@ -1,45 +0,0 @@ -# Quick Start with docker-compose - -Environment variables documentation - [see usage](README.md#env_variables).
-We have plenty of example files with more complex configurations. Please check them out in ``docker`` directory. - -* Add a new service in docker-compose.yml - -```yaml -version: '2' -services: - kafka-ui: - image: provectuslabs/kafka-ui - container_name: kafka-ui - ports: - - "8080:8080" - restart: always - environment: - - KAFKA_CLUSTERS_0_NAME=local - - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=kafka:9092 - - KAFKA_CLUSTERS_0_ZOOKEEPER=localhost:2181 -``` - -* If you prefer UI for Apache Kafka in read only mode - -```yaml -version: '2' -services: - kafka-ui: - image: provectuslabs/kafka-ui - container_name: kafka-ui - ports: - - "8080:8080" - restart: always - environment: - - KAFKA_CLUSTERS_0_NAME=local - - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=kafka:9092 - - KAFKA_CLUSTERS_0_ZOOKEEPER=localhost:2181 - - KAFKA_CLUSTERS_0_READONLY=true -``` - -* Start UI for Apache Kafka process - -```bash -docker-compose up -d kafka-ui -``` diff --git a/documentation/compose/DOCKER_COMPOSE.md b/documentation/compose/DOCKER_COMPOSE.md index 2ea3f09c990..1ca7de1dc0b 100644 --- a/documentation/compose/DOCKER_COMPOSE.md +++ b/documentation/compose/DOCKER_COMPOSE.md @@ -1,12 +1,16 @@ # Descriptions of docker-compose configurations (*.yaml) 1. [kafka-ui.yaml](./kafka-ui.yaml) - Default configuration with 2 kafka clusters with two nodes of Schema Registry, one kafka-connect and a few dummy topics. -2. [kafka-clusters-only.yaml](./kafka-clusters-only.yaml) - A configuration for development purposes, everything besides `kafka-ui` itself (to be run locally). -3. [kafka-ui-ssl.yml](./kafka-ssl.yml) - Connect to Kafka via TLS/SSL -4. [kafka-cluster-sr-auth.yaml](./kafka-cluster-sr-auth.yaml) - Schema registry with authentication. -5. [kafka-ui-auth-context.yaml](./kafka-ui-auth-context.yaml) - Basic (username/password) authentication with custom path (URL) (issue 861). -6. [kafka-ui-connectors.yaml](./kafka-ui-connectors.yaml) - Configuration with different connectors (github-source, s3, sink-activities, source-activities) and Ksql functionality. -7. [kafka-ui-jmx-secured.yml](./kafka-ui-jmx-secured.yml) - Kafka’s JMX with SSL and authentication. -8. [kafka-ui-reverse-proxy.yaml](./kafka-ui-reverse-proxy.yaml) - An example for using the app behind a proxy (like nginx). -9. [kafka-ui-sasl.yaml](./kafka-ui-sasl.yaml) - SASL auth for Kafka. -10. [kafka-ui-traefik-proxy.yaml](./kafka-ui-traefik-proxy.yaml) - Traefik specific proxy configuration. +2. [kafka-ui-arm64.yaml](./kafka-ui-arm64.yaml) - Default configuration for ARM64(Mac M1) architecture with 1 kafka cluster without zookeeper with one node of Schema Registry, one kafka-connect and a few dummy topics. +3. [kafka-clusters-only.yaml](./kafka-clusters-only.yaml) - A configuration for development purposes, everything besides `kafka-ui` itself (to be run locally). +4. [kafka-ui-ssl.yml](./kafka-ssl.yml) - Connect to Kafka via TLS/SSL +5. [kafka-cluster-sr-auth.yaml](./kafka-cluster-sr-auth.yaml) - Schema registry with authentication. +6. [kafka-ui-auth-context.yaml](./kafka-ui-auth-context.yaml) - Basic (username/password) authentication with custom path (URL) (issue 861). +7. [e2e-tests.yaml](./e2e-tests.yaml) - Configuration with different connectors (github-source, s3, sink-activities, source-activities) and Ksql functionality. +8. [kafka-ui-jmx-secured.yml](./kafka-ui-jmx-secured.yml) - Kafka’s JMX with SSL and authentication. +9. [kafka-ui-reverse-proxy.yaml](./nginx-proxy.yaml) - An example for using the app behind a proxy (like nginx). +10. [kafka-ui-sasl.yaml](./kafka-ui-sasl.yaml) - SASL auth for Kafka. +11. [kafka-ui-traefik-proxy.yaml](./traefik-proxy.yaml) - Traefik specific proxy configuration. +12. [oauth-cognito.yaml](./oauth-cognito.yaml) - OAuth2 with Cognito +13. [kafka-ui-with-jmx-exporter.yaml](./kafka-ui-with-jmx-exporter.yaml) - A configuration with 2 kafka clusters with enabled prometheus jmx exporters instead of jmx. +14. [kafka-with-zookeeper.yaml](./kafka-with-zookeeper.yaml) - An example for using kafka with zookeeper \ No newline at end of file diff --git a/documentation/compose/auth-ldap.yaml b/documentation/compose/auth-ldap.yaml deleted file mode 100644 index 7c25adce5dd..00000000000 --- a/documentation/compose/auth-ldap.yaml +++ /dev/null @@ -1,86 +0,0 @@ ---- -version: '2' -services: - - kafka-ui: - container_name: kafka-ui - image: provectuslabs/kafka-ui:latest - ports: - - 8080:8080 - depends_on: - - zookeeper0 - - kafka0 - - schemaregistry0 - environment: - KAFKA_CLUSTERS_0_NAME: local - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 - KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper0:2181 - KAFKA_CLUSTERS_0_JMXPORT: 9997 - KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schemaregistry0:8085 - KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME: first - KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS: http://kafka-connect0:8083 - KAFKA_CLUSTERS_1_NAME: secondLocal - KAFKA_CLUSTERS_1_BOOTSTRAPSERVERS: kafka1:29092 - KAFKA_CLUSTERS_1_ZOOKEEPER: zookeeper1:2181 - KAFKA_CLUSTERS_1_JMXPORT: 9998 - KAFKA_CLUSTERS_1_SCHEMAREGISTRY: http://schemaregistry1:8085 - KAFKA_CLUSTERS_1_KAFKACONNECT_0_NAME: first - KAFKA_CLUSTERS_1_KAFKACONNECT_0_ADDRESS: http://kafka-connect0:8083 - AUTH_TYPE: "LDAP" - SPRING_LDAP_URLS: "ldap://ldap:10389" - SPRING_LDAP_DN_PATTERN: "cn={0},ou=people,dc=planetexpress,dc=com" -# USER SEARCH FILTER INSTEAD OF DN -# SPRING_LDAP_USERFILTER_SEARCHBASE: "dc=planetexpress,dc=com" -# SPRING_LDAP_USERFILTER_SEARCHFILTER: "(&(uid={0})(objectClass=inetOrgPerson))" -# LDAP ADMIN USER -# SPRING_LDAP_ADMINUSER: "cn=admin,dc=planetexpress,dc=com" -# SPRING_LDAP_ADMINPASSWORD: "GoodNewsEveryone" - - - - ldap: - image: rroemhild/test-openldap:latest - hostname: "ldap" - - zookeeper0: - image: confluentinc/cp-zookeeper:5.2.4 - environment: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - ports: - - 2181:2181 - - kafka0: - image: confluentinc/cp-kafka:5.3.1 - depends_on: - - zookeeper0 - ports: - - 9092:9092 - - 9997:9997 - environment: - KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper0:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - JMX_PORT: 9997 - KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9997 - - schemaregistry0: - image: confluentinc/cp-schema-registry:5.5.0 - ports: - - 8085:8085 - depends_on: - - zookeeper0 - - kafka0 - environment: - SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092 - SCHEMA_REGISTRY_KAFKASTORE_CONNECTION_URL: zookeeper0:2181 - SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT - SCHEMA_REGISTRY_HOST_NAME: schemaregistry0 - SCHEMA_REGISTRY_LISTENERS: http://schemaregistry0:8085 - - SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: "http" - SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO - SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas \ No newline at end of file diff --git a/documentation/compose/message.json b/documentation/compose/data/message.json similarity index 100% rename from documentation/compose/message.json rename to documentation/compose/data/message.json diff --git a/documentation/compose/proxy.conf b/documentation/compose/data/proxy.conf similarity index 100% rename from documentation/compose/proxy.conf rename to documentation/compose/data/proxy.conf diff --git a/documentation/compose/e2e-tests.yaml b/documentation/compose/e2e-tests.yaml new file mode 100644 index 00000000000..3685d48c238 --- /dev/null +++ b/documentation/compose/e2e-tests.yaml @@ -0,0 +1,190 @@ +--- +version: '3.5' +services: + + kafka-ui: + container_name: kafka-ui + image: provectuslabs/kafka-ui:latest + ports: + - 8080:8080 + healthcheck: + test: wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health + interval: 30s + timeout: 10s + retries: 10 + depends_on: + kafka0: + condition: service_healthy + schemaregistry0: + condition: service_healthy + kafka-connect0: + condition: service_healthy + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 + KAFKA_CLUSTERS_0_METRICS_PORT: 9997 + KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schemaregistry0:8085 + KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME: first + KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS: http://kafka-connect0:8083 + KAFKA_CLUSTERS_0_KSQLDBSERVER: http://ksqldb:8088 + + kafka0: + image: confluentinc/cp-kafka:7.2.1 + hostname: kafka0 + container_name: kafka0 + healthcheck: + test: unset JMX_PORT && KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9999" && kafka-broker-api-versions --bootstrap-server=localhost:9092 + interval: 30s + timeout: 10s + retries: 10 + ports: + - "9092:9092" + - "9997:9997" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT' + KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092' + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_JMX_PORT: 9997 + KAFKA_JMX_HOSTNAME: localhost + KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9997 + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093' + KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' + volumes: + - ./scripts/update_run.sh:/tmp/update_run.sh + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" + + schemaregistry0: + image: confluentinc/cp-schema-registry:7.2.1 + ports: + - 8085:8085 + depends_on: + kafka0: + condition: service_healthy + healthcheck: + test: [ "CMD", "timeout", "1", "curl", "--silent", "--fail", "http://schemaregistry0:8085/subjects" ] + interval: 30s + timeout: 10s + retries: 10 + environment: + SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092 + SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT + SCHEMA_REGISTRY_HOST_NAME: schemaregistry0 + SCHEMA_REGISTRY_LISTENERS: http://schemaregistry0:8085 + + SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: "http" + SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO + SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas + + kafka-connect0: + build: + context: ./kafka-connect + args: + image: confluentinc/cp-kafka-connect:6.0.1 + ports: + - 8083:8083 + depends_on: + kafka0: + condition: service_healthy + schemaregistry0: + condition: service_healthy + healthcheck: + test: [ "CMD", "nc", "127.0.0.1", "8083" ] + interval: 30s + timeout: 10s + retries: 10 + environment: + CONNECT_BOOTSTRAP_SERVERS: kafka0:29092 + CONNECT_GROUP_ID: compose-connect-group + CONNECT_CONFIG_STORAGE_TOPIC: _connect_configs + CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR: 1 + CONNECT_OFFSET_STORAGE_TOPIC: _connect_offset + CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR: 1 + CONNECT_STATUS_STORAGE_TOPIC: _connect_status + CONNECT_STATUS_STORAGE_REPLICATION_FACTOR: 1 + CONNECT_KEY_CONVERTER: org.apache.kafka.connect.storage.StringConverter + CONNECT_KEY_CONVERTER_SCHEMA_REGISTRY_URL: http://schemaregistry0:8085 + CONNECT_VALUE_CONVERTER: org.apache.kafka.connect.storage.StringConverter + CONNECT_VALUE_CONVERTER_SCHEMA_REGISTRY_URL: http://schemaregistry0:8085 + CONNECT_INTERNAL_KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter + CONNECT_INTERNAL_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter + CONNECT_REST_ADVERTISED_HOST_NAME: kafka-connect0 + CONNECT_PLUGIN_PATH: "/usr/share/java,/usr/share/confluent-hub-components" + # AWS_ACCESS_KEY_ID: "" + # AWS_SECRET_ACCESS_KEY: "" + + kafka-init-topics: + image: confluentinc/cp-kafka:7.2.1 + volumes: + - ./data/message.json:/data/message.json + depends_on: + kafka0: + condition: service_healthy + command: "bash -c 'echo Waiting for Kafka to be ready... && \ + cub kafka-ready -b kafka0:29092 1 30 && \ + kafka-topics --create --topic users --partitions 3 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \ + kafka-topics --create --topic messages --partitions 2 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \ + kafka-console-producer --bootstrap-server kafka0:29092 --topic users < /data/message.json'" + + postgres-db: + build: + context: ./postgres + args: + image: postgres:9.6.22 + ports: + - 5432:5432 + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U dev_user" ] + interval: 10s + timeout: 5s + retries: 5 + environment: + POSTGRES_USER: 'dev_user' + POSTGRES_PASSWORD: '12345' + + create-connectors: + image: ellerbrock/alpine-bash-curl-ssl + depends_on: + postgres-db: + condition: service_healthy + kafka-connect0: + condition: service_healthy + volumes: + - ./connectors:/connectors + command: bash -c '/connectors/start.sh' + + ksqldb: + image: confluentinc/ksqldb-server:0.18.0 + healthcheck: + test: [ "CMD", "timeout", "1", "curl", "--silent", "--fail", "http://localhost:8088/info" ] + interval: 30s + timeout: 10s + retries: 10 + depends_on: + kafka0: + condition: service_healthy + kafka-connect0: + condition: service_healthy + schemaregistry0: + condition: service_healthy + ports: + - 8088:8088 + environment: + KSQL_CUB_KAFKA_TIMEOUT: 120 + KSQL_LISTENERS: http://0.0.0.0:8088 + KSQL_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092 + KSQL_KSQL_LOGGING_PROCESSING_STREAM_AUTO_CREATE: "true" + KSQL_KSQL_LOGGING_PROCESSING_TOPIC_AUTO_CREATE: "true" + KSQL_KSQL_CONNECT_URL: http://kafka-connect0:8083 + KSQL_KSQL_SCHEMA_REGISTRY_URL: http://schemaregistry0:8085 + KSQL_KSQL_SERVICE_ID: my_ksql_1 + KSQL_KSQL_HIDDEN_TOPICS: '^_.*' + KSQL_CACHE_MAX_BYTES_BUFFERING: 0 diff --git a/documentation/compose/jaas/client.properties b/documentation/compose/jaas/client.properties old mode 100644 new mode 100755 diff --git a/documentation/compose/jaas/kafka_connect.jaas b/documentation/compose/jaas/kafka_connect.jaas old mode 100644 new mode 100755 diff --git a/documentation/compose/jaas/kafka_connect.password b/documentation/compose/jaas/kafka_connect.password old mode 100644 new mode 100755 diff --git a/documentation/compose/jaas/kafka_server.conf b/documentation/compose/jaas/kafka_server.conf index ef41c992e21..0c1fb34652a 100644 --- a/documentation/compose/jaas/kafka_server.conf +++ b/documentation/compose/jaas/kafka_server.conf @@ -11,4 +11,8 @@ KafkaClient { user_admin="admin-secret"; }; -Client {}; \ No newline at end of file +Client { + org.apache.zookeeper.server.auth.DigestLoginModule required + username="zkuser" + password="zkuserpassword"; +}; diff --git a/documentation/compose/jaas/schema_registry.jaas b/documentation/compose/jaas/schema_registry.jaas old mode 100644 new mode 100755 diff --git a/documentation/compose/jaas/schema_registry.password b/documentation/compose/jaas/schema_registry.password old mode 100644 new mode 100755 diff --git a/documentation/compose/jaas/zookeeper_jaas.conf b/documentation/compose/jaas/zookeeper_jaas.conf new file mode 100644 index 00000000000..2d7fd1b1c29 --- /dev/null +++ b/documentation/compose/jaas/zookeeper_jaas.conf @@ -0,0 +1,4 @@ +Server { + org.apache.zookeeper.server.auth.DigestLoginModule required + user_zkuser="zkuserpassword"; +}; diff --git a/documentation/compose/jmx-exporter/kafka-broker.yml b/documentation/compose/jmx-exporter/kafka-broker.yml new file mode 100644 index 00000000000..efe0a463567 --- /dev/null +++ b/documentation/compose/jmx-exporter/kafka-broker.yml @@ -0,0 +1,2 @@ +rules: + - pattern: ".*" diff --git a/documentation/compose/jmx-exporter/kafka-prepare-and-run b/documentation/compose/jmx-exporter/kafka-prepare-and-run new file mode 100755 index 00000000000..2ccf17df505 --- /dev/null +++ b/documentation/compose/jmx-exporter/kafka-prepare-and-run @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +JAVA_AGENT_FILE="/usr/share/jmx_exporter/jmx_prometheus_javaagent.jar" +if [ ! -f "$JAVA_AGENT_FILE" ] +then + echo "Downloading jmx_exporter javaagent" + curl -o $JAVA_AGENT_FILE https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/0.16.1/jmx_prometheus_javaagent-0.16.1.jar +fi + +exec /etc/confluent/docker/run \ No newline at end of file diff --git a/documentation/compose/kafka-cluster-sr-auth.yaml b/documentation/compose/kafka-cluster-sr-auth.yaml index 6dbcb12e361..09403cef27a 100644 --- a/documentation/compose/kafka-cluster-sr-auth.yaml +++ b/documentation/compose/kafka-cluster-sr-auth.yaml @@ -2,43 +2,44 @@ version: '2' services: - zookeeper1: - image: confluentinc/cp-zookeeper:5.2.4 - environment: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - ports: - - 2182:2181 - kafka1: - image: confluentinc/cp-kafka:5.3.1 - depends_on: - - zookeeper1 + image: confluentinc/cp-kafka:7.2.1 + hostname: kafka1 + container_name: kafka1 + ports: + - "9092:9092" + - "9997:9997" environment: KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper1:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka1:29092,PLAINTEXT_HOST://localhost:9093 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT' + KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka1:29092,PLAINTEXT_HOST://localhost:9092' KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - JMX_PORT: 9998 - KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=localhost -Dcom.sun.management.jmxremote.rmi.port=9998 - ports: - - 9093:9093 - - 9998:9998 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_JMX_PORT: 9997 + KAFKA_JMX_HOSTNAME: localhost + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka1:29093' + KAFKA_LISTENERS: 'PLAINTEXT://kafka1:29092,CONTROLLER://kafka1:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' + volumes: + - ./scripts/update_run.sh:/tmp/update_run.sh + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" schemaregistry1: - image: confluentinc/cp-schema-registry:5.5.0 + image: confluentinc/cp-schema-registry:7.2.1 ports: - 18085:8085 depends_on: - - zookeeper1 - kafka1 volumes: - ./jaas:/conf environment: SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka1:29092 - SCHEMA_REGISTRY_KAFKASTORE_CONNECTION_URL: zookeeper1:2181 SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT SCHEMA_REGISTRY_HOST_NAME: schemaregistry1 SCHEMA_REGISTRY_LISTENERS: http://schemaregistry1:8085 @@ -54,13 +55,29 @@ services: SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas kafka-init-topics: - image: confluentinc/cp-kafka:5.3.1 + image: confluentinc/cp-kafka:7.2.1 volumes: - - ./message.json:/data/message.json + - ./data/message.json:/data/message.json depends_on: - kafka1 command: "bash -c 'echo Waiting for Kafka to be ready... && \ cub kafka-ready -b kafka1:29092 1 30 && \ - kafka-topics --create --topic second.users --partitions 3 --replication-factor 1 --if-not-exists --zookeeper zookeeper1:2181 && \ - kafka-topics --create --topic second.messages --partitions 2 --replication-factor 1 --if-not-exists --zookeeper zookeeper1:2181 && \ - kafka-console-producer --broker-list kafka1:29092 -topic second.users < /data/message.json'" + kafka-topics --create --topic users --partitions 3 --replication-factor 1 --if-not-exists --bootstrap-server kafka1:29092 && \ + kafka-topics --create --topic messages --partitions 2 --replication-factor 1 --if-not-exists --bootstrap-server kafka1:29092 && \ + kafka-console-producer --bootstrap-server kafka1:29092 --topic users < /data/message.json'" + + kafka-ui: + container_name: kafka-ui + image: provectuslabs/kafka-ui:latest + ports: + - 8080:8080 + depends_on: + - kafka1 + - schemaregistry1 + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka1:29092 + KAFKA_CLUSTERS_0_METRICS_PORT: 9997 + KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schemaregistry1:8085 + KAFKA_CLUSTERS_0_SCHEMAREGISTRYAUTH_USERNAME: admin + KAFKA_CLUSTERS_0_SCHEMAREGISTRYAUTH_PASSWORD: letmein diff --git a/documentation/compose/kafka-clusters-only.yaml b/documentation/compose/kafka-clusters-only.yaml deleted file mode 100644 index 1e51dd5a4c9..00000000000 --- a/documentation/compose/kafka-clusters-only.yaml +++ /dev/null @@ -1,146 +0,0 @@ ---- -version: '2' -services: - - zookeeper0: - image: confluentinc/cp-zookeeper:5.2.4 - environment: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - ports: - - 2181:2181 - - kafka0: - image: confluentinc/cp-kafka:5.3.1 - depends_on: - - zookeeper0 - environment: - KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper0:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 2 - JMX_PORT: 9997 - KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=localhost -Dcom.sun.management.jmxremote.rmi.port=9997 - ports: - - 9092:9092 - - 9997:9997 - - kafka01: - image: confluentinc/cp-kafka:5.3.1 - depends_on: - - zookeeper0 - environment: - KAFKA_BROKER_ID: 2 - KAFKA_ZOOKEEPER_CONNECT: zookeeper0:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka01:29092,PLAINTEXT_HOST://localhost:9094 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,PLAIN:PLAINTEXT - KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 2 - JMX_PORT: 9999 - KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=localhost -Dcom.sun.management.jmxremote.rmi.port=9999 - ports: - - 9094:9094 - - 9999:9999 - - zookeeper1: - image: confluentinc/cp-zookeeper:5.2.4 - environment: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - ports: - - 2182:2181 - - kafka1: - image: confluentinc/cp-kafka:5.3.1 - depends_on: - - zookeeper1 - environment: - KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper1:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka1:29092,PLAINTEXT_HOST://localhost:9093 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - JMX_PORT: 9998 - KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=localhost -Dcom.sun.management.jmxremote.rmi.port=9998 - ports: - - 9093:9093 - - 9998:9998 - - schemaregistry0: - image: confluentinc/cp-schema-registry:5.5.0 - depends_on: - - zookeeper0 - - kafka0 - - kafka01 - environment: - SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092,PLAINTEXT://kafka01:29092 - SCHEMA_REGISTRY_KAFKASTORE_CONNECTION_URL: zookeeper0:2181 - SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT - SCHEMA_REGISTRY_HOST_NAME: schemaregistry0 - SCHEMA_REGISTRY_LISTENERS: http://schemaregistry0:8085 - - SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: "http" - SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO - SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas - ports: - - 8085:8085 - - schemaregistry1: - image: confluentinc/cp-schema-registry:5.5.0 - ports: - - 18085:8085 - depends_on: - - zookeeper1 - - kafka1 - environment: - SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka1:29092 - SCHEMA_REGISTRY_KAFKASTORE_CONNECTION_URL: zookeeper1:2181 - SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT - SCHEMA_REGISTRY_HOST_NAME: schemaregistry1 - SCHEMA_REGISTRY_LISTENERS: http://schemaregistry1:8085 - - SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: "http" - SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO - SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas - - kafka-connect0: - image: confluentinc/cp-kafka-connect:6.0.1 - ports: - - 8083:8083 - depends_on: - - kafka0 - - schemaregistry0 - environment: - CONNECT_BOOTSTRAP_SERVERS: kafka0:29092 - CONNECT_GROUP_ID: compose-connect-group - CONNECT_CONFIG_STORAGE_TOPIC: _connect_configs - CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR: 1 - CONNECT_OFFSET_STORAGE_TOPIC: _connect_offset - CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR: 1 - CONNECT_STATUS_STORAGE_TOPIC: _connect_status - CONNECT_STATUS_STORAGE_REPLICATION_FACTOR: 1 - CONNECT_KEY_CONVERTER: org.apache.kafka.connect.storage.StringConverter - CONNECT_KEY_CONVERTER_SCHEMA_REGISTRY_URL: http://schemaregistry0:8085 - CONNECT_VALUE_CONVERTER: org.apache.kafka.connect.storage.StringConverter - CONNECT_VALUE_CONVERTER_SCHEMA_REGISTRY_URL: http://schemaregistry0:8085 - CONNECT_INTERNAL_KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter - CONNECT_INTERNAL_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter - CONNECT_REST_ADVERTISED_HOST_NAME: kafka-connect0 - CONNECT_PLUGIN_PATH: "/usr/share/java,/usr/share/confluent-hub-components" - - - kafka-init-topics: - image: confluentinc/cp-kafka:5.3.1 - volumes: - - ./message.json:/data/message.json - depends_on: - - kafka1 - command: "bash -c 'echo Waiting for Kafka to be ready... && \ - cub kafka-ready -b kafka1:29092 1 30 && \ - kafka-topics --create --topic second.users --partitions 3 --replication-factor 1 --if-not-exists --zookeeper zookeeper1:2181 && \ - kafka-topics --create --topic second.messages --partitions 2 --replication-factor 1 --if-not-exists --zookeeper zookeeper1:2181 && \ - kafka-topics --create --topic first.messages --partitions 2 --replication-factor 1 --if-not-exists --zookeeper zookeeper0:2181 && \ - kafka-console-producer --broker-list kafka1:29092 -topic second.users < /data/message.json'" diff --git a/documentation/compose/kafka-ssl-components.yaml b/documentation/compose/kafka-ssl-components.yaml new file mode 100644 index 00000000000..407ce5b97a7 --- /dev/null +++ b/documentation/compose/kafka-ssl-components.yaml @@ -0,0 +1,178 @@ +--- +version: '3.4' +services: + kafka-ui: + container_name: kafka-ui + image: provectuslabs/kafka-ui:latest + ports: + - 8080:8080 + depends_on: + - kafka0 + - schemaregistry0 + - kafka-connect0 + - ksqldb0 + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL: SSL + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 # SSL LISTENER! + KAFKA_CLUSTERS_0_PROPERTIES_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: '' # DISABLE COMMON NAME VERIFICATION + + KAFKA_CLUSTERS_0_SCHEMAREGISTRY: https://schemaregistry0:8085 + KAFKA_CLUSTERS_0_SCHEMAREGISTRYSSL_KEYSTORELOCATION: /kafka.keystore.jks + KAFKA_CLUSTERS_0_SCHEMAREGISTRYSSL_KEYSTOREPASSWORD: "secret" + + KAFKA_CLUSTERS_0_KSQLDBSERVER: https://ksqldb0:8088 + KAFKA_CLUSTERS_0_KSQLDBSERVERSSL_KEYSTORELOCATION: /kafka.keystore.jks + KAFKA_CLUSTERS_0_KSQLDBSERVERSSL_KEYSTOREPASSWORD: "secret" + + KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME: local + KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS: https://kafka-connect0:8083 + KAFKA_CLUSTERS_0_KAFKACONNECT_0_KEYSTORELOCATION: /kafka.keystore.jks + KAFKA_CLUSTERS_0_KAFKACONNECT_0_KEYSTOREPASSWORD: "secret" + + KAFKA_CLUSTERS_0_SSL_TRUSTSTORELOCATION: /kafka.truststore.jks + KAFKA_CLUSTERS_0_SSL_TRUSTSTOREPASSWORD: "secret" + DYNAMIC_CONFIG_ENABLED: 'true' # not necessary for ssl, added for tests + + volumes: + - ./ssl/kafka.truststore.jks:/kafka.truststore.jks + - ./ssl/kafka.keystore.jks:/kafka.keystore.jks + + kafka0: + image: confluentinc/cp-kafka:7.2.1 + hostname: kafka0 + container_name: kafka0 + ports: + - "9092:9092" + - "9997:9997" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,SSL:SSL,PLAINTEXT_HOST:PLAINTEXT' + KAFKA_ADVERTISED_LISTENERS: 'SSL://kafka0:29092,PLAINTEXT_HOST://localhost:9092' + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_JMX_PORT: 9997 + KAFKA_JMX_HOSTNAME: localhost + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093' + KAFKA_LISTENERS: 'SSL://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_INTER_BROKER_LISTENER_NAME: 'SSL' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' + KAFKA_SECURITY_PROTOCOL: SSL + KAFKA_SSL_ENABLED_MECHANISMS: PLAIN,SSL + KAFKA_SSL_KEYSTORE_FILENAME: kafka.keystore.jks + KAFKA_SSL_KEYSTORE_CREDENTIALS: creds + KAFKA_SSL_KEY_CREDENTIALS: creds + KAFKA_SSL_TRUSTSTORE_FILENAME: kafka.truststore.jks + KAFKA_SSL_TRUSTSTORE_CREDENTIALS: creds + #KAFKA_SSL_CLIENT_AUTH: 'required' + KAFKA_SSL_CLIENT_AUTH: 'requested' + KAFKA_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: '' # COMMON NAME VERIFICATION IS DISABLED SERVER-SIDE + volumes: + - ./scripts/update_run.sh:/tmp/update_run.sh + - ./ssl/creds:/etc/kafka/secrets/creds + - ./ssl/kafka.truststore.jks:/etc/kafka/secrets/kafka.truststore.jks + - ./ssl/kafka.keystore.jks:/etc/kafka/secrets/kafka.keystore.jks + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" + + schemaregistry0: + image: confluentinc/cp-schema-registry:7.2.1 + depends_on: + - kafka0 + environment: + SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: SSL://kafka0:29092 + SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: SSL + SCHEMA_REGISTRY_KAFKASTORE_SSL_TRUSTSTORE_LOCATION: /kafka.truststore.jks + SCHEMA_REGISTRY_KAFKASTORE_SSL_TRUSTSTORE_PASSWORD: secret + SCHEMA_REGISTRY_KAFKASTORE_SSL_KEYSTORE_LOCATION: /kafka.keystore.jks + SCHEMA_REGISTRY_KAFKASTORE_SSL_KEYSTORE_PASSWORD: secret + SCHEMA_REGISTRY_KAFKASTORE_SSL_KEY_PASSWORD: secret + SCHEMA_REGISTRY_HOST_NAME: schemaregistry0 + SCHEMA_REGISTRY_LISTENERS: https://schemaregistry0:8085 + SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: https + + SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: "https" + SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO + SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas + SCHEMA_REGISTRY_SSL_CLIENT_AUTHENTICATION: "REQUIRED" + SCHEMA_REGISTRY_SSL_TRUSTSTORE_LOCATION: /kafka.truststore.jks + SCHEMA_REGISTRY_SSL_TRUSTSTORE_PASSWORD: secret + SCHEMA_REGISTRY_SSL_KEYSTORE_LOCATION: /kafka.keystore.jks + SCHEMA_REGISTRY_SSL_KEYSTORE_PASSWORD: secret + SCHEMA_REGISTRY_SSL_KEY_PASSWORD: secret + ports: + - 8085:8085 + volumes: + - ./ssl/kafka.truststore.jks:/kafka.truststore.jks + - ./ssl/kafka.keystore.jks:/kafka.keystore.jks + + kafka-connect0: + image: confluentinc/cp-kafka-connect:7.2.1 + ports: + - 8083:8083 + depends_on: + - kafka0 + - schemaregistry0 + environment: + CONNECT_BOOTSTRAP_SERVERS: kafka0:29092 + CONNECT_GROUP_ID: compose-connect-group + CONNECT_CONFIG_STORAGE_TOPIC: _connect_configs + CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR: 1 + CONNECT_OFFSET_STORAGE_TOPIC: _connect_offset + CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR: 1 + CONNECT_STATUS_STORAGE_TOPIC: _connect_status + CONNECT_STATUS_STORAGE_REPLICATION_FACTOR: 1 + CONNECT_KEY_CONVERTER: org.apache.kafka.connect.storage.StringConverter + CONNECT_KEY_CONVERTER_SCHEMA_REGISTRY_URL: https://schemaregistry0:8085 + CONNECT_VALUE_CONVERTER: org.apache.kafka.connect.storage.StringConverter + CONNECT_VALUE_CONVERTER_SCHEMA_REGISTRY_URL: https://schemaregistry0:8085 + CONNECT_INTERNAL_KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter + CONNECT_INTERNAL_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter + CONNECT_REST_ADVERTISED_HOST_NAME: kafka-connect0 + CONNECT_PLUGIN_PATH: "/usr/share/java,/usr/share/confluent-hub-components" + CONNECT_SECURITY_PROTOCOL: "SSL" + CONNECT_SSL_KEYSTORE_LOCATION: "/kafka.keystore.jks" + CONNECT_SSL_KEY_PASSWORD: "secret" + CONNECT_SSL_KEYSTORE_PASSWORD: "secret" + CONNECT_SSL_TRUSTSTORE_LOCATION: "/kafka.truststore.jks" + CONNECT_SSL_TRUSTSTORE_PASSWORD: "secret" + CONNECT_SSL_CLIENT_AUTH: "requested" + CONNECT_REST_ADVERTISED_LISTENER: "https" + CONNECT_LISTENERS: "https://kafka-connect0:8083" + volumes: + - ./ssl/kafka.truststore.jks:/kafka.truststore.jks + - ./ssl/kafka.keystore.jks:/kafka.keystore.jks + + ksqldb0: + image: confluentinc/ksqldb-server:0.18.0 + depends_on: + - kafka0 + - kafka-connect0 + - schemaregistry0 + ports: + - 8088:8088 + environment: + KSQL_CUB_KAFKA_TIMEOUT: 120 + KSQL_LISTENERS: https://0.0.0.0:8088 + KSQL_BOOTSTRAP_SERVERS: SSL://kafka0:29092 + KSQL_SECURITY_PROTOCOL: SSL + KSQL_SSL_TRUSTSTORE_LOCATION: /kafka.truststore.jks + KSQL_SSL_TRUSTSTORE_PASSWORD: secret + KSQL_SSL_KEYSTORE_LOCATION: /kafka.keystore.jks + KSQL_SSL_KEYSTORE_PASSWORD: secret + KSQL_SSL_KEY_PASSWORD: secret + KSQL_SSL_CLIENT_AUTHENTICATION: REQUIRED + KSQL_KSQL_LOGGING_PROCESSING_STREAM_AUTO_CREATE: "true" + KSQL_KSQL_LOGGING_PROCESSING_TOPIC_AUTO_CREATE: "true" + KSQL_KSQL_CONNECT_URL: https://kafka-connect0:8083 + KSQL_KSQL_SCHEMA_REGISTRY_URL: https://schemaregistry0:8085 + KSQL_KSQL_SERVICE_ID: my_ksql_1 + KSQL_KSQL_HIDDEN_TOPICS: '^_.*' + KSQL_CACHE_MAX_BYTES_BUFFERING: 0 + volumes: + - ./ssl/kafka.truststore.jks:/kafka.truststore.jks + - ./ssl/kafka.keystore.jks:/kafka.keystore.jks diff --git a/documentation/compose/kafka-ssl.yml b/documentation/compose/kafka-ssl.yml index 367874fc5c7..08ff9dc4af8 100644 --- a/documentation/compose/kafka-ssl.yml +++ b/documentation/compose/kafka-ssl.yml @@ -1,47 +1,50 @@ --- version: '3.4' services: - kafka-ui: container_name: kafka-ui image: provectuslabs/kafka-ui:latest ports: - 8080:8080 depends_on: - - zookeeper0 - - kafka0 + - kafka environment: KAFKA_CLUSTERS_0_NAME: local KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL: SSL - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 # SSL LISTENER! - KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper0:2181 - KAFKA_CLUSTERS_0_PROPERTIES_SSL_TRUSTSTORE_LOCATION: /kafka.truststore.jks - KAFKA_CLUSTERS_0_PROPERTIES_SSL_TRUSTSTORE_PASSWORD: secret + KAFKA_CLUSTERS_0_PROPERTIES_SSL_KEYSTORE_LOCATION: /kafka.keystore.jks + KAFKA_CLUSTERS_0_PROPERTIES_SSL_KEYSTORE_PASSWORD: "secret" + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092 # SSL LISTENER! + KAFKA_CLUSTERS_0_SSL_TRUSTSTORELOCATION: /kafka.truststore.jks + KAFKA_CLUSTERS_0_SSL_TRUSTSTOREPASSWORD: "secret" + KAFKA_CLUSTERS_0_PROPERTIES_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: '' # DISABLE COMMON NAME VERIFICATION volumes: - ./ssl/kafka.truststore.jks:/kafka.truststore.jks + - ./ssl/kafka.keystore.jks:/kafka.keystore.jks - zookeeper0: - image: confluentinc/cp-zookeeper:6.0.1 - environment: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - ports: - - 2181:2181 - - kafka0: - image: confluentinc/cp-kafka:6.0.1 - hostname: kafka0 - depends_on: - - zookeeper0 + kafka: + image: confluentinc/cp-kafka:7.2.1 + hostname: kafka + container_name: kafka ports: - - '9092:9092' + - "9092:9092" + - "9997:9997" environment: KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper0:2181 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,SSL:SSL,PLAINTEXT_HOST:PLAINTEXT' + KAFKA_ADVERTISED_LISTENERS: 'SSL://kafka:29092,PLAINTEXT_HOST://localhost:9092' KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - KAFKA_ADVERTISED_LISTENERS: SSL://kafka0:29092,PLAINTEXT_HOST://localhost:9092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: SSL:SSL,PLAINTEXT_HOST:PLAINTEXT - KAFKA_INTER_BROKER_LISTENER_NAME: SSL + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_JMX_PORT: 9997 + KAFKA_JMX_HOSTNAME: localhost + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka:29093' + KAFKA_LISTENERS: 'SSL://kafka:29092,CONTROLLER://kafka:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_INTER_BROKER_LISTENER_NAME: 'SSL' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' KAFKA_SECURITY_PROTOCOL: SSL KAFKA_SSL_ENABLED_MECHANISMS: PLAIN,SSL KAFKA_SSL_KEYSTORE_FILENAME: kafka.keystore.jks @@ -50,9 +53,11 @@ services: KAFKA_SSL_TRUSTSTORE_FILENAME: kafka.truststore.jks KAFKA_SSL_TRUSTSTORE_CREDENTIALS: creds #KAFKA_SSL_CLIENT_AUTH: 'required' - KAFKA_SSL_CLIENT_AUTH: "requested" + KAFKA_SSL_CLIENT_AUTH: 'requested' KAFKA_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: '' # COMMON NAME VERIFICATION IS DISABLED SERVER-SIDE volumes: + - ./scripts/update_run.sh:/tmp/update_run.sh - ./ssl/creds:/etc/kafka/secrets/creds - ./ssl/kafka.truststore.jks:/etc/kafka/secrets/kafka.truststore.jks - ./ssl/kafka.keystore.jks:/etc/kafka/secrets/kafka.keystore.jks + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" diff --git a/documentation/compose/kafka-ui-acl-with-zk.yaml b/documentation/compose/kafka-ui-acl-with-zk.yaml new file mode 100644 index 00000000000..e1d70b29702 --- /dev/null +++ b/documentation/compose/kafka-ui-acl-with-zk.yaml @@ -0,0 +1,59 @@ +--- +version: '2' +services: + + kafka-ui: + container_name: kafka-ui + image: provectuslabs/kafka-ui:latest + ports: + - 8080:8080 + depends_on: + - zookeeper + - kafka + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092 + KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL: SASL_PLAINTEXT + KAFKA_CLUSTERS_0_PROPERTIES_SASL_MECHANISM: PLAIN + KAFKA_CLUSTERS_0_PROPERTIES_SASL_JAAS_CONFIG: 'org.apache.kafka.common.security.plain.PlainLoginModule required username="admin" password="admin-secret";' + + zookeeper: + image: wurstmeister/zookeeper:3.4.6 + environment: + JVMFLAGS: "-Djava.security.auth.login.config=/etc/zookeeper/zookeeper_jaas.conf" + volumes: + - ./jaas/zookeeper_jaas.conf:/etc/zookeeper/zookeeper_jaas.conf + ports: + - 2181:2181 + + kafka: + image: confluentinc/cp-kafka:7.2.1 + hostname: kafka + container_name: kafka + ports: + - "9092:9092" + - "9997:9997" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,SASL_PLAINTEXT:SASL_PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT' + KAFKA_ADVERTISED_LISTENERS: 'SASL_PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092' + KAFKA_OPTS: "-Djava.security.auth.login.config=/etc/kafka/jaas/kafka_server.conf" + KAFKA_AUTHORIZER_CLASS_NAME: "kafka.security.authorizer.AclAuthorizer" + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_JMX_PORT: 9997 + KAFKA_JMX_HOSTNAME: localhost + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka:29093' + KAFKA_LISTENERS: 'SASL_PLAINTEXT://kafka:29092,CONTROLLER://kafka:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_INTER_BROKER_LISTENER_NAME: 'SASL_PLAINTEXT' + KAFKA_SASL_ENABLED_MECHANISMS: 'PLAIN' + KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL: 'PLAIN' + KAFKA_SECURITY_PROTOCOL: 'SASL_PLAINTEXT' + KAFKA_SUPER_USERS: 'User:admin' + volumes: + - ./scripts/update_run.sh:/tmp/update_run.sh + - ./jaas:/etc/kafka/jaas diff --git a/documentation/compose/kafka-ui-arm64.yaml b/documentation/compose/kafka-ui-arm64.yaml new file mode 100644 index 00000000000..082d7cb5af0 --- /dev/null +++ b/documentation/compose/kafka-ui-arm64.yaml @@ -0,0 +1,106 @@ +# ARM64 supported images for kafka can be found here +# https://hub.docker.com/r/confluentinc/cp-kafka/tags?page=1&name=arm64 +--- +version: '2' +services: + kafka-ui: + container_name: kafka-ui + image: provectuslabs/kafka-ui:latest + ports: + - 8080:8080 + depends_on: + - kafka0 + - schema-registry0 + - kafka-connect0 + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 + KAFKA_CLUSTERS_0_METRICS_PORT: 9997 + KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schema-registry0:8085 + KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME: first + KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS: http://kafka-connect0:8083 + DYNAMIC_CONFIG_ENABLED: 'true' # not necessary, added for tests + KAFKA_CLUSTERS_0_AUDIT_TOPICAUDITENABLED: 'true' + KAFKA_CLUSTERS_0_AUDIT_CONSOLEAUDITENABLED: 'true' + + kafka0: + image: confluentinc/cp-kafka:7.2.1.arm64 + hostname: kafka0 + container_name: kafka0 + ports: + - 9092:9092 + - 9997:9997 + environment: + KAFKA_BROKER_ID: 1 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093' + KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' + KAFKA_JMX_PORT: 9997 + KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9997 + volumes: + - ./scripts/update_run.sh:/tmp/update_run.sh + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" + + schema-registry0: + image: confluentinc/cp-schema-registry:7.2.1.arm64 + ports: + - 8085:8085 + depends_on: + - kafka0 + environment: + SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092 + SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT + SCHEMA_REGISTRY_HOST_NAME: schema-registry0 + SCHEMA_REGISTRY_LISTENERS: http://schema-registry0:8085 + + SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: "http" + SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO + SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas + + kafka-connect0: + image: confluentinc/cp-kafka-connect:7.2.1.arm64 + ports: + - 8083:8083 + depends_on: + - kafka0 + - schema-registry0 + environment: + CONNECT_BOOTSTRAP_SERVERS: kafka0:29092 + CONNECT_GROUP_ID: compose-connect-group + CONNECT_CONFIG_STORAGE_TOPIC: _connect_configs + CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR: 1 + CONNECT_OFFSET_STORAGE_TOPIC: _connect_offset + CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR: 1 + CONNECT_STATUS_STORAGE_TOPIC: _connect_status + CONNECT_STATUS_STORAGE_REPLICATION_FACTOR: 1 + CONNECT_KEY_CONVERTER: org.apache.kafka.connect.storage.StringConverter + CONNECT_KEY_CONVERTER_SCHEMA_REGISTRY_URL: http://schema-registry0:8085 + CONNECT_VALUE_CONVERTER: org.apache.kafka.connect.storage.StringConverter + CONNECT_VALUE_CONVERTER_SCHEMA_REGISTRY_URL: http://schema-registry0:8085 + CONNECT_INTERNAL_KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter + CONNECT_INTERNAL_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter + CONNECT_REST_ADVERTISED_HOST_NAME: kafka-connect0 + CONNECT_PLUGIN_PATH: "/usr/share/java,/usr/share/confluent-hub-components" + + kafka-init-topics: + image: confluentinc/cp-kafka:7.2.1.arm64 + volumes: + - ./data/message.json:/data/message.json + depends_on: + - kafka0 + command: "bash -c 'echo Waiting for Kafka to be ready... && \ + cub kafka-ready -b kafka0:29092 1 30 && \ + kafka-topics --create --topic second.users --partitions 3 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \ + kafka-topics --create --topic second.messages --partitions 2 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \ + kafka-topics --create --topic first.messages --partitions 2 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \ + kafka-console-producer --bootstrap-server kafka0:29092 --topic second.users < /data/message.json'" diff --git a/documentation/compose/kafka-ui-auth-context.yaml b/documentation/compose/kafka-ui-auth-context.yaml index a3c4bee36b8..69eebbfeebb 100644 --- a/documentation/compose/kafka-ui-auth-context.yaml +++ b/documentation/compose/kafka-ui-auth-context.yaml @@ -8,52 +8,40 @@ services: ports: - 8080:8080 depends_on: - - zookeeper0 - - kafka0 + - kafka environment: KAFKA_CLUSTERS_0_NAME: local - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 - KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper0:2181 - KAFKA_CLUSTERS_0_JMXPORT: 9997 + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092 + KAFKA_CLUSTERS_0_METRICS_PORT: 9997 SERVER_SERVLET_CONTEXT_PATH: /kafkaui AUTH_TYPE: "LOGIN_FORM" SPRING_SECURITY_USER_NAME: admin SPRING_SECURITY_USER_PASSWORD: pass - zookeeper0: - image: confluentinc/cp-zookeeper:5.2.4 - environment: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - ports: - - 2181:2181 - - kafka0: - image: confluentinc/cp-kafka:5.3.1 - depends_on: - - zookeeper0 + kafka: + image: confluentinc/cp-kafka:7.2.1 + hostname: kafka + container_name: kafka ports: - - 9092:9092 - - 9997:9997 + - "9092:9092" + - "9997:9997" environment: KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper0:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT' + KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092' KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - JMX_PORT: 9997 - KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9997 - - kafka-init-topics: - image: confluentinc/cp-kafka:5.3.1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_JMX_PORT: 9997 + KAFKA_JMX_HOSTNAME: localhost + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka:29093' + KAFKA_LISTENERS: 'PLAINTEXT://kafka:29092,CONTROLLER://kafka:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' volumes: - - ./message.json:/data/message.json - depends_on: - - kafka0 - command: "bash -c 'echo Waiting for Kafka to be ready... && \ - cub kafka-ready -b kafka0:29092 1 30 && \ - kafka-topics --create --topic second.users --partitions 3 --replication-factor 1 --if-not-exists --zookeeper zookeeper0:2181 && \ - kafka-topics --create --topic second.messages --partitions 2 --replication-factor 1 --if-not-exists --zookeeper zookeeper0:2181 && \ - kafka-topics --create --topic first.messages --partitions 2 --replication-factor 1 --if-not-exists --zookeeper zookeeper0:2181 && \ - kafka-console-producer --broker-list kafka0:29092 -topic second.users < /data/message.json'" + - ./scripts/update_run.sh:/tmp/update_run.sh + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" \ No newline at end of file diff --git a/documentation/compose/kafka-ui-connectors-auth.yaml b/documentation/compose/kafka-ui-connectors-auth.yaml index a7367911206..d1f31f79696 100644 --- a/documentation/compose/kafka-ui-connectors-auth.yaml +++ b/documentation/compose/kafka-ui-connectors-auth.yaml @@ -1,68 +1,62 @@ --- -version: '2' +version: "2" services: - kafka-ui: container_name: kafka-ui image: provectuslabs/kafka-ui:latest ports: - 8080:8080 depends_on: - - zookeeper0 - - zookeeper1 - kafka0 - - kafka1 - schemaregistry0 - kafka-connect0 environment: KAFKA_CLUSTERS_0_NAME: local KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 - KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper0:2181 - KAFKA_CLUSTERS_0_JMXPORT: 9997 + KAFKA_CLUSTERS_0_METRICS_PORT: 9997 KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schemaregistry0:8085 KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME: first KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS: http://kafka-connect0:8083 KAFKA_CLUSTERS_0_KAFKACONNECT_0_USERNAME: admin KAFKA_CLUSTERS_0_KAFKACONNECT_0_PASSWORD: admin-secret - KAFKA_CLUSTERS_0_KSQLDBSERVER: http://ksqldb:8088 - - zookeeper0: - image: confluentinc/cp-zookeeper:5.2.4 - environment: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - ports: - - 2181:2181 kafka0: - image: confluentinc/cp-kafka:5.3.1 - depends_on: - - zookeeper0 + image: confluentinc/cp-kafka:7.2.1 + hostname: kafka0 + container_name: kafka0 ports: - - 9092:9092 - - 9997:9997 + - "9092:9092" + - "9997:9997" environment: KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper0:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT" + KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092" KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 - JMX_PORT: 9997 + KAFKA_JMX_PORT: 9997 + KAFKA_JMX_HOSTNAME: localhost KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9997 + KAFKA_PROCESS_ROLES: "broker,controller" + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: "1@kafka0:29093" + KAFKA_LISTENERS: "PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092" + KAFKA_INTER_BROKER_LISTENER_NAME: "PLAINTEXT" + KAFKA_CONTROLLER_LISTENER_NAMES: "CONTROLLER" + KAFKA_LOG_DIRS: "/tmp/kraft-combined-logs" + volumes: + - ./scripts/update_run.sh:/tmp/update_run.sh + command: 'bash -c ''if [ ! -f /tmp/update_run.sh ]; then echo "ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi''' schemaregistry0: - image: confluentinc/cp-schema-registry:5.5.0 + image: confluentinc/cp-schema-registry:7.2.1 ports: - 8085:8085 depends_on: - - zookeeper0 - kafka0 environment: SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092 - SCHEMA_REGISTRY_KAFKASTORE_CONNECTION_URL: zookeeper0:2181 SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT SCHEMA_REGISTRY_HOST_NAME: schemaregistry0 SCHEMA_REGISTRY_LISTENERS: http://schemaregistry0:8085 @@ -71,12 +65,11 @@ services: SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas - kafka-connect0: build: context: ./kafka-connect args: - image: confluentinc/cp-kafka-connect:6.0.1 + image: confluentinc/cp-kafka-connect:7.2.1 ports: - 8083:8083 depends_on: @@ -100,51 +93,22 @@ services: CONNECT_INTERNAL_KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter CONNECT_INTERNAL_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter CONNECT_REST_ADVERTISED_HOST_NAME: kafka-connect0 + CONNECT_REST_PORT: 8083 CONNECT_PLUGIN_PATH: "/usr/share/java,/usr/share/confluent-hub-components" CONNECT_REST_EXTENSION_CLASSES: "org.apache.kafka.connect.rest.basic.auth.extension.BasicAuthSecurityRestExtension" KAFKA_OPTS: "-Djava.security.auth.login.config=/conf/kafka_connect.jaas" -# AWS_ACCESS_KEY_ID: "" -# AWS_SECRET_ACCESS_KEY: "" + # AWS_ACCESS_KEY_ID: "" + # AWS_SECRET_ACCESS_KEY: "" kafka-init-topics: - image: confluentinc/cp-kafka:5.3.1 + image: confluentinc/cp-kafka:7.2.1 volumes: - - ./message.json:/data/message.json - depends_on: - - kafka1 - command: "bash -c 'echo Waiting for Kafka to be ready... && \ - cub kafka-ready -b kafka1:29092 1 30 && \ - kafka-topics --create --topic second.users --partitions 3 --replication-factor 1 --if-not-exists --zookeeper zookeeper1:2181 && \ - kafka-topics --create --topic second.messages --partitions 2 --replication-factor 1 --if-not-exists --zookeeper zookeeper1:2181 && \ - kafka-topics --create --topic first.messages --partitions 2 --replication-factor 1 --if-not-exists --zookeeper zookeeper0:2181 && \ - kafka-console-producer --broker-list kafka1:29092 -topic second.users < /data/message.json'" - - create-connectors: - image: ellerbrock/alpine-bash-curl-ssl - depends_on: - - postgres-db - - kafka-connect0 - volumes: - - ./connectors:/connectors - command: bash -c '/connectors/start.sh' - - ksqldb: - image: confluentinc/ksqldb-server:0.18.0 + - ./data/message.json:/data/message.json depends_on: - kafka0 - - kafka-connect0 - - schemaregistry0 - ports: - - 8088:8088 - environment: - KSQL_CUB_KAFKA_TIMEOUT: 120 - KSQL_LISTENERS: http://0.0.0.0:8088 - KSQL_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092 - KSQL_KSQL_LOGGING_PROCESSING_STREAM_AUTO_CREATE: "true" - KSQL_KSQL_LOGGING_PROCESSING_TOPIC_AUTO_CREATE: "true" - KSQL_KSQL_CONNECT_URL: http://kafka-connect0:8083 - KSQL_KSQL_SCHEMA_REGISTRY_URL: http://schemaregistry0:8085 - KSQL_KSQL_SERVICE_ID: my_ksql_1 - KSQL_KSQL_HIDDEN_TOPICS: '^_.*' - KSQL_CACHE_MAX_BYTES_BUFFERING: 0 + command: "bash -c 'echo Waiting for Kafka to be ready... && \ + cub kafka-ready -b kafka0:29092 1 30 && \ + kafka-topics --create --topic users --partitions 3 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \ + kafka-topics --create --topic messages --partitions 2 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \ + kafka-console-producer --bootstrap-server kafka0:29092 --topic users < /data/message.json'" diff --git a/documentation/compose/kafka-ui-connectors.yaml b/documentation/compose/kafka-ui-connectors.yaml deleted file mode 100644 index 4b3d4e42755..00000000000 --- a/documentation/compose/kafka-ui-connectors.yaml +++ /dev/null @@ -1,201 +0,0 @@ ---- -version: '2' -services: - - kafka-ui: - container_name: kafka-ui - image: provectuslabs/kafka-ui:latest - ports: - - 8080:8080 - depends_on: - - zookeeper0 - - zookeeper1 - - kafka0 - - kafka1 - - schemaregistry0 - - kafka-connect0 - environment: - KAFKA_CLUSTERS_0_NAME: local - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 - KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper0:2181 - KAFKA_CLUSTERS_0_JMXPORT: 9997 - KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schemaregistry0:8085 - KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME: first - KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS: http://kafka-connect0:8083 - KAFKA_CLUSTERS_0_KSQLDBSERVER: http://ksqldb:8088 - KAFKA_CLUSTERS_1_NAME: secondLocal - KAFKA_CLUSTERS_1_BOOTSTRAPSERVERS: kafka1:29092 - KAFKA_CLUSTERS_1_ZOOKEEPER: zookeeper1:2181 - KAFKA_CLUSTERS_1_JMXPORT: 9998 - KAFKA_CLUSTERS_1_SCHEMAREGISTRY: http://schemaregistry1:8085 - - zookeeper0: - image: confluentinc/cp-zookeeper:5.2.4 - environment: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - ports: - - 2181:2181 - - kafka0: - image: confluentinc/cp-kafka:5.3.1 - depends_on: - - zookeeper0 - ports: - - 9092:9092 - - 9997:9997 - environment: - KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper0:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 - KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 - JMX_PORT: 9997 - KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9997 - - zookeeper1: - image: confluentinc/cp-zookeeper:5.2.4 - environment: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - - kafka1: - image: confluentinc/cp-kafka:5.3.1 - depends_on: - - zookeeper1 - ports: - - 9093:9093 - - 9998:9998 - environment: - KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper1:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka1:29092,PLAINTEXT_HOST://localhost:9093 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 - KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 - JMX_PORT: 9998 - KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka1 -Dcom.sun.management.jmxremote.rmi.port=9998 - - schemaregistry0: - image: confluentinc/cp-schema-registry:5.5.0 - ports: - - 8085:8085 - depends_on: - - zookeeper0 - - kafka0 - environment: - SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092 - SCHEMA_REGISTRY_KAFKASTORE_CONNECTION_URL: zookeeper0:2181 - SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT - SCHEMA_REGISTRY_HOST_NAME: schemaregistry0 - SCHEMA_REGISTRY_LISTENERS: http://schemaregistry0:8085 - - SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: "http" - SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO - SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas - - schemaregistry1: - image: confluentinc/cp-schema-registry:5.5.0 - ports: - - 18085:8085 - depends_on: - - zookeeper1 - - kafka1 - environment: - SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka1:29092 - SCHEMA_REGISTRY_KAFKASTORE_CONNECTION_URL: zookeeper1:2181 - SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT - SCHEMA_REGISTRY_HOST_NAME: schemaregistry1 - SCHEMA_REGISTRY_LISTENERS: http://schemaregistry1:8085 - - SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: "http" - SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO - SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas - - kafka-connect0: - build: - context: ./kafka-connect - args: - image: confluentinc/cp-kafka-connect:6.0.1 - ports: - - 8083:8083 - depends_on: - - kafka0 - - schemaregistry0 - environment: - CONNECT_BOOTSTRAP_SERVERS: kafka0:29092 - CONNECT_GROUP_ID: compose-connect-group - CONNECT_CONFIG_STORAGE_TOPIC: _connect_configs - CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR: 1 - CONNECT_OFFSET_STORAGE_TOPIC: _connect_offset - CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR: 1 - CONNECT_STATUS_STORAGE_TOPIC: _connect_status - CONNECT_STATUS_STORAGE_REPLICATION_FACTOR: 1 - CONNECT_KEY_CONVERTER: org.apache.kafka.connect.storage.StringConverter - CONNECT_KEY_CONVERTER_SCHEMA_REGISTRY_URL: http://schemaregistry0:8085 - CONNECT_VALUE_CONVERTER: org.apache.kafka.connect.storage.StringConverter - CONNECT_VALUE_CONVERTER_SCHEMA_REGISTRY_URL: http://schemaregistry0:8085 - CONNECT_INTERNAL_KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter - CONNECT_INTERNAL_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter - CONNECT_REST_ADVERTISED_HOST_NAME: kafka-connect0 - CONNECT_PLUGIN_PATH: "/usr/share/java,/usr/share/confluent-hub-components" -# AWS_ACCESS_KEY_ID: "" -# AWS_SECRET_ACCESS_KEY: "" - - kafka-init-topics: - image: confluentinc/cp-kafka:5.3.1 - volumes: - - ./message.json:/data/message.json - depends_on: - - kafka1 - command: "bash -c 'echo Waiting for Kafka to be ready... && \ - cub kafka-ready -b kafka1:29092 1 30 && \ - kafka-topics --create --topic second.users --partitions 3 --replication-factor 1 --if-not-exists --zookeeper zookeeper1:2181 && \ - kafka-topics --create --topic second.messages --partitions 2 --replication-factor 1 --if-not-exists --zookeeper zookeeper1:2181 && \ - kafka-topics --create --topic first.messages --partitions 2 --replication-factor 1 --if-not-exists --zookeeper zookeeper0:2181 && \ - kafka-console-producer --broker-list kafka1:29092 -topic second.users < /data/message.json'" - - postgres-db: - build: - context: ./postgres - args: - image: postgres:9.6.22 - ports: - - 5432:5432 - environment: - POSTGRES_USER: 'dev_user' - POSTGRES_PASSWORD: '12345' - - create-connectors: - image: ellerbrock/alpine-bash-curl-ssl - depends_on: - - postgres-db - - kafka-connect0 - volumes: - - ./connectors:/connectors - command: bash -c '/connectors/start.sh' - - ksqldb: - image: confluentinc/ksqldb-server:0.18.0 - depends_on: - - kafka0 - - kafka-connect0 - - schemaregistry0 - ports: - - 8088:8088 - environment: - KSQL_CUB_KAFKA_TIMEOUT: 120 - KSQL_LISTENERS: http://0.0.0.0:8088 - KSQL_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092 - KSQL_KSQL_LOGGING_PROCESSING_STREAM_AUTO_CREATE: "true" - KSQL_KSQL_LOGGING_PROCESSING_TOPIC_AUTO_CREATE: "true" - KSQL_KSQL_CONNECT_URL: http://kafka-connect0:8083 - KSQL_KSQL_SCHEMA_REGISTRY_URL: http://schemaregistry0:8085 - KSQL_KSQL_SERVICE_ID: my_ksql_1 - KSQL_KSQL_HIDDEN_TOPICS: '^_.*' - KSQL_CACHE_MAX_BYTES_BUFFERING: 0 \ No newline at end of file diff --git a/documentation/compose/kafka-ui-jmx-secured.yml b/documentation/compose/kafka-ui-jmx-secured.yml index 133a19986d5..408f388ba54 100644 --- a/documentation/compose/kafka-ui-jmx-secured.yml +++ b/documentation/compose/kafka-ui-jmx-secured.yml @@ -7,56 +7,48 @@ services: image: provectuslabs/kafka-ui:latest ports: - 8080:8080 - - 5005:5005 depends_on: - - zookeeper0 - kafka0 - - schemaregistry0 - - kafka-connect0 environment: KAFKA_CLUSTERS_0_NAME: local KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 - KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper0:2181 KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schemaregistry0:8085 KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME: first KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS: http://kafka-connect0:8083 - KAFKA_CLUSTERS_0_JMXPORT: 9997 - KAFKA_CLUSTERS_0_JMXSSL: 'true' - KAFKA_CLUSTERS_0_JMXUSERNAME: root - KAFKA_CLUSTERS_0_JMXPASSWORD: password - JAVA_OPTS: >- - -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 - -Djavax.net.ssl.trustStore=/jmx/clienttruststore - -Djavax.net.ssl.trustStorePassword=12345678 - -Djavax.net.ssl.keyStore=/jmx/clientkeystore - -Djavax.net.ssl.keyStorePassword=12345678 + KAFKA_CLUSTERS_0_METRICS_PORT: 9997 + KAFKA_CLUSTERS_0_METRICS_USERNAME: root + KAFKA_CLUSTERS_0_METRICS_PASSWORD: password + KAFKA_CLUSTERS_0_METRICS_KEYSTORE_LOCATION: /jmx/clientkeystore + KAFKA_CLUSTERS_0_METRICS_KEYSTORE_PASSWORD: '12345678' + KAFKA_CLUSTERS_0_SSL_TRUSTSTORE_LOCATION: /jmx/clienttruststore + KAFKA_CLUSTERS_0_SSL_TRUSTSTORE_PASSWORD: '12345678' volumes: - ./jmx/clienttruststore:/jmx/clienttruststore - ./jmx/clientkeystore:/jmx/clientkeystore - zookeeper0: - image: confluentinc/cp-zookeeper:5.2.4 - environment: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - ports: - - 2181:2181 - kafka0: - image: confluentinc/cp-kafka:5.3.1 - depends_on: - - zookeeper0 + image: confluentinc/cp-kafka:7.2.1 + hostname: kafka0 + container_name: kafka0 ports: - 9092:9092 - 9997:9997 environment: KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper0:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT' + KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092' KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - JMX_PORT: 9997 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_JMX_PORT: 9997 + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093' + KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' # CHMOD 700 FOR JMXREMOTE.* FILES KAFKA_JMX_OPTS: >- -Dcom.sun.management.jmxremote @@ -72,65 +64,10 @@ services: -Dcom.sun.management.jmxremote.access.file=/jmx/jmxremote.access -Dcom.sun.management.jmxremote.rmi.port=9997 -Djava.rmi.server.hostname=kafka0 - -Djava.rmi.server.logCalls=true -# -Djavax.net.debug=ssl:handshake volumes: - - ./jmx/serverkeystore:/jmx/serverkeystore - - ./jmx/servertruststore:/jmx/servertruststore - - ./jmx/jmxremote.password:/jmx/jmxremote.password - - ./jmx/jmxremote.access:/jmx/jmxremote.access - - schemaregistry0: - image: confluentinc/cp-schema-registry:5.5.0 - ports: - - 8085:8085 - depends_on: - - zookeeper0 - - kafka0 - environment: - SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092 - SCHEMA_REGISTRY_KAFKASTORE_CONNECTION_URL: zookeeper0:2181 - SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT - SCHEMA_REGISTRY_HOST_NAME: schemaregistry0 - SCHEMA_REGISTRY_LISTENERS: http://schemaregistry0:8085 - - SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: "http" - SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO - SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas - - kafka-connect0: - image: confluentinc/cp-kafka-connect:6.0.1 - ports: - - 8083:8083 - depends_on: - - kafka0 - - schemaregistry0 - environment: - CONNECT_BOOTSTRAP_SERVERS: kafka0:29092 - CONNECT_GROUP_ID: compose-connect-group - CONNECT_CONFIG_STORAGE_TOPIC: _connect_configs - CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR: 1 - CONNECT_OFFSET_STORAGE_TOPIC: _connect_offset - CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR: 1 - CONNECT_STATUS_STORAGE_TOPIC: _connect_status - CONNECT_STATUS_STORAGE_REPLICATION_FACTOR: 1 - CONNECT_KEY_CONVERTER: org.apache.kafka.connect.storage.StringConverter - CONNECT_KEY_CONVERTER_SCHEMA_REGISTRY_URL: http://schemaregistry0:8085 - CONNECT_VALUE_CONVERTER: org.apache.kafka.connect.storage.StringConverter - CONNECT_VALUE_CONVERTER_SCHEMA_REGISTRY_URL: http://schemaregistry0:8085 - CONNECT_INTERNAL_KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter - CONNECT_INTERNAL_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter - CONNECT_REST_ADVERTISED_HOST_NAME: kafka-connect0 - CONNECT_PLUGIN_PATH: "/usr/share/java,/usr/share/confluent-hub-components" - - kafka-init-topics: - image: confluentinc/cp-kafka:5.3.1 - volumes: - - ./message.json:/data/message.json - depends_on: - - kafka0 - command: "bash -c 'echo Waiting for Kafka to be ready... && \ - cub kafka-ready -b kafka0:29092 1 30 && \ - kafka-topics --create --topic second.users --partitions 3 --replication-factor 1 --if-not-exists --zookeeper zookeeper0:2181 && \ - kafka-topics --create --topic first.messages --partitions 2 --replication-factor 1 --if-not-exists --zookeeper zookeeper0:2181 && \ - kafka-console-producer --broker-list kafka0:29092 -topic second.users < /data/message.json'" + - ./jmx/serverkeystore:/jmx/serverkeystore + - ./jmx/servertruststore:/jmx/servertruststore + - ./jmx/jmxremote.password:/jmx/jmxremote.password + - ./jmx/jmxremote.access:/jmx/jmxremote.access + - ./scripts/update_run.sh:/tmp/update_run.sh + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" diff --git a/documentation/compose/kafka-ui-reverse-proxy.yaml b/documentation/compose/kafka-ui-reverse-proxy.yaml deleted file mode 100644 index 69d94e627a6..00000000000 --- a/documentation/compose/kafka-ui-reverse-proxy.yaml +++ /dev/null @@ -1,19 +0,0 @@ ---- -version: '2' -services: - nginx: - image: nginx:latest - volumes: - - ./proxy.conf:/etc/nginx/conf.d/default.conf - ports: - - 8080:80 - - kafka-ui: - container_name: kafka-ui - image: provectuslabs/kafka-ui:latest - ports: - - 8082:8080 - environment: - KAFKA_CLUSTERS_0_NAME: local - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092 - SERVER_SERVLET_CONTEXT_PATH: /kafka-ui diff --git a/documentation/compose/kafka-ui-sasl.yaml b/documentation/compose/kafka-ui-sasl.yaml index 1c0312f11a2..e4a2b3cc4a7 100644 --- a/documentation/compose/kafka-ui-sasl.yaml +++ b/documentation/compose/kafka-ui-sasl.yaml @@ -8,45 +8,45 @@ services: ports: - 8080:8080 depends_on: - - zookeeper - kafka environment: KAFKA_CLUSTERS_0_NAME: local -# SERVER_SERVLET_CONTEXT_PATH: "/kafkaui" - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092 - KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper:2181 + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092 KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL: SASL_PLAINTEXT KAFKA_CLUSTERS_0_PROPERTIES_SASL_MECHANISM: PLAIN KAFKA_CLUSTERS_0_PROPERTIES_SASL_JAAS_CONFIG: 'org.apache.kafka.common.security.plain.PlainLoginModule required username="admin" password="admin-secret";' - zookeeper: - image: confluentinc/cp-zookeeper:5.2.4 - environment: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - ports: - - 2181:2181 + DYNAMIC_CONFIG_ENABLED: true # not necessary for sasl auth, added for tests kafka: - image: wurstmeister/kafka:latest + image: confluentinc/cp-kafka:7.2.1 hostname: kafka container_name: kafka - depends_on: - - zookeeper ports: - - '9092:9092' + - "9092:9092" + - "9997:9997" environment: - KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' - KAFKA_LISTENERS: SASL_PLAINTEXT://kafka:9092 - KAFKA_ADVERTISED_LISTENERS: SASL_PLAINTEXT://kafka:9092 - KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true' - ALLOW_PLAINTEXT_LISTENER: 'yes' + KAFKA_BROKER_ID: 1 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,SASL_PLAINTEXT:SASL_PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT' + KAFKA_ADVERTISED_LISTENERS: 'SASL_PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092' KAFKA_OPTS: "-Djava.security.auth.login.config=/etc/kafka/jaas/kafka_server.conf" - KAFKA_AUTHORIZER_CLASS_NAME: kafka.security.auth.SimpleAclAuthorizer - KAFKA_INTER_BROKER_LISTENER_NAME: SASL_PLAINTEXT - KAFKA_SASL_ENABLED_MECHANISMS: PLAIN - KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL: PLAIN - KAFKA_SECURITY_PROTOCOL: SASL_PLAINTEXT - KAFKA_SUPER_USERS: User:admin,User:enzo - KAFKA_ALLOW_EVERYONE_IF_NO_ACL_FOUND: 'true' + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_JMX_PORT: 9997 + KAFKA_JMX_HOSTNAME: localhost + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka:29093' + KAFKA_LISTENERS: 'SASL_PLAINTEXT://kafka:29092,CONTROLLER://kafka:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_INTER_BROKER_LISTENER_NAME: 'SASL_PLAINTEXT' + KAFKA_SASL_ENABLED_MECHANISMS: 'PLAIN' + KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL: 'PLAIN' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' + KAFKA_SECURITY_PROTOCOL: 'SASL_PLAINTEXT' + KAFKA_SUPER_USERS: 'User:admin,User:enzo' volumes: - - ./jaas:/etc/kafka/jaas \ No newline at end of file + - ./scripts/update_run.sh:/tmp/update_run.sh + - ./jaas:/etc/kafka/jaas + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" diff --git a/documentation/compose/kafka-ui-serdes.yaml b/documentation/compose/kafka-ui-serdes.yaml new file mode 100644 index 00000000000..eee510a13d6 --- /dev/null +++ b/documentation/compose/kafka-ui-serdes.yaml @@ -0,0 +1,113 @@ +--- +version: '2' +services: + + kafka-ui: + container_name: kafka-ui + image: provectuslabs/kafka-ui:latest + ports: + - 8080:8080 + depends_on: + - kafka0 + - schemaregistry0 + environment: + kafka.clusters.0.name: SerdeExampleCluster + kafka.clusters.0.bootstrapServers: kafka0:29092 + kafka.clusters.0.schemaRegistry: http://schemaregistry0:8085 + + # optional SSL settings for cluster (will be used by SchemaRegistry serde, if set) + #kafka.clusters.0.ssl.keystoreLocation: /kafka.keystore.jks + #kafka.clusters.0.ssl.keystorePassword: "secret" + #kafka.clusters.0.ssl.truststoreLocation: /kafka.truststore.jks + #kafka.clusters.0.ssl.truststorePassword: "secret" + + # optional auth properties for SR + #kafka.clusters.0.schemaRegistryAuth.username: "use" + #kafka.clusters.0.schemaRegistryAuth.password: "pswrd" + + kafka.clusters.0.defaultKeySerde: Int32 #optional + kafka.clusters.0.defaultValueSerde: String #optional + + kafka.clusters.0.serde.0.name: ProtobufFile + kafka.clusters.0.serde.0.topicKeysPattern: "topic1" + kafka.clusters.0.serde.0.topicValuesPattern: "topic1" + kafka.clusters.0.serde.0.properties.protobufFilesDir: /protofiles/ + kafka.clusters.0.serde.0.properties.protobufMessageNameForKey: test.MyKey # default type for keys + kafka.clusters.0.serde.0.properties.protobufMessageName: test.MyValue # default type for values + kafka.clusters.0.serde.0.properties.protobufMessageNameForKeyByTopic.topic1: test.MySpecificTopicKey # keys type for topic "topic1" + kafka.clusters.0.serde.0.properties.protobufMessageNameByTopic.topic1: test.MySpecificTopicValue # values type for topic "topic1" + + kafka.clusters.0.serde.1.name: String + #kafka.clusters.0.serde.1.properties.encoding: "UTF-16" #optional, default is UTF-8 + kafka.clusters.0.serde.1.topicValuesPattern: "json-events|text-events" + + kafka.clusters.0.serde.2.name: AsciiString + kafka.clusters.0.serde.2.className: com.provectus.kafka.ui.serdes.builtin.StringSerde + kafka.clusters.0.serde.2.properties.encoding: "ASCII" + + kafka.clusters.0.serde.3.name: SchemaRegistry # will be configured automatically using cluster SR + kafka.clusters.0.serde.3.topicValuesPattern: "sr-topic.*" + + kafka.clusters.0.serde.4.name: AnotherSchemaRegistry + kafka.clusters.0.serde.4.className: com.provectus.kafka.ui.serdes.builtin.sr.SchemaRegistrySerde + kafka.clusters.0.serde.4.properties.url: http://schemaregistry0:8085 + kafka.clusters.0.serde.4.properties.keySchemaNameTemplate: "%s-key" + kafka.clusters.0.serde.4.properties.schemaNameTemplate: "%s-value" + #kafka.clusters.0.serde.4.topicValuesPattern: "sr2-topic.*" + # optional auth and ssl properties for SR (overrides cluster-level): + #kafka.clusters.0.serde.4.properties.username: "user" + #kafka.clusters.0.serde.4.properties.password: "passw" + #kafka.clusters.0.serde.4.properties.keystoreLocation: /kafka.keystore.jks + #kafka.clusters.0.serde.4.properties.keystorePassword: "secret" + #kafka.clusters.0.serde.4.properties.truststoreLocation: /kafka.truststore.jks + #kafka.clusters.0.serde.4.properties.truststorePassword: "secret" + + kafka.clusters.0.serde.5.name: UInt64 + kafka.clusters.0.serde.5.topicKeysPattern: "topic-with-uint64keys" + volumes: + - ./proto:/protofiles + + kafka0: + image: confluentinc/cp-kafka:7.2.1 + hostname: kafka0 + container_name: kafka0 + ports: + - "9092:9092" + - "9997:9997" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT' + KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092' + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_JMX_PORT: 9997 + KAFKA_JMX_HOSTNAME: localhost + KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9997 + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093' + KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' + volumes: + - ./scripts/update_run.sh:/tmp/update_run.sh + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" + + schemaregistry0: + image: confluentinc/cp-schema-registry:7.2.1 + ports: + - 8085:8085 + depends_on: + - kafka0 + environment: + SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092 + SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT + SCHEMA_REGISTRY_HOST_NAME: schemaregistry0 + SCHEMA_REGISTRY_LISTENERS: http://schemaregistry0:8085 + + SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: "http" + SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO + SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas diff --git a/documentation/compose/kafka-ui-with-jmx-exporter.yaml b/documentation/compose/kafka-ui-with-jmx-exporter.yaml new file mode 100644 index 00000000000..b0d940694b1 --- /dev/null +++ b/documentation/compose/kafka-ui-with-jmx-exporter.yaml @@ -0,0 +1,44 @@ +--- +version: '2' +services: + + kafka0: + image: confluentinc/cp-kafka:7.2.1 + hostname: kafka0 + container_name: kafka0 + ports: + - "9092:9092" + - "11001:11001" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT' + KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092' + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093' + KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' + KAFKA_OPTS: -javaagent:/usr/share/jmx_exporter/jmx_prometheus_javaagent.jar=11001:/usr/share/jmx_exporter/kafka-broker.yml + volumes: + - ./jmx-exporter:/usr/share/jmx_exporter/ + - ./scripts/update_run.sh:/tmp/update_run.sh + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /usr/share/jmx_exporter/kafka-prepare-and-run ; fi'" + + kafka-ui: + container_name: kafka-ui + image: provectuslabs/kafka-ui:latest + ports: + - 8080:8080 + depends_on: + - kafka0 + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 + KAFKA_CLUSTERS_0_METRICS_PORT: 11001 + KAFKA_CLUSTERS_0_METRICS_TYPE: PROMETHEUS diff --git a/documentation/compose/kafka-ui.yaml b/documentation/compose/kafka-ui.yaml index 8afe6b6d2f1..14a269ca7cb 100644 --- a/documentation/compose/kafka-ui.yaml +++ b/documentation/compose/kafka-ui.yaml @@ -8,86 +8,88 @@ services: ports: - 8080:8080 depends_on: - - zookeeper0 - - zookeeper1 - kafka0 - kafka1 - schemaregistry0 + - schemaregistry1 - kafka-connect0 environment: KAFKA_CLUSTERS_0_NAME: local KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 - KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper0:2181 - KAFKA_CLUSTERS_0_JMXPORT: 9997 + KAFKA_CLUSTERS_0_METRICS_PORT: 9997 KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schemaregistry0:8085 KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME: first KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS: http://kafka-connect0:8083 KAFKA_CLUSTERS_1_NAME: secondLocal KAFKA_CLUSTERS_1_BOOTSTRAPSERVERS: kafka1:29092 - KAFKA_CLUSTERS_1_ZOOKEEPER: zookeeper1:2181 - KAFKA_CLUSTERS_1_JMXPORT: 9998 + KAFKA_CLUSTERS_1_METRICS_PORT: 9998 KAFKA_CLUSTERS_1_SCHEMAREGISTRY: http://schemaregistry1:8085 - KAFKA_CLUSTERS_1_KAFKACONNECT_0_NAME: first - KAFKA_CLUSTERS_1_KAFKACONNECT_0_ADDRESS: http://kafka-connect0:8083 - - zookeeper0: - image: confluentinc/cp-zookeeper:5.2.4 - environment: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - ports: - - 2181:2181 + DYNAMIC_CONFIG_ENABLED: 'true' kafka0: - image: confluentinc/cp-kafka:5.3.1 - depends_on: - - zookeeper0 + image: confluentinc/cp-kafka:7.2.1 + hostname: kafka0 + container_name: kafka0 ports: - - 9092:9092 - - 9997:9997 + - "9092:9092" + - "9997:9997" environment: KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper0:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT' + KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092' KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - JMX_PORT: 9997 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_JMX_PORT: 9997 KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9997 - - zookeeper1: - image: confluentinc/cp-zookeeper:5.2.4 - environment: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093' + KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' + volumes: + - ./scripts/update_run.sh:/tmp/update_run.sh + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" kafka1: - image: confluentinc/cp-kafka:5.3.1 - depends_on: - - zookeeper1 + image: confluentinc/cp-kafka:7.2.1 + hostname: kafka1 + container_name: kafka1 ports: - - 9093:9093 - - 9998:9998 + - "9093:9092" + - "9998:9998" environment: KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper1:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka1:29092,PLAINTEXT_HOST://localhost:9093 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT' + KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka1:29092,PLAINTEXT_HOST://localhost:9092' KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - JMX_PORT: 9998 - KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka1 -Dcom.sun.management.jmxremote.rmi.port=9998 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_JMX_PORT: 9998 + KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9998 + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka1:29093' + KAFKA_LISTENERS: 'PLAINTEXT://kafka1:29092,CONTROLLER://kafka1:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' + volumes: + - ./scripts/update_run.sh:/tmp/update_run.sh + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" schemaregistry0: - image: confluentinc/cp-schema-registry:5.5.0 + image: confluentinc/cp-schema-registry:7.2.1 ports: - 8085:8085 depends_on: - - zookeeper0 - kafka0 environment: SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092 - SCHEMA_REGISTRY_KAFKASTORE_CONNECTION_URL: zookeeper0:2181 SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT SCHEMA_REGISTRY_HOST_NAME: schemaregistry0 SCHEMA_REGISTRY_LISTENERS: http://schemaregistry0:8085 @@ -97,15 +99,13 @@ services: SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas schemaregistry1: - image: confluentinc/cp-schema-registry:5.5.0 + image: confluentinc/cp-schema-registry:7.2.1 ports: - 18085:8085 depends_on: - - zookeeper1 - kafka1 environment: SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka1:29092 - SCHEMA_REGISTRY_KAFKASTORE_CONNECTION_URL: zookeeper1:2181 SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT SCHEMA_REGISTRY_HOST_NAME: schemaregistry1 SCHEMA_REGISTRY_LISTENERS: http://schemaregistry1:8085 @@ -115,7 +115,7 @@ services: SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas kafka-connect0: - image: confluentinc/cp-kafka-connect:6.0.1 + image: confluentinc/cp-kafka-connect:7.2.1 ports: - 8083:8083 depends_on: @@ -140,14 +140,14 @@ services: CONNECT_PLUGIN_PATH: "/usr/share/java,/usr/share/confluent-hub-components" kafka-init-topics: - image: confluentinc/cp-kafka:5.3.1 + image: confluentinc/cp-kafka:7.2.1 volumes: - - ./message.json:/data/message.json + - ./data/message.json:/data/message.json depends_on: - kafka1 command: "bash -c 'echo Waiting for Kafka to be ready... && \ cub kafka-ready -b kafka1:29092 1 30 && \ - kafka-topics --create --topic second.users --partitions 3 --replication-factor 1 --if-not-exists --zookeeper zookeeper1:2181 && \ - kafka-topics --create --topic second.messages --partitions 2 --replication-factor 1 --if-not-exists --zookeeper zookeeper1:2181 && \ - kafka-topics --create --topic first.messages --partitions 2 --replication-factor 1 --if-not-exists --zookeeper zookeeper0:2181 && \ - kafka-console-producer --broker-list kafka1:29092 -topic second.users < /data/message.json'" + kafka-topics --create --topic second.users --partitions 3 --replication-factor 1 --if-not-exists --bootstrap-server kafka1:29092 && \ + kafka-topics --create --topic second.messages --partitions 2 --replication-factor 1 --if-not-exists --bootstrap-server kafka1:29092 && \ + kafka-topics --create --topic first.messages --partitions 2 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \ + kafka-console-producer --bootstrap-server kafka1:29092 -topic second.users < /data/message.json'" diff --git a/documentation/compose/kafka-with-zookeeper.yaml b/documentation/compose/kafka-with-zookeeper.yaml new file mode 100644 index 00000000000..7342a976314 --- /dev/null +++ b/documentation/compose/kafka-with-zookeeper.yaml @@ -0,0 +1,48 @@ +--- +version: '2' +services: + + zookeeper: + image: confluentinc/cp-zookeeper:7.2.1 + hostname: zookeeper + container_name: zookeeper + ports: + - "2181:2181" + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + + kafka: + image: confluentinc/cp-server:7.2.1 + hostname: kafka + container_name: kafka + depends_on: + - zookeeper + ports: + - "9092:9092" + - "9997:9997" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_CONFLUENT_LICENSE_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_CONFLUENT_BALANCER_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_JMX_PORT: 9997 + KAFKA_JMX_HOSTNAME: kafka + + kafka-init-topics: + image: confluentinc/cp-kafka:7.2.1 + volumes: + - ./data/message.json:/data/message.json + depends_on: + - kafka + command: "bash -c 'echo Waiting for Kafka to be ready... && \ + cub kafka-ready -b kafka:29092 1 30 && \ + kafka-topics --create --topic users --partitions 3 --replication-factor 1 --if-not-exists --bootstrap-server kafka:29092 && \ + kafka-topics --create --topic messages --partitions 2 --replication-factor 1 --if-not-exists --bootstrap-server kafka:29092 && \ + kafka-console-producer --bootstrap-server kafka:29092 --topic users < /data/message.json'" diff --git a/documentation/compose/ldap.yaml b/documentation/compose/ldap.yaml new file mode 100644 index 00000000000..e4ff68f3ba0 --- /dev/null +++ b/documentation/compose/ldap.yaml @@ -0,0 +1,79 @@ +--- +version: '2' +services: + + kafka-ui: + container_name: kafka-ui + image: provectuslabs/kafka-ui:latest + ports: + - 8080:8080 + depends_on: + - kafka0 + - schemaregistry0 + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 + KAFKA_CLUSTERS_0_METRICS_PORT: 9997 + KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schemaregistry0:8085 + + AUTH_TYPE: "LDAP" + SPRING_LDAP_URLS: "ldap://ldap:10389" + SPRING_LDAP_BASE: "cn={0},ou=people,dc=planetexpress,dc=com" + SPRING_LDAP_ADMIN_USER: "cn=admin,dc=planetexpress,dc=com" + SPRING_LDAP_ADMIN_PASSWORD: "GoodNewsEveryone" + SPRING_LDAP_USER_FILTER_SEARCH_BASE: "dc=planetexpress,dc=com" + SPRING_LDAP_USER_FILTER_SEARCH_FILTER: "(&(uid={0})(objectClass=inetOrgPerson))" + SPRING_LDAP_GROUP_FILTER_SEARCH_BASE: "ou=people,dc=planetexpress,dc=com" +# OAUTH2.LDAP.ACTIVEDIRECTORY: true +# OAUTH2.LDAP.AСTIVEDIRECTORY.DOMAIN: "memelord.lol" + + ldap: + image: rroemhild/test-openldap:latest + hostname: "ldap" + ports: + - 10389:10389 + + kafka0: + image: confluentinc/cp-kafka:7.2.1 + hostname: kafka0 + container_name: kafka0 + ports: + - "9092:9092" + - "9997:9997" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT' + KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092' + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_JMX_PORT: 9997 + KAFKA_JMX_HOSTNAME: localhost + KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9997 + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093' + KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' + volumes: + - ./scripts/update_run.sh:/tmp/update_run.sh + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" + + schemaregistry0: + image: confluentinc/cp-schema-registry:7.2.1 + ports: + - 8085:8085 + depends_on: + - kafka0 + environment: + SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092 + SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT + SCHEMA_REGISTRY_HOST_NAME: schemaregistry0 + SCHEMA_REGISTRY_LISTENERS: http://schemaregistry0:8085 + + SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: "http" + SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO + SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas diff --git a/documentation/compose/nginx-proxy.yaml b/documentation/compose/nginx-proxy.yaml new file mode 100644 index 00000000000..9a255a5eb9e --- /dev/null +++ b/documentation/compose/nginx-proxy.yaml @@ -0,0 +1,19 @@ +--- +version: '2' +services: + nginx: + image: nginx:latest + volumes: + - ./data/proxy.conf:/etc/nginx/conf.d/default.conf + ports: + - 8080:80 + + kafka-ui: + container_name: kafka-ui + image: provectuslabs/kafka-ui:latest + ports: + - 8082:8080 + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092 + SERVER_SERVLET_CONTEXT_PATH: /kafka-ui diff --git a/documentation/compose/proto/key-types.proto b/documentation/compose/proto/key-types.proto new file mode 100644 index 00000000000..1f5e22a427d --- /dev/null +++ b/documentation/compose/proto/key-types.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; +package test; + +import "google/protobuf/wrappers.proto"; + +message MyKey { + string myKeyF1 = 1; + google.protobuf.UInt64Value uint_64_wrapper = 2; +} + +message MySpecificTopicKey { + string special_field1 = 1; + string special_field2 = 2; + google.protobuf.FloatValue float_wrapper = 3; +} diff --git a/documentation/compose/proto/values.proto b/documentation/compose/proto/values.proto new file mode 100644 index 00000000000..fff8d9bbd96 --- /dev/null +++ b/documentation/compose/proto/values.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; +package test; + +message MySpecificTopicValue { + string f1 = 1; + string f2 = 2; +} + +message MyValue { + int32 version = 1; + string payload = 2; + map intToStringMap = 3; + map strToObjMap = 4; +} diff --git a/documentation/compose/scripts/clusterID b/documentation/compose/scripts/clusterID new file mode 100644 index 00000000000..4417a5a68d7 --- /dev/null +++ b/documentation/compose/scripts/clusterID @@ -0,0 +1 @@ +zlFiTJelTOuhnklFwLWixw \ No newline at end of file diff --git a/documentation/compose/scripts/create_cluster_id.sh b/documentation/compose/scripts/create_cluster_id.sh new file mode 100644 index 00000000000..d946fbc4af3 --- /dev/null +++ b/documentation/compose/scripts/create_cluster_id.sh @@ -0,0 +1 @@ +kafka-storage random-uuid > /workspace/kafka-ui/documentation/compose/clusterID \ No newline at end of file diff --git a/documentation/compose/scripts/update_run.sh b/documentation/compose/scripts/update_run.sh new file mode 100755 index 00000000000..023c832b4e1 --- /dev/null +++ b/documentation/compose/scripts/update_run.sh @@ -0,0 +1,11 @@ +# This script is required to run kafka cluster (without zookeeper) +#!/bin/sh + +# Docker workaround: Remove check for KAFKA_ZOOKEEPER_CONNECT parameter +sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure + +# Docker workaround: Ignore cub zk-ready +sed -i 's/cub zk-ready/echo ignore zk-ready/' /etc/confluent/docker/ensure + +# KRaft required step: Format the storage directory with a new cluster ID +echo "kafka-storage format --ignore-formatted -t $(kafka-storage random-uuid) -c /etc/kafka/kafka.properties" >> /etc/confluent/docker/ensure \ No newline at end of file diff --git a/documentation/compose/scripts/update_run_cluster.sh b/documentation/compose/scripts/update_run_cluster.sh new file mode 100644 index 00000000000..31da333aae6 --- /dev/null +++ b/documentation/compose/scripts/update_run_cluster.sh @@ -0,0 +1,11 @@ +# This script is required to run kafka cluster (without zookeeper) +#!/bin/sh + +# Docker workaround: Remove check for KAFKA_ZOOKEEPER_CONNECT parameter +sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure + +# Docker workaround: Ignore cub zk-ready +sed -i 's/cub zk-ready/echo ignore zk-ready/' /etc/confluent/docker/ensure + +# KRaft required step: Format the storage directory with a new cluster ID +echo "kafka-storage format --ignore-formatted -t $(cat /tmp/clusterID) -c /etc/kafka/kafka.properties" >> /etc/confluent/docker/ensure \ No newline at end of file diff --git a/documentation/compose/ssl/generate_certs.sh b/documentation/compose/ssl/generate_certs.sh old mode 100644 new mode 100755 index ebb916657bd..455321ef580 --- a/documentation/compose/ssl/generate_certs.sh +++ b/documentation/compose/ssl/generate_certs.sh @@ -144,7 +144,8 @@ echo "Now the trust store's private key (CA) will sign the keystore's certificat echo openssl x509 -req -CA $CA_CERT_FILE -CAkey $trust_store_private_key_file \ -in $KEYSTORE_SIGN_REQUEST -out $KEYSTORE_SIGNED_CERT \ - -days $VALIDITY_IN_DAYS -CAcreateserial + -days $VALIDITY_IN_DAYS -CAcreateserial \ + -extensions kafka -extfile san.cnf # creates $KEYSTORE_SIGN_REQUEST_SRL which is never used or needed. echo diff --git a/documentation/compose/ssl/kafka.keystore.jks b/documentation/compose/ssl/kafka.keystore.jks index 54b3f3d1bc4..eab29e914a8 100644 Binary files a/documentation/compose/ssl/kafka.keystore.jks and b/documentation/compose/ssl/kafka.keystore.jks differ diff --git a/documentation/compose/ssl/kafka.truststore.jks b/documentation/compose/ssl/kafka.truststore.jks index eff350ce4e9..875caf6f156 100644 Binary files a/documentation/compose/ssl/kafka.truststore.jks and b/documentation/compose/ssl/kafka.truststore.jks differ diff --git a/documentation/compose/ssl/san.cnf b/documentation/compose/ssl/san.cnf new file mode 100644 index 00000000000..5c69c8eca61 --- /dev/null +++ b/documentation/compose/ssl/san.cnf @@ -0,0 +1,2 @@ +[kafka] +subjectAltName = DNS:kafka0,DNS:schemaregistry0,DNS:kafka-connect0,DNS:ksqldb0 diff --git a/documentation/compose/kafka-ui-traefik-proxy.yaml b/documentation/compose/traefik-proxy.yaml similarity index 100% rename from documentation/compose/kafka-ui-traefik-proxy.yaml rename to documentation/compose/traefik-proxy.yaml diff --git a/documentation/guides/AWS_IAM.md b/documentation/guides/AWS_IAM.md deleted file mode 100644 index 80bfab205bc..00000000000 --- a/documentation/guides/AWS_IAM.md +++ /dev/null @@ -1,41 +0,0 @@ -# How to configure AWS IAM Authentication - -UI for Apache Kafka comes with built-in [aws-msk-iam-auth](https://github.com/aws/aws-msk-iam-auth) library. - -You could pass sasl configs in properties section for each cluster. - -More details could be found here: [aws-msk-iam-auth](https://github.com/aws/aws-msk-iam-auth) - -## Examples: - -Please replace -* with broker list -* with your aws profile - - -### Running From Docker Image - -```sh -docker run -p 8080:8080 \ - -e KAFKA_CLUSTERS_0_NAME=local \ - -e KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS= \ - -e KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL=SASL_SSL \ - -e KAFKA_CLUSTERS_0_PROPERTIES_SASL_MECHANISM=AWS_MSK_IAM \ - -e KAFKA_CLUSTERS_0_PROPERTIES_SASL_CLIENT_CALLBACK_HANDLER_CLASS=software.amazon.msk.auth.iam.IAMClientCallbackHandler \ - -e KAFKA_CLUSTERS_0_PROPERTIES_SASL_JAAS_CONFIG=software.amazon.msk.auth.iam.IAMLoginModule required awsProfileName=""; \ - -d provectuslabs/kafka-ui:latest -``` - -### Configuring by application.yaml - -```yaml -kafka: - clusters: - - name: local - bootstrapServers: - properties: - security.protocol: SASL_SSL - sasl.mechanism: AWS_MSK_IAM - sasl.client.callback.handler.class: software.amazon.msk.auth.iam.IAMClientCallbackHandler - sasl.jaas.config: software.amazon.msk.auth.iam.IAMLoginModule required awsProfileName=""; -``` \ No newline at end of file diff --git a/documentation/guides/Protobuf.md b/documentation/guides/Protobuf.md deleted file mode 100644 index d7c50ffb65e..00000000000 --- a/documentation/guides/Protobuf.md +++ /dev/null @@ -1,33 +0,0 @@ -# Kafkaui Protobuf Support - -Kafkaui supports deserializing protobuf messages in two ways: -1. Using Confluent Schema Registry's [protobuf support](https://docs.confluent.io/platform/current/schema-registry/serdes-develop/serdes-protobuf.html). -2. Supplying a protobuf file as well as a configuration that maps topic names to protobuf types. - -## Configuring Kafkaui with a Protobuf File - -To configure Kafkaui to deserialize protobuf messages using a supplied protobuf schema add the following to the config: -```yaml -kafka: - clusters: - - # Cluster configuration omitted. - # protobufFile is the path to the protobuf schema. - protobufFile: path/to/my.proto - # protobufMessageName is the default protobuf type that is used to deserilize - # the message's value if the topic is not found in protobufMessageNameByTopic. - protobufMessageName: my.Type1 - # protobufMessageNameByTopic is a mapping of topic names to protobuf types. - # This mapping is required and is used to deserialize the Kafka message's value. - protobufMessageNameByTopic: - topic1: my.Type1 - topic2: my.Type2 - # protobufMessageNameForKey is the default protobuf type that is used to deserilize - # the message's key if the topic is not found in protobufMessageNameForKeyByTopic. - protobufMessageNameForKey: my.Type1 - # protobufMessageNameForKeyByTopic is a mapping of topic names to protobuf types. - # This mapping is optional and is used to deserialize the Kafka message's key. - # If a protobuf type is not found for a topic's key, the key is deserialized as a string, - # unless protobufMessageNameForKey is specified. - protobufMessageNameForKeyByTopic: - topic1: my.KeyType1 -``` \ No newline at end of file diff --git a/documentation/guides/SASL_SCRAM.md b/documentation/guides/SASL_SCRAM.md deleted file mode 100644 index be360cd0aec..00000000000 --- a/documentation/guides/SASL_SCRAM.md +++ /dev/null @@ -1,58 +0,0 @@ -# How to configure SASL SCRAM Authentication - -You could pass sasl configs in properties section for each cluster. - -## Examples: - -Please replace -- with cluster name -- with broker list -- with username -- with password - -### Running From Docker Image - -```sh -docker run -p 8080:8080 \ - -e KAFKA_CLUSTERS_0_NAME= \ - -e KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS= \ - -e KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL=SASL_SSL \ - -e KAFKA_CLUSTERS_0_PROPERTIES_SASL_MECHANISM=SCRAM-SHA-512 \ - -e KAFKA_CLUSTERS_0_PROPERTIES_SASL_JAAS_CONFIG=org.apache.kafka.common.security.scram.ScramLoginModule required username="" password=""; \ - -d provectuslabs/kafka-ui:latest -``` - -### Running From Docker-compose file - -```yaml - -version: '3.4' -services: - - kafka-ui: - image: provectuslabs/kafka-ui - container_name: kafka-ui - ports: - - "888:8080" - restart: always - environment: - - KAFKA_CLUSTERS_0_NAME= - - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS= - - KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL=SASL_SSL - - KAFKA_CLUSTERS_0_PROPERTIES_SASL_MECHANISM=SCRAM-SHA-512 - - KAFKA_CLUSTERS_0_PROPERTIES_SASL_JAAS_CONFIG=org.apache.kafka.common.security.scram.ScramLoginModule required username="" password=""; - - KAFKA_CLUSTERS_0_PROPERTIES_PROTOCOL=SASL -``` - -### Configuring by application.yaml - -```yaml -kafka: - clusters: - - name: local - bootstrapServers: - properties: - security.protocol: SASL_SSL - sasl.mechanism: SCRAM-SHA-512 - sasl.jaas.config: org.apache.kafka.common.security.scram.ScramLoginModule required username="" password=""; -``` diff --git a/documentation/guides/SECURE_BROKER.md b/documentation/guides/SECURE_BROKER.md deleted file mode 100644 index ab15bb63ab3..00000000000 --- a/documentation/guides/SECURE_BROKER.md +++ /dev/null @@ -1,7 +0,0 @@ -## Connecting to a Secure Broker - -The app supports TLS (SSL) and SASL connections for [encryption and authentication](http://kafka.apache.org/090/documentation.html#security).
- -### Running From Docker-compose file - -See [this](/documentation/compose/kafka-ssl.yml) docker-compose file reference for ssl-enabled kafka diff --git a/documentation/guides/SSO.md b/documentation/guides/SSO.md deleted file mode 100644 index 1ddfab2c7fa..00000000000 --- a/documentation/guides/SSO.md +++ /dev/null @@ -1,72 +0,0 @@ -# How to configure SSO -SSO require additionaly to configure TLS for application, in that example we will use self-signed certificate, in case of use legal certificates please skip step 1. -## Step 1 -At this step we will generate self-signed PKCS12 keypair. -``` bash -mkdir cert -keytool -genkeypair -alias ui-for-apache-kafka -keyalg RSA -keysize 2048 \ - -storetype PKCS12 -keystore cert/ui-for-apache-kafka.p12 -validity 3650 -``` -## Step 2 -Create new application in any SSO provider, we will continue with [Auth0](https://auth0.com). - - - -After that need to provide callback URLs, in our case we will use `https://127.0.0.1:8080/login/oauth2/code/auth0` - - - -This is a main parameters required for enabling SSO - - - -## Step 3 -To launch UI for Apache Kafka with enabled TLS and SSO run following: -``` bash -docker run -p 8080:8080 -v `pwd`/cert:/opt/cert -e AUTH_TYPE=LOGIN_FORM \ - -e SECURITY_BASIC_ENABLED=true \ - -e SERVER_SSL_KEY_STORE_TYPE=PKCS12 \ - -e SERVER_SSL_KEY_STORE=/opt/cert/ui-for-apache-kafka.p12 \ - -e SERVER_SSL_KEY_STORE_PASSWORD=123456 \ - -e SERVER_SSL_KEY_ALIAS=ui-for-apache-kafka \ - -e SERVER_SSL_ENABLED=true \ - -e SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_AUTH0_CLIENTID=uhvaPKIHU4ZF8Ne4B6PGvF0hWW6OcUSB \ - -e SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_AUTH0_CLIENTSECRET=YXfRjmodifiedTujnkVr7zuW9ECCAK4TcnCio-i \ - -e SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_AUTH0_ISSUER_URI=https://dev-a63ggcut.auth0.com/ \ - -e SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_AUTH0_SCOPE=openid \ - -e TRUST_STORE=/opt/cert/ui-for-apache-kafka.p12 \ - -e TRUST_STORE_PASSWORD=123456 \ -provectuslabs/kafka-ui:latest -``` -In the case with trusted CA-signed SSL certificate and SSL termination somewhere outside of application we can pass only SSO related environment variables: -``` bash -docker run -p 8080:8080 -v `pwd`/cert:/opt/cert -e AUTH_TYPE=OAUTH2 \ - -e SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_AUTH0_CLIENTID=uhvaPKIHU4ZF8Ne4B6PGvF0hWW6OcUSB \ - -e SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_AUTH0_CLIENTSECRET=YXfRjmodifiedTujnkVr7zuW9ECCAK4TcnCio-i \ - -e SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_AUTH0_ISSUER_URI=https://dev-a63ggcut.auth0.com/ \ - -e SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_AUTH0_SCOPE=openid \ -provectuslabs/kafka-ui:latest -``` - -## Step 4 (Load Balancer HTTP) (optional) -If you're using load balancer/proxy and use HTTP between the proxy and the app, you might want to set `server_forward-headers-strategy` to `native` as well (`SERVER_FORWARDHEADERSSTRATEGY=native`), for more info refer to [this issue](https://github.com/provectus/kafka-ui/issues/1017). - -## Step 5 (Azure) (optional) -For Azure AD (Office365) OAUTH2 you'll want to add additional environment variables: - -```bash -docker run -p 8080:8080 \ - -e KAFKA_CLUSTERS_0_NAME="${cluster_name}"\ - -e KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS="${kafka_listeners}" \ - -e KAFKA_CLUSTERS_0_ZOOKEEPER="${zookeeper_servers}" \ - -e KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS="${kafka_connect_servers}" - -e AUTH_TYPE=OAUTH2 \ - -e SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_AUTH0_CLIENTID=uhvaPKIHU4ZF8Ne4B6PGvF0hWW6OcUSB \ - -e SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_AUTH0_CLIENTSECRET=YXfRjmodifiedTujnkVr7zuW9ECCAK4TcnCio-i \ - -e SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_AUTH0_SCOPE="https://graph.microsoft.com/User.Read" \ - -e SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_AUTH0_ISSUER_URI="https://login.microsoftonline.com/{tenant-id}/v2.0" \ - -d provectuslabs/kafka-ui:latest" -``` - -Note that scope is created by default when Application registration is done in Azure portal. -You'll need to update application registration manifest to include `"accessTokenAcceptedVersion": 2` diff --git a/documentation/project/ROADMAP.md b/documentation/project/ROADMAP.md deleted file mode 100644 index 407dab725a1..00000000000 --- a/documentation/project/ROADMAP.md +++ /dev/null @@ -1,22 +0,0 @@ -Kafka-UI Project Roadmap -==================== - -Roadmap exists in a form of a github project board and is located [here](https://github.com/provectus/kafka-ui/projects/8). - -### How to use this document - -The roadmap provides a list of features we decided to prioritize in project development. It should serve as a reference point to understand projects' goals. - -We do prioritize them based on the feedback from the community, our own vision and other conditions and circumstances. - -The roadmap sets the general way of development. The roadmap is mostly about long-term features. All the features could be re-prioritized, rescheduled or canceled. - -If there's no feature `X`, that **doesn't** mean we're **not** going to implement it. Feel free to raise the issue for the consideration.
-If a feature you want to see live is not present on roadmap, but there's an issue for the feature, feel free to vote for it using reactions in the issue. - - -### How to contribute - -Since the roadmap consists mostly of big long-term features, implementing them might be not easy for a beginner outside collaborator. - -A good starting point is checking the [CONTRIBUTING.md](https://github.com/provectus/kafka-ui/blob/master/CONTRIBUTING.md) document. \ No newline at end of file diff --git a/documentation/project/contributing/README.md b/documentation/project/contributing/README.md deleted file mode 100644 index f30100ecf61..00000000000 --- a/documentation/project/contributing/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Contributing guidelines - -### Set up the local environment for development - -* [Prerequisites](software-required.md) - -* [Building the app](building.md) -* [Running the app](running.md) -* [Writing tests](testing.md) \ No newline at end of file diff --git a/documentation/project/contributing/building.md b/documentation/project/contributing/building.md deleted file mode 100644 index 21562426e5e..00000000000 --- a/documentation/project/contributing/building.md +++ /dev/null @@ -1,24 +0,0 @@ -### Building the application locally - -Once you installed the prerequisites and cloned the repository, run the following commands in your project directory: - -Build a docker container with the app: -```sh -./mvnw clean install -Pprod -``` -Start the app with Kafka clusters: -```sh -docker-compose -f ./documentation/compose/kafka-ui.yaml up -d -``` -To see the app, navigate to http://localhost:8080. - -If you want to start only kafka clusters (to run the app via `spring-boot:run`): -```sh -docker-compose -f ./documentation/compose/kafka-clusters-only.yaml up -d -``` - -Then, start the app. - -## Where to go next - -In the next section, you'll [learn how to run the application](running.md). \ No newline at end of file diff --git a/documentation/project/contributing/running.md b/documentation/project/contributing/running.md deleted file mode 100644 index a74f198f0c1..00000000000 --- a/documentation/project/contributing/running.md +++ /dev/null @@ -1,25 +0,0 @@ -# Running the app - -### Running locally via docker -If you have built a container locally or wish to run a public one you could bring everything up like this: -```shell -docker-compose -f documentation/compose/kafka-ui.yaml up -d -``` - -### Running locally without docker -Once you built the app, run the following in `kafka-ui-api/`: - -```sh -./mvnw spring-boot:run -Pprod - -# or - -./mvnw spring-boot:run -Pprod -Dspring.config.location=file:///path/to/conf.yaml -``` - -### Running in kubernetes -``` bash -helm repo add kafka-ui https://provectus.github.io/kafka-ui -helm install kafka-ui kafka-ui/kafka-ui -``` -To read more please follow to [chart documentation](charts/kafka-ui/README.md) \ No newline at end of file diff --git a/documentation/project/contributing/set-up-git.md b/documentation/project/contributing/set-up-git.md deleted file mode 100644 index 2400b8b509d..00000000000 --- a/documentation/project/contributing/set-up-git.md +++ /dev/null @@ -1,8 +0,0 @@ -### Nothing special here yet. - \ No newline at end of file diff --git a/documentation/project/contributing/software-required.md b/documentation/project/contributing/software-required.md deleted file mode 100644 index 8d3b86c7311..00000000000 --- a/documentation/project/contributing/software-required.md +++ /dev/null @@ -1,31 +0,0 @@ -### Get the required software for Linux or macOS - -This page explains how to get the software you need to use a Linux or macOS -machine for local development. Before you begin contributing you must have: - -* a GitHub account -* Java 13 or newer -* `git` -* `docker` - -### Installing prerequisites on macOS -1. Install [brew](https://brew.sh/). - -2. Install brew cask: -```sh -> brew cask -``` -3Install JDK 13 via Homebrew cask: -```sh -> brew tap adoptopenjdk/openjdk -> brew install adoptopenjdk13 -``` - -## Tips - -Consider allocating not less than 4GB of memory for your docker. -Otherwise, some apps within a stack (e.g. `kafka-ui.yaml`) might crash. - -## Where to go next - -In the next section, you'll [learn how to build the application](building.md). diff --git a/documentation/project/contributing/testing.md b/documentation/project/contributing/testing.md deleted file mode 100644 index 98c90f8a54d..00000000000 --- a/documentation/project/contributing/testing.md +++ /dev/null @@ -1,28 +0,0 @@ -# Testing - - - -## Test suites - - -## Writing new tests - - -### Writing tests for new features - - -### Writing tests for bug fixes - - -### Writing new integration tests - - - -## Running tests - -### Unit Tests - - -### Integration Tests - - diff --git a/etc/checkstyle/checkstyle-e2e.xml b/etc/checkstyle/checkstyle-e2e.xml new file mode 100644 index 00000000000..c2af9c987b3 --- /dev/null +++ b/etc/checkstyle/checkstyle-e2e.xml @@ -0,0 +1,333 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/etc/checkstyle/checkstyle.xml b/etc/checkstyle/checkstyle.xml index c47505c74b0..745f1bc368d 100644 --- a/etc/checkstyle/checkstyle.xml +++ b/etc/checkstyle/checkstyle.xml @@ -297,7 +297,7 @@ value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/> - + @@ -318,7 +318,7 @@ - + @@ -330,4 +330,4 @@ - \ No newline at end of file + diff --git a/kafka-ui-api/.mvn/wrapper/MavenWrapperDownloader.java b/kafka-ui-api/.mvn/wrapper/MavenWrapperDownloader.java deleted file mode 100644 index e76d1f3241d..00000000000 --- a/kafka-ui-api/.mvn/wrapper/MavenWrapperDownloader.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2007-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import java.net.*; -import java.io.*; -import java.nio.channels.*; -import java.util.Properties; - -public class MavenWrapperDownloader { - - private static final String WRAPPER_VERSION = "0.5.6"; - /** - * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. - */ - private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" - + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; - - /** - * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to - * use instead of the default one. - */ - private static final String MAVEN_WRAPPER_PROPERTIES_PATH = - ".mvn/wrapper/maven-wrapper.properties"; - - /** - * Path where the maven-wrapper.jar will be saved to. - */ - private static final String MAVEN_WRAPPER_JAR_PATH = - ".mvn/wrapper/maven-wrapper.jar"; - - /** - * Name of the property which should be used to override the default download url for the wrapper. - */ - private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; - - public static void main(String args[]) { - System.out.println("- Downloader started"); - File baseDirectory = new File(args[0]); - System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); - - // If the maven-wrapper.properties exists, read it and check if it contains a custom - // wrapperUrl parameter. - File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); - String url = DEFAULT_DOWNLOAD_URL; - if(mavenWrapperPropertyFile.exists()) { - FileInputStream mavenWrapperPropertyFileInputStream = null; - try { - mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); - Properties mavenWrapperProperties = new Properties(); - mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); - url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); - } catch (IOException e) { - System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); - } finally { - try { - if(mavenWrapperPropertyFileInputStream != null) { - mavenWrapperPropertyFileInputStream.close(); - } - } catch (IOException e) { - // Ignore ... - } - } - } - System.out.println("- Downloading from: " + url); - - File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); - if(!outputFile.getParentFile().exists()) { - if(!outputFile.getParentFile().mkdirs()) { - System.out.println( - "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); - } - } - System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); - try { - downloadFileFromURL(url, outputFile); - System.out.println("Done"); - System.exit(0); - } catch (Throwable e) { - System.out.println("- Error downloading"); - e.printStackTrace(); - System.exit(1); - } - } - - private static void downloadFileFromURL(String urlString, File destination) throws Exception { - if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { - String username = System.getenv("MVNW_USERNAME"); - char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); - Authenticator.setDefault(new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(username, password); - } - }); - } - URL website = new URL(urlString); - ReadableByteChannel rbc; - rbc = Channels.newChannel(website.openStream()); - FileOutputStream fos = new FileOutputStream(destination); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - fos.close(); - rbc.close(); - } - -} diff --git a/kafka-ui-api/.mvn/wrapper/maven-wrapper.jar b/kafka-ui-api/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index 2cc7d4a55c0..00000000000 Binary files a/kafka-ui-api/.mvn/wrapper/maven-wrapper.jar and /dev/null differ diff --git a/kafka-ui-api/.mvn/wrapper/maven-wrapper.properties b/kafka-ui-api/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 642d572ce90..00000000000 --- a/kafka-ui-api/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,2 +0,0 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/kafka-ui-api/Dockerfile b/kafka-ui-api/Dockerfile index 5488a771810..98dcdb46ac3 100644 --- a/kafka-ui-api/Dockerfile +++ b/kafka-ui-api/Dockerfile @@ -1,7 +1,16 @@ -FROM alpine:3.15.0 +#FROM azul/zulu-openjdk-alpine:17-jre-headless +FROM azul/zulu-openjdk-alpine@sha256:a36679ac0d28cb835e2a8c00e1e0d95509c6c51c5081c7782b85edb1f37a771a -RUN apk add --no-cache openjdk13-jre libc6-compat gcompat \ -&& addgroup -S kafkaui && adduser -S kafkaui -G kafkaui +RUN apk add --no-cache \ + # snappy codec + gcompat \ + # configuring timezones + tzdata +RUN addgroup -S kafkaui && adduser -S kafkaui -G kafkaui + +# creating folder for dynamic config usage (certificates uploads, etc) +RUN mkdir /etc/kafkaui/ +RUN chown kafkaui /etc/kafkaui USER kafkaui @@ -12,4 +21,5 @@ ENV JAVA_OPTS= EXPOSE 8080 -CMD java $JAVA_OPTS -jar kafka-ui-api.jar +# see JmxSslSocketFactory docs to understand why add-opens is needed +CMD java --add-opens java.rmi/javax.rmi.ssl=ALL-UNNAMED $JAVA_OPTS -jar kafka-ui-api.jar diff --git a/kafka-ui-api/mvnw b/kafka-ui-api/mvnw deleted file mode 100755 index a16b5431b4c..00000000000 --- a/kafka-ui-api/mvnw +++ /dev/null @@ -1,310 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi - - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/kafka-ui-api/mvnw.cmd b/kafka-ui-api/mvnw.cmd deleted file mode 100644 index c8d43372c98..00000000000 --- a/kafka-ui-api/mvnw.cmd +++ /dev/null @@ -1,182 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM https://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - -FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% diff --git a/kafka-ui-api/pom.xml b/kafka-ui-api/pom.xml index 9665aad565f..d572a4ffa34 100644 --- a/kafka-ui-api/pom.xml +++ b/kafka-ui-api/pom.xml @@ -12,7 +12,7 @@ kafka-ui-api - 0.8.8 + 0.8.10 jacoco reuseReports ${project.basedir}/target/jacoco.exec @@ -20,18 +20,6 @@ java - - - - org.springframework.boot - spring-boot-dependencies - ${spring-boot.version} - pom - import - - - - org.springframework.boot @@ -54,6 +42,11 @@ kafka-ui-contract ${project.version} + + com.provectus + kafka-ui-serde-api + ${kafka-ui-serde-api.version} + org.apache.kafka kafka-clients @@ -62,7 +55,7 @@ org.apache.commons commons-lang3 - 3.9 + 3.12.0 org.projectlombok @@ -88,6 +81,12 @@ io.confluent kafka-json-schema-serializer ${confluent.version} + + + commons-collections + commons-collections + + io.confluent @@ -98,7 +97,7 @@ software.amazon.msk aws-msk-iam-auth - 1.1.3 + 1.1.7 @@ -116,6 +115,16 @@ io.projectreactor.addons reactor-extra + + org.json + json + ${org.json.version} + + + io.micrometer + micrometer-registry-prometheus + runtime + org.springframework.boot @@ -132,28 +141,29 @@ commons-pool2 ${apache.commons.version} + + org.apache.commons + commons-collections4 + 4.4 + org.testcontainers testcontainers - ${test.containers.version} test org.testcontainers kafka - ${test.containers.version} test org.testcontainers junit-jupiter - ${test.containers.version} test org.junit.jupiter junit-jupiter-engine - ${junit-jupiter-engine.version} test @@ -168,6 +178,12 @@ ${mockito.version} test + + net.bytebuddy + byte-buddy + ${byte-buddy.version} + test + org.assertj assertj-core @@ -180,6 +196,18 @@ 2.2.14 test + + com.squareup.okhttp3 + mockwebserver + ${okhttp3.mockwebserver.version} + test + + + com.squareup.okhttp3 + okhttp + ${okhttp3.mockwebserver.version} + test + org.springframework.boot @@ -193,15 +221,34 @@ - org.springframework.boot - spring-boot-starter-data-ldap + org.opendatadiscovery + oddrn-generator-java + ${odd-oddrn-generator.version} + + + org.opendatadiscovery + ingestion-contract-client + + + org.springframework.boot + spring-boot-starter-webflux + + + io.projectreactor + reactor-core + + + io.projectreactor.ipc + reactor-netty + + + ${odd-oddrn-client.version} + org.springframework.security spring-security-ldap - - org.codehaus.groovy groovy-jsr223 @@ -212,6 +259,16 @@ groovy-json ${groovy.version} + + org.apache.datasketches + datasketches-java + ${datasketches-java.version} + + + org.springframework.boot + spring-boot-devtools + true + @@ -225,6 +282,7 @@ repackage + build-info @@ -232,10 +290,7 @@ org.apache.maven.plugins maven-compiler-plugin - ${maven-compiler-plugin.version} - ${maven.compiler.source} - ${maven.compiler.target} org.mapstruct @@ -263,7 +318,6 @@ org.apache.maven.plugins maven-surefire-plugin - ${maven-surefire-plugin.version} @{argLine} --illegal-access=permit @@ -271,12 +325,12 @@ org.apache.maven.plugins maven-checkstyle-plugin - 3.1.1 + 3.3.0 com.puppycrawl.tools checkstyle - 8.32 + 10.3.1 @@ -296,6 +350,7 @@ + org.antlr @@ -348,7 +403,7 @@ pl.project13.maven git-commit-id-plugin - 4.0.0 + 4.9.10 get-the-git-infos @@ -370,7 +425,6 @@ maven-resources-plugin - ${maven-resources-plugin.version} copy-resources @@ -394,48 +448,61 @@ frontend-maven-plugin ${frontend-maven-plugin.version} + ${skipUIBuild} ../kafka-ui-react-app - ${project.version} - ${git.commit.id.abbrev} + ${project.version} + ${git.commit.id.abbrev} - install node and npm + install node and pnpm - install-node-and-npm + install-node-and-pnpm ${node.version} + ${pnpm.version} - npm install + pnpm install - npm + pnpm install - npm run build + pnpm build - npm + pnpm - run build + build - com.spotify - dockerfile-maven-plugin - ${dockerfile-maven-plugin.version} + io.fabric8 + docker-maven-plugin + ${fabric8-maven-plugin.version} - true + true + + + provectuslabs/kafka-ui:${git.revision} + + ${project.basedir} + + ${project.build.finalName}.jar + + + + @@ -444,14 +511,6 @@ build - - ${git.revision} - provectuslabs/kafka-ui - - ${project.build.finalName}.jar - ${project.artifactId}.jar - - @@ -460,5 +519,4 @@ - diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/KafkaUiApplication.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/KafkaUiApplication.java index ded03514fee..8d0eafeff39 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/KafkaUiApplication.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/KafkaUiApplication.java @@ -1,16 +1,26 @@ package com.provectus.kafka.ui; -import org.springframework.boot.SpringApplication; +import com.provectus.kafka.ui.util.DynamicConfigOperations; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; -@SpringBootApplication +@SpringBootApplication(exclude = LdapAutoConfiguration.class) @EnableScheduling @EnableAsync public class KafkaUiApplication { public static void main(String[] args) { - SpringApplication.run(KafkaUiApplication.class, args); + startApplication(args); + } + + public static ConfigurableApplicationContext startApplication(String[] args) { + return new SpringApplicationBuilder(KafkaUiApplication.class) + .initializers(DynamicConfigOperations.dynamicConfigPropertiesInitializer()) + .build() + .run(args); } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/client/KafkaConnectClients.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/client/KafkaConnectClients.java deleted file mode 100644 index de0c9054ae2..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/client/KafkaConnectClients.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.provectus.kafka.ui.client; - -import com.provectus.kafka.ui.connect.api.KafkaConnectClientApi; -import com.provectus.kafka.ui.model.KafkaConnectCluster; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -public final class KafkaConnectClients { - - private KafkaConnectClients() { - - } - - private static final Map CACHE = new ConcurrentHashMap<>(); - - public static KafkaConnectClientApi withKafkaConnectConfig(KafkaConnectCluster config) { - return CACHE.computeIfAbsent(config.getAddress(), s -> new RetryingKafkaConnectClient(config)); - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/client/KsqlClient.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/client/KsqlClient.java deleted file mode 100644 index 2e8026648d7..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/client/KsqlClient.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.provectus.kafka.ui.client; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.provectus.kafka.ui.exception.UnprocessableEntityException; -import com.provectus.kafka.ui.model.KsqlCommandResponseDTO; -import com.provectus.kafka.ui.strategy.ksql.statement.BaseStrategy; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.BodyInserters; -import org.springframework.web.reactive.function.client.ClientResponse; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; - -@Service -@RequiredArgsConstructor -@Slf4j -public class KsqlClient { - private final WebClient webClient; - private final ObjectMapper mapper; - - public Mono execute(BaseStrategy ksqlStatement) { - return webClient.post() - .uri(ksqlStatement.getUri()) - .accept(new MediaType("application", "vnd.ksql.v1+json")) - .body(BodyInserters.fromValue(ksqlStatement.getKsqlCommand())) - .retrieve() - .onStatus(HttpStatus::isError, this::getErrorMessage) - .bodyToMono(byte[].class) - .map(this::toJson) - .map(ksqlStatement::serializeResponse); - } - - private Mono getErrorMessage(ClientResponse response) { - return response - .bodyToMono(byte[].class) - .map(this::toJson) - .map(jsonNode -> jsonNode.get("message").asText()) - .flatMap(error -> Mono.error(new UnprocessableEntityException(error))); - } - - @SneakyThrows - private JsonNode toJson(byte[] content) { - return this.mapper.readTree(content); - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/client/RetryingKafkaConnectClient.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/client/RetryingKafkaConnectClient.java index 70716613730..74b9485008e 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/client/RetryingKafkaConnectClient.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/client/RetryingKafkaConnectClient.java @@ -1,22 +1,30 @@ package com.provectus.kafka.ui.client; +import static com.provectus.kafka.ui.config.ClustersProperties.ConnectCluster; + +import com.provectus.kafka.ui.config.ClustersProperties; import com.provectus.kafka.ui.connect.ApiClient; import com.provectus.kafka.ui.connect.api.KafkaConnectClientApi; import com.provectus.kafka.ui.connect.model.Connector; +import com.provectus.kafka.ui.connect.model.ConnectorPlugin; +import com.provectus.kafka.ui.connect.model.ConnectorPluginConfigValidationResponse; +import com.provectus.kafka.ui.connect.model.ConnectorStatus; +import com.provectus.kafka.ui.connect.model.ConnectorTask; +import com.provectus.kafka.ui.connect.model.ConnectorTopics; import com.provectus.kafka.ui.connect.model.NewConnector; +import com.provectus.kafka.ui.connect.model.TaskStatus; import com.provectus.kafka.ui.exception.KafkaConnectConflictReponseException; import com.provectus.kafka.ui.exception.ValidationException; -import com.provectus.kafka.ui.model.KafkaConnectCluster; +import com.provectus.kafka.ui.util.WebClientConfigurator; import java.time.Duration; import java.util.List; import java.util.Map; +import javax.annotation.Nullable; import lombok.extern.slf4j.Slf4j; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.util.MultiValueMap; +import org.springframework.http.ResponseEntity; +import org.springframework.util.unit.DataSize; import org.springframework.web.client.RestClientException; +import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,8 +35,10 @@ public class RetryingKafkaConnectClient extends KafkaConnectClientApi { private static final int MAX_RETRIES = 5; private static final Duration RETRIES_DELAY = Duration.ofMillis(200); - public RetryingKafkaConnectClient(KafkaConnectCluster config) { - super(new RetryingApiClient(config)); + public RetryingKafkaConnectClient(ConnectCluster config, + @Nullable ClustersProperties.TruststoreConfig truststoreConfig, + DataSize maxBuffSize) { + super(new RetryingApiClient(config, truststoreConfig, maxBuffSize)); } private static Retry conflictCodeRetry() { @@ -71,43 +81,204 @@ public Mono setConnectorConfig(String connectorName, Map> createConnectorWithHttpInfo(NewConnector newConnector) + throws WebClientResponseException { + return withRetryOnConflict(super.createConnectorWithHttpInfo(newConnector)); + } + + @Override + public Mono deleteConnector(String connectorName) throws WebClientResponseException { + return withRetryOnConflict(super.deleteConnector(connectorName)); + } + + @Override + public Mono> deleteConnectorWithHttpInfo(String connectorName) + throws WebClientResponseException { + return withRetryOnConflict(super.deleteConnectorWithHttpInfo(connectorName)); + } + + + @Override + public Mono getConnector(String connectorName) throws WebClientResponseException { + return withRetryOnConflict(super.getConnector(connectorName)); + } + + @Override + public Mono> getConnectorWithHttpInfo(String connectorName) + throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorWithHttpInfo(connectorName)); + } + + @Override + public Mono> getConnectorConfig(String connectorName) throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorConfig(connectorName)); + } + + @Override + public Mono>> getConnectorConfigWithHttpInfo(String connectorName) + throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorConfigWithHttpInfo(connectorName)); + } + + @Override + public Flux getConnectorPlugins() throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorPlugins()); + } + + @Override + public Mono>> getConnectorPluginsWithHttpInfo() + throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorPluginsWithHttpInfo()); + } + + @Override + public Mono getConnectorStatus(String connectorName) throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorStatus(connectorName)); + } + + @Override + public Mono> getConnectorStatusWithHttpInfo(String connectorName) + throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorStatusWithHttpInfo(connectorName)); + } + + @Override + public Mono getConnectorTaskStatus(String connectorName, Integer taskId) + throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorTaskStatus(connectorName, taskId)); + } + + @Override + public Mono> getConnectorTaskStatusWithHttpInfo(String connectorName, Integer taskId) + throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorTaskStatusWithHttpInfo(connectorName, taskId)); + } + + @Override + public Flux getConnectorTasks(String connectorName) throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorTasks(connectorName)); + } + + @Override + public Mono>> getConnectorTasksWithHttpInfo(String connectorName) + throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorTasksWithHttpInfo(connectorName)); + } + + @Override + public Mono> getConnectorTopics(String connectorName) throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorTopics(connectorName)); + } + + @Override + public Mono>> getConnectorTopicsWithHttpInfo(String connectorName) + throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorTopicsWithHttpInfo(connectorName)); + } + + @Override + public Flux getConnectors(String search) throws WebClientResponseException { + return withRetryOnConflict(super.getConnectors(search)); + } + + @Override + public Mono>> getConnectorsWithHttpInfo(String search) throws WebClientResponseException { + return withRetryOnConflict(super.getConnectorsWithHttpInfo(search)); + } + + @Override + public Mono pauseConnector(String connectorName) throws WebClientResponseException { + return withRetryOnConflict(super.pauseConnector(connectorName)); + } + + @Override + public Mono> pauseConnectorWithHttpInfo(String connectorName) throws WebClientResponseException { + return withRetryOnConflict(super.pauseConnectorWithHttpInfo(connectorName)); + } + + @Override + public Mono restartConnector(String connectorName, Boolean includeTasks, Boolean onlyFailed) + throws WebClientResponseException { + return withRetryOnConflict(super.restartConnector(connectorName, includeTasks, onlyFailed)); + } + + @Override + public Mono> restartConnectorWithHttpInfo(String connectorName, Boolean includeTasks, + Boolean onlyFailed) throws WebClientResponseException { + return withRetryOnConflict(super.restartConnectorWithHttpInfo(connectorName, includeTasks, onlyFailed)); + } + + @Override + public Mono restartConnectorTask(String connectorName, Integer taskId) throws WebClientResponseException { + return withRetryOnConflict(super.restartConnectorTask(connectorName, taskId)); + } + + @Override + public Mono> restartConnectorTaskWithHttpInfo(String connectorName, Integer taskId) + throws WebClientResponseException { + return withRetryOnConflict(super.restartConnectorTaskWithHttpInfo(connectorName, taskId)); + } + + @Override + public Mono resumeConnector(String connectorName) throws WebClientResponseException { + return super.resumeConnector(connectorName); + } + + @Override + public Mono> resumeConnectorWithHttpInfo(String connectorName) + throws WebClientResponseException { + return withRetryOnConflict(super.resumeConnectorWithHttpInfo(connectorName)); + } + + @Override + public Mono> setConnectorConfigWithHttpInfo(String connectorName, + Map requestBody) + throws WebClientResponseException { + return withRetryOnConflict(super.setConnectorConfigWithHttpInfo(connectorName, requestBody)); + } + + @Override + public Mono validateConnectorPluginConfig(String pluginName, + Map requestBody) + throws WebClientResponseException { + return withRetryOnConflict(super.validateConnectorPluginConfig(pluginName, requestBody)); + } + + @Override + public Mono> validateConnectorPluginConfigWithHttpInfo( + String pluginName, Map requestBody) throws WebClientResponseException { + return withRetryOnConflict(super.validateConnectorPluginConfigWithHttpInfo(pluginName, requestBody)); + } + private static class RetryingApiClient extends ApiClient { - public RetryingApiClient(KafkaConnectCluster config) { - super(); + public RetryingApiClient(ConnectCluster config, + ClustersProperties.TruststoreConfig truststoreConfig, + DataSize maxBuffSize) { + super(buildWebClient(maxBuffSize, config, truststoreConfig), null, null); setBasePath(config.getAddress()); - setUsername(config.getUserName()); + setUsername(config.getUsername()); setPassword(config.getPassword()); } - @Override - public Mono invokeAPI(String path, HttpMethod method, Map pathParams, - MultiValueMap queryParams, Object body, - HttpHeaders headerParams, - MultiValueMap cookieParams, - MultiValueMap formParams, List accept, - MediaType contentType, String[] authNames, - ParameterizedTypeReference returnType) - throws RestClientException { - return withRetryOnConflict( - super.invokeAPI(path, method, pathParams, queryParams, body, headerParams, cookieParams, - formParams, accept, contentType, authNames, returnType) - ); - } - - @Override - public Flux invokeFluxAPI(String path, HttpMethod method, Map pathParams, - MultiValueMap queryParams, Object body, - HttpHeaders headerParams, - MultiValueMap cookieParams, - MultiValueMap formParams, - List accept, MediaType contentType, - String[] authNames, ParameterizedTypeReference returnType) - throws RestClientException { - return withRetryOnConflict( - super.invokeFluxAPI(path, method, pathParams, queryParams, body, headerParams, - cookieParams, formParams, accept, contentType, authNames, returnType) - ); + public static WebClient buildWebClient(DataSize maxBuffSize, + ConnectCluster config, + ClustersProperties.TruststoreConfig truststoreConfig) { + return new WebClientConfigurator() + .configureSsl( + truststoreConfig, + new ClustersProperties.KeystoreConfig( + config.getKeystoreLocation(), + config.getKeystorePassword() + ) + ) + .configureBasicAuth( + config.getUsername(), + config.getPassword() + ) + .configureBufferSize(maxBuffSize) + .build(); } } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ClustersProperties.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ClustersProperties.java index 0d83e143c5d..e0b20d6c93f 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ClustersProperties.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ClustersProperties.java @@ -1,13 +1,19 @@ package com.provectus.kafka.ui.config; +import com.provectus.kafka.ui.model.MetricsConfig; +import jakarta.annotation.PostConstruct; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Properties; import java.util.Set; -import javax.annotation.PostConstruct; +import javax.annotation.Nullable; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.util.StringUtils; @@ -19,47 +25,177 @@ public class ClustersProperties { List clusters = new ArrayList<>(); + String internalTopicPrefix; + + Integer adminClientTimeout; + + PollingProperties polling = new PollingProperties(); + @Data public static class Cluster { String name; String bootstrapServers; String schemaRegistry; SchemaRegistryAuth schemaRegistryAuth; + KeystoreConfig schemaRegistrySsl; String ksqldbServer; - String schemaNameTemplate = "%s-value"; - String keySchemaNameTemplate = "%s-key"; - String protobufFile; - String protobufMessageName; - Map protobufMessageNameByTopic; - String protobufMessageNameForKey; - Map protobufMessageNameForKeyByTopic; + KsqldbServerAuth ksqldbServerAuth; + KeystoreConfig ksqldbServerSsl; List kafkaConnect; - int jmxPort; - boolean jmxSsl; - String jmxUsername; - String jmxPassword; - Properties properties; + MetricsConfigData metrics; + Map properties; boolean readOnly = false; - boolean disableLogDirsCollection = false; + List serde; + String defaultKeySerde; + String defaultValueSerde; + List masking; + Long pollingThrottleRate; + TruststoreConfig ssl; + AuditProperties audit; + } + + @Data + public static class PollingProperties { + Integer pollTimeoutMs; + Integer maxPageSize; + Integer defaultPageSize; } @Data + @ToString(exclude = "password") + public static class MetricsConfigData { + String type; + Integer port; + Boolean ssl; + String username; + String password; + String keystoreLocation; + String keystorePassword; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder(toBuilder = true) + @ToString(exclude = {"password", "keystorePassword"}) public static class ConnectCluster { String name; String address; - String userName; + String username; String password; + String keystoreLocation; + String keystorePassword; } @Data + @ToString(exclude = {"password"}) public static class SchemaRegistryAuth { String username; String password; } + @Data + @ToString(exclude = {"truststorePassword"}) + public static class TruststoreConfig { + String truststoreLocation; + String truststorePassword; + } + + @Data + public static class SerdeConfig { + String name; + String className; + String filePath; + Map properties; + String topicKeysPattern; + String topicValuesPattern; + } + + @Data + @ToString(exclude = "password") + public static class KsqldbServerAuth { + String username; + String password; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @ToString(exclude = {"keystorePassword"}) + public static class KeystoreConfig { + String keystoreLocation; + String keystorePassword; + } + + @Data + public static class Masking { + Type type; + List fields; + String fieldsNamePattern; + List maskingCharsReplacement; //used when type=MASK + String replacement; //used when type=REPLACE + String topicKeysPattern; + String topicValuesPattern; + + public enum Type { + REMOVE, MASK, REPLACE + } + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class AuditProperties { + String topic; + Integer auditTopicsPartitions; + Boolean topicAuditEnabled; + Boolean consoleAuditEnabled; + LogLevel level; + Map auditTopicProperties; + + public enum LogLevel { + ALL, + ALTER_ONLY //default + } + } + @PostConstruct public void validateAndSetDefaults() { - validateClusterNames(); + if (clusters != null) { + validateClusterNames(); + flattenClusterProperties(); + setMetricsDefaults(); + } + } + + private void setMetricsDefaults() { + for (Cluster cluster : clusters) { + if (cluster.getMetrics() != null && !StringUtils.hasText(cluster.getMetrics().getType())) { + cluster.getMetrics().setType(MetricsConfig.JMX_METRICS_TYPE); + } + } + } + + private void flattenClusterProperties() { + for (Cluster cluster : clusters) { + cluster.setProperties(flattenClusterProperties(null, cluster.getProperties())); + } + } + + private Map flattenClusterProperties(@Nullable String prefix, + @Nullable Map propertiesMap) { + Map flattened = new HashMap<>(); + if (propertiesMap != null) { + propertiesMap.forEach((k, v) -> { + String key = prefix == null ? k : prefix + "." + k; + if (v instanceof Map) { + flattened.putAll(flattenClusterProperties(key, (Map) v)); + } else { + flattened.put(key, v); + } + }); + } + return flattened; } private void validateClusterNames() { diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/Config.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/Config.java index 92174d2a478..2ad0538c0ec 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/Config.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/Config.java @@ -1,17 +1,10 @@ package com.provectus.kafka.ui.config; -import com.provectus.kafka.ui.model.JmxConnectionInfo; -import com.provectus.kafka.ui.util.JmxPoolFactory; import java.util.Collections; import java.util.Map; -import javax.management.remote.JMXConnector; import lombok.AllArgsConstructor; -import org.apache.commons.pool2.KeyedObjectPool; -import org.apache.commons.pool2.impl.GenericKeyedObjectPool; -import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig; import org.openapitools.jackson.nullable.JsonNullableModule; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties; import org.springframework.context.ApplicationContext; @@ -21,8 +14,6 @@ import org.springframework.http.server.reactive.HttpHandler; import org.springframework.jmx.export.MBeanExporter; import org.springframework.util.StringUtils; -import org.springframework.util.unit.DataSize; -import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; @Configuration @@ -49,21 +40,6 @@ public HttpHandler httpHandler(ObjectProvider propsProvider) return httpHandler; } - - @Bean - public KeyedObjectPool pool() { - var pool = new GenericKeyedObjectPool<>(new JmxPoolFactory()); - pool.setConfig(poolConfig()); - return pool; - } - - private GenericKeyedObjectPoolConfig poolConfig() { - final var poolConfig = new GenericKeyedObjectPoolConfig(); - poolConfig.setMaxIdlePerKey(3); - poolConfig.setMaxTotalPerKey(3); - return poolConfig; - } - @Bean public MBeanExporter exporter() { final var exporter = new MBeanExporter(); @@ -73,14 +49,7 @@ public MBeanExporter exporter() { } @Bean - public WebClient webClient( - @Value("${webclient.max-in-memory-buffer-size:20MB}") DataSize maxBuffSize) { - return WebClient.builder() - .codecs(c -> c.defaultCodecs().maxInMemorySize((int) maxBuffSize.toBytes())) - .build(); - } - - @Bean + // will be used by webflux json mapping public JsonNullableModule jsonNullableModule() { return new JsonNullableModule(); } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CorsGlobalConfiguration.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CorsGlobalConfiguration.java index 0128110ab72..a0f892492e1 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CorsGlobalConfiguration.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CorsGlobalConfiguration.java @@ -1,58 +1,39 @@ package com.provectus.kafka.ui.config; -import lombok.AllArgsConstructor; -import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; -import org.springframework.core.io.ClassPathResource; -import org.springframework.util.StringUtils; -import org.springframework.web.reactive.config.CorsRegistry; -import org.springframework.web.reactive.config.WebFluxConfigurer; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.RouterFunctions; -import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; @Configuration -@Profile("local") -@AllArgsConstructor -public class CorsGlobalConfiguration implements WebFluxConfigurer { - - private final ServerProperties serverProperties; - - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**") - .allowedOrigins("*") - .allowedMethods("*") - .allowedHeaders("*") - .allowCredentials(false); - } - - private String withContext(String pattern) { - final String basePath = serverProperties.getServlet().getContextPath(); - if (StringUtils.hasText(basePath)) { - return basePath + pattern; - } else { - return pattern; - } - } +public class CorsGlobalConfiguration { @Bean - public RouterFunction cssFilesRouter() { - return RouterFunctions - .resources(withContext("/static/css/**"), new ClassPathResource("static/static/css/")); + public WebFilter corsFilter() { + return (final ServerWebExchange ctx, final WebFilterChain chain) -> { + final ServerHttpRequest request = ctx.getRequest(); + + final ServerHttpResponse response = ctx.getResponse(); + final HttpHeaders headers = response.getHeaders(); + headers.add("Access-Control-Allow-Origin", "*"); + headers.add("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS"); + headers.add("Access-Control-Max-Age", "3600"); + headers.add("Access-Control-Allow-Headers", "Content-Type"); + + if (request.getMethod() == HttpMethod.OPTIONS) { + response.setStatusCode(HttpStatus.OK); + return Mono.empty(); + } + + return chain.filter(ctx); + }; } - @Bean - public RouterFunction jsFilesRouter() { - return RouterFunctions - .resources(withContext("/static/js/**"), new ClassPathResource("static/static/js/")); - } - - @Bean - public RouterFunction mediaFilesRouter() { - return RouterFunctions - .resources(withContext("/static/media/**"), new ClassPathResource("static/static/media/")); - } -} \ No newline at end of file +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/WebclientProperties.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/WebclientProperties.java new file mode 100644 index 00000000000..24d8bf01db1 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/WebclientProperties.java @@ -0,0 +1,32 @@ +package com.provectus.kafka.ui.config; + +import com.provectus.kafka.ui.exception.ValidationException; +import javax.annotation.PostConstruct; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.unit.DataSize; + +@Configuration +@ConfigurationProperties("webclient") +@Data +public class WebclientProperties { + + String maxInMemoryBufferSize; + + @PostConstruct + public void validate() { + validateAndSetDefaultBufferSize(); + } + + private void validateAndSetDefaultBufferSize() { + if (maxInMemoryBufferSize != null) { + try { + DataSize.parse(maxInMemoryBufferSize); + } catch (Exception e) { + throw new ValidationException("Invalid format for webclient.maxInMemoryBufferSize"); + } + } + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/AbstractAuthSecurityConfig.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/AbstractAuthSecurityConfig.java index 7fc9f827802..0c70b79716e 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/AbstractAuthSecurityConfig.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/AbstractAuthSecurityConfig.java @@ -13,6 +13,7 @@ protected AbstractAuthSecurityConfig() { "/resources/**", "/actuator/health/**", "/actuator/info", + "/actuator/prometheus", "/auth", "/login", "/logout", diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/AuthenticatedUser.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/AuthenticatedUser.java new file mode 100644 index 00000000000..9ff33cad3a2 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/AuthenticatedUser.java @@ -0,0 +1,7 @@ +package com.provectus.kafka.ui.config.auth; + +import java.util.Collection; + +public record AuthenticatedUser(String principal, Collection groups) { + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/BasicAuthSecurityConfig.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/BasicAuthSecurityConfig.java index 4ee3e53b5b5..c62e83e665e 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/BasicAuthSecurityConfig.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/BasicAuthSecurityConfig.java @@ -1,40 +1,51 @@ package com.provectus.kafka.ui.config.auth; import com.provectus.kafka.ui.util.EmptyRedirectStrategy; -import lombok.extern.log4j.Log4j2; +import java.net.URI; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler; +import org.springframework.security.web.server.authentication.logout.RedirectServerLogoutSuccessHandler; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; @Configuration @EnableWebFluxSecurity @ConditionalOnProperty(value = "auth.type", havingValue = "LOGIN_FORM") -@Log4j2 +@Slf4j public class BasicAuthSecurityConfig extends AbstractAuthSecurityConfig { + public static final String LOGIN_URL = "/auth"; + public static final String LOGOUT_URL = "/auth?logout"; + @Bean public SecurityWebFilterChain configure(ServerHttpSecurity http) { log.info("Configuring LOGIN_FORM authentication."); - http.authorizeExchange() - .pathMatchers(AUTH_WHITELIST) - .permitAll() - .anyExchange() - .authenticated(); - - final RedirectServerAuthenticationSuccessHandler handler = new RedirectServerAuthenticationSuccessHandler(); - handler.setRedirectStrategy(new EmptyRedirectStrategy()); - - http - .httpBasic().and() - .formLogin() - .loginPage("/auth") - .authenticationSuccessHandler(handler); - - return http.csrf().disable().build(); + + final var authHandler = new RedirectServerAuthenticationSuccessHandler(); + authHandler.setRedirectStrategy(new EmptyRedirectStrategy()); + + final var logoutSuccessHandler = new RedirectServerLogoutSuccessHandler(); + logoutSuccessHandler.setLogoutSuccessUrl(URI.create(LOGOUT_URL)); + + + return http.authorizeExchange(spec -> spec + .pathMatchers(AUTH_WHITELIST) + .permitAll() + .anyExchange() + .authenticated() + ) + .formLogin(spec -> spec.loginPage(LOGIN_URL).authenticationSuccessHandler(authHandler)) + .logout(spec -> spec + .logoutSuccessHandler(logoutSuccessHandler) + .requiresLogout(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/logout"))) + .csrf(ServerHttpSecurity.CsrfSpec::disable) + .build(); } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/DisabledAuthSecurityConfig.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/DisabledAuthSecurityConfig.java index d30aa4631bd..39d56a05bf6 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/DisabledAuthSecurityConfig.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/DisabledAuthSecurityConfig.java @@ -1,6 +1,6 @@ package com.provectus.kafka.ui.config.auth; -import lombok.extern.log4j.Log4j2; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.ApplicationContext; @@ -14,7 +14,7 @@ @Configuration @EnableWebFluxSecurity @ConditionalOnProperty(value = "auth.type", havingValue = "DISABLED") -@Log4j2 +@Slf4j public class DisabledAuthSecurityConfig extends AbstractAuthSecurityConfig { @Bean @@ -27,10 +27,12 @@ public SecurityWebFilterChain configure(ServerHttpSecurity http, Environment env System.exit(1); } log.warn("Authentication is disabled. Access will be unrestricted."); - return http.authorizeExchange() - .anyExchange().permitAll() - .and() - .csrf().disable() + + return http.authorizeExchange(spec -> spec + .anyExchange() + .permitAll() + ) + .csrf(ServerHttpSecurity.CsrfSpec::disable) .build(); } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapProperties.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapProperties.java new file mode 100644 index 00000000000..9eac9e5db01 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapProperties.java @@ -0,0 +1,26 @@ +package com.provectus.kafka.ui.config.auth; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("spring.ldap") +@Data +public class LdapProperties { + + private String urls; + private String base; + private String adminUser; + private String adminPassword; + private String userFilterSearchBase; + private String userFilterSearchFilter; + private String groupFilterSearchBase; + private String groupFilterSearchFilter; + private String groupRoleAttribute; + + @Value("${oauth2.ldap.activeDirectory:false}") + private boolean isActiveDirectory; + @Value("${oauth2.ldap.aсtiveDirectory.domain:@null}") + private String activeDirectoryDomain; + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapSecurityConfig.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapSecurityConfig.java index 0d629a88360..20ce2aaa583 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapSecurityConfig.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapSecurityConfig.java @@ -1,87 +1,151 @@ package com.provectus.kafka.ui.config.auth; +import static com.provectus.kafka.ui.config.auth.AbstractAuthSecurityConfig.AUTH_WHITELIST; + +import com.provectus.kafka.ui.service.rbac.AccessControlService; +import com.provectus.kafka.ui.service.rbac.extractor.RbacLdapAuthoritiesExtractor; +import java.util.Collection; import java.util.List; -import lombok.extern.log4j.Log4j2; -import org.springframework.beans.factory.annotation.Value; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; +import org.springframework.ldap.core.DirContextOperations; import org.springframework.ldap.core.support.BaseLdapPathContextSource; import org.springframework.ldap.core.support.LdapContextSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManagerAdapter; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider; import org.springframework.security.ldap.authentication.BindAuthenticator; import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; +import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider; import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; import org.springframework.security.ldap.search.LdapUserSearch; +import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator; +import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; +import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper; import org.springframework.security.web.server.SecurityWebFilterChain; @Configuration @EnableWebFluxSecurity @ConditionalOnProperty(value = "auth.type", havingValue = "LDAP") -@Log4j2 -public class LdapSecurityConfig extends AbstractAuthSecurityConfig { - - @Value("${spring.ldap.urls}") - private String ldapUrls; - @Value("${spring.ldap.dn.pattern:#{null}}") - private String ldapUserDnPattern; - @Value("${spring.ldap.adminUser:#{null}}") - private String adminUser; - @Value("${spring.ldap.adminPassword:#{null}}") - private String adminPassword; - @Value("${spring.ldap.userFilter.searchBase:#{null}}") - private String userFilterSearchBase; - @Value("${spring.ldap.userFilter.searchFilter:#{null}}") - private String userFilterSearchFilter; +@Import(LdapAutoConfiguration.class) +@EnableConfigurationProperties(LdapProperties.class) +@RequiredArgsConstructor +@Slf4j +public class LdapSecurityConfig { + + private final LdapProperties props; @Bean - public ReactiveAuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + public ReactiveAuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource, + LdapAuthoritiesPopulator authoritiesExtractor, + AccessControlService acs) { + var rbacEnabled = acs.isRbacEnabled(); BindAuthenticator ba = new BindAuthenticator(contextSource); - if (ldapUserDnPattern != null) { - ba.setUserDnPatterns(new String[] {ldapUserDnPattern}); + if (props.getBase() != null) { + ba.setUserDnPatterns(new String[] {props.getBase()}); } - if (userFilterSearchFilter != null) { + if (props.getUserFilterSearchFilter() != null) { LdapUserSearch userSearch = - new FilterBasedLdapUserSearch(userFilterSearchBase, userFilterSearchFilter, contextSource); + new FilterBasedLdapUserSearch(props.getUserFilterSearchBase(), props.getUserFilterSearchFilter(), + contextSource); ba.setUserSearch(userSearch); } - LdapAuthenticationProvider lap = new LdapAuthenticationProvider(ba); + AbstractLdapAuthenticationProvider authenticationProvider; + if (!props.isActiveDirectory()) { + authenticationProvider = rbacEnabled + ? new LdapAuthenticationProvider(ba, authoritiesExtractor) + : new LdapAuthenticationProvider(ba); + } else { + authenticationProvider = new ActiveDirectoryLdapAuthenticationProvider(props.getActiveDirectoryDomain(), + props.getUrls()); // TODO Issue #3741 + authenticationProvider.setUseAuthenticationRequestCredentials(true); + } + + if (rbacEnabled) { + authenticationProvider.setUserDetailsContextMapper(new UserDetailsMapper()); + } - AuthenticationManager am = new ProviderManager(List.of(lap)); + AuthenticationManager am = new ProviderManager(List.of(authenticationProvider)); return new ReactiveAuthenticationManagerAdapter(am); } @Bean + @Primary public BaseLdapPathContextSource contextSource() { LdapContextSource ctx = new LdapContextSource(); - ctx.setUrl(ldapUrls); - ctx.setUserDn(adminUser); - ctx.setPassword(adminPassword); + ctx.setUrl(props.getUrls()); + ctx.setUserDn(props.getAdminUser()); + ctx.setPassword(props.getAdminPassword()); ctx.afterPropertiesSet(); return ctx; } + @Bean + @Primary + public DefaultLdapAuthoritiesPopulator ldapAuthoritiesExtractor(ApplicationContext context, + BaseLdapPathContextSource contextSource, + AccessControlService acs) { + var rbacEnabled = acs != null && acs.isRbacEnabled(); + + DefaultLdapAuthoritiesPopulator extractor; + + if (rbacEnabled) { + extractor = new RbacLdapAuthoritiesExtractor(context, contextSource, props.getGroupFilterSearchBase()); + } else { + extractor = new DefaultLdapAuthoritiesPopulator(contextSource, props.getGroupFilterSearchBase()); + } + + Optional.ofNullable(props.getGroupFilterSearchFilter()).ifPresent(extractor::setGroupSearchFilter); + extractor.setRolePrefix(""); + extractor.setConvertToUpperCase(false); + extractor.setSearchSubtree(true); + return extractor; + } + @Bean public SecurityWebFilterChain configureLdap(ServerHttpSecurity http) { log.info("Configuring LDAP authentication."); + if (props.isActiveDirectory()) { + log.info("Active Directory support for LDAP has been enabled."); + } - http - .authorizeExchange() - .pathMatchers(AUTH_WHITELIST) - .permitAll() - .anyExchange() - .authenticated() - .and() - .httpBasic(); + return http.authorizeExchange(spec -> spec + .pathMatchers(AUTH_WHITELIST) + .permitAll() + .anyExchange() + .authenticated() + ) + .formLogin(Customizer.withDefaults()) + .logout(Customizer.withDefaults()) + .csrf(ServerHttpSecurity.CsrfSpec::disable) + .build(); + } - return http.csrf().disable().build(); + private static class UserDetailsMapper extends LdapUserDetailsMapper { + @Override + public UserDetails mapUserFromContext(DirContextOperations ctx, String username, + Collection authorities) { + UserDetails userDetails = super.mapUserFromContext(ctx, username, authorities); + return new RbacLdapUser(userDetails); + } } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthProperties.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthProperties.java new file mode 100644 index 00000000000..4064fbc352d --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthProperties.java @@ -0,0 +1,53 @@ +package com.provectus.kafka.ui.config.auth; + +import jakarta.annotation.PostConstruct; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.Assert; + +@ConfigurationProperties("auth.oauth2") +@Data +public class OAuthProperties { + private Map client = new HashMap<>(); + + @PostConstruct + public void init() { + getClient().values().forEach((provider) -> { + if (provider.getCustomParams() == null) { + provider.setCustomParams(Collections.emptyMap()); + } + if (provider.getScope() == null) { + provider.setScope(Collections.emptySet()); + } + }); + + getClient().values().forEach(this::validateProvider); + } + + private void validateProvider(final OAuth2Provider provider) { + Assert.hasText(provider.getClientId(), "Client id must not be empty."); + Assert.hasText(provider.getProvider(), "Provider name must not be empty"); + } + + @Data + public static class OAuth2Provider { + private String provider; + private String clientId; + private String clientSecret; + private String clientName; + private String redirectUri; + private String authorizationGrantType; + private Set scope; + private String issuerUri; + private String authorizationUri; + private String tokenUri; + private String userInfoUri; + private String jwkSetUri; + private String userNameAttribute; + private Map customParams; + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthPropertiesConverter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthPropertiesConverter.java new file mode 100644 index 00000000000..f7f986f5ea9 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthPropertiesConverter.java @@ -0,0 +1,79 @@ +package com.provectus.kafka.ui.config.auth; + +import static com.provectus.kafka.ui.config.auth.OAuthProperties.OAuth2Provider; +import static org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Provider; +import static org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Registration; + +import java.util.Optional; +import java.util.Set; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; +import org.springframework.security.config.oauth2.client.CommonOAuth2Provider; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class OAuthPropertiesConverter { + + private static final String TYPE = "type"; + private static final String GOOGLE = "google"; + public static final String DUMMY = "dummy"; + + public static OAuth2ClientProperties convertProperties(final OAuthProperties properties) { + final var result = new OAuth2ClientProperties(); + properties.getClient().forEach((key, provider) -> { + var registration = new Registration(); + registration.setClientId(provider.getClientId()); + registration.setClientSecret(provider.getClientSecret()); + registration.setClientName(provider.getClientName()); + registration.setScope(Optional.ofNullable(provider.getScope()).orElse(Set.of())); + registration.setRedirectUri(provider.getRedirectUri()); + registration.setAuthorizationGrantType(provider.getAuthorizationGrantType()); + + result.getRegistration().put(key, registration); + + var clientProvider = new Provider(); + applyCustomTransformations(provider); + + clientProvider.setAuthorizationUri(provider.getAuthorizationUri()); + clientProvider.setIssuerUri(provider.getIssuerUri()); + clientProvider.setJwkSetUri(provider.getJwkSetUri()); + clientProvider.setTokenUri(provider.getTokenUri()); + clientProvider.setUserInfoUri(provider.getUserInfoUri()); + clientProvider.setUserNameAttribute(provider.getUserNameAttribute()); + + result.getProvider().put(key, clientProvider); + }); + return result; + } + + private static void applyCustomTransformations(OAuth2Provider provider) { + applyGoogleTransformations(provider); + } + + private static void applyGoogleTransformations(OAuth2Provider provider) { + if (!isGoogle(provider)) { + return; + } + + String allowedDomain = provider.getCustomParams().get("allowedDomain"); + if (StringUtils.isEmpty(allowedDomain)) { + return; + } + + String authorizationUri = CommonOAuth2Provider.GOOGLE + .getBuilder(DUMMY) + .clientId(DUMMY) + .build() + .getProviderDetails() + .getAuthorizationUri(); + + final String newUri = authorizationUri + "?hd=" + allowedDomain; + provider.setAuthorizationUri(newUri); + } + + private static boolean isGoogle(OAuth2Provider provider) { + return GOOGLE.equalsIgnoreCase(provider.getCustomParams().get(TYPE)); + } +} + diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthSecurityConfig.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthSecurityConfig.java index 657cab26449..797b41c6df1 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthSecurityConfig.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthSecurityConfig.java @@ -1,66 +1,129 @@ package com.provectus.kafka.ui.config.auth; -import lombok.AllArgsConstructor; +import com.provectus.kafka.ui.config.auth.logout.OAuthLogoutSuccessHandler; +import com.provectus.kafka.ui.service.rbac.AccessControlService; +import com.provectus.kafka.ui.service.rbac.extractor.ProviderAuthorityExtractor; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.jetbrains.annotations.Nullable; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.ApplicationContext; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesMapper; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.web.server.logout.OidcClientInitiatedServerLogoutSuccessHandler; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; +import org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.web.server.SecurityWebFilterChain; -import org.springframework.util.ClassUtils; +import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; +import reactor.core.publisher.Mono; @Configuration -@EnableWebFluxSecurity @ConditionalOnProperty(value = "auth.type", havingValue = "OAUTH2") -@AllArgsConstructor +@EnableConfigurationProperties(OAuthProperties.class) +@EnableWebFluxSecurity +@EnableReactiveMethodSecurity +@RequiredArgsConstructor @Log4j2 public class OAuthSecurityConfig extends AbstractAuthSecurityConfig { - public static final String REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME = - "org.springframework.security.oauth2.client.registration." - + "ReactiveClientRegistrationRepository"; + private final OAuthProperties properties; - private static final boolean IS_OAUTH2_PRESENT = ClassUtils.isPresent( - REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME, - OAuthSecurityConfig.class.getClassLoader() - ); + @Bean + public SecurityWebFilterChain configure(ServerHttpSecurity http, OAuthLogoutSuccessHandler logoutHandler) { + log.info("Configuring OAUTH2 authentication."); - private final ApplicationContext context; + return http.authorizeExchange(spec -> spec + .pathMatchers(AUTH_WHITELIST) + .permitAll() + .anyExchange() + .authenticated() + ) + .oauth2Login(Customizer.withDefaults()) + .logout(spec -> spec.logoutSuccessHandler(logoutHandler)) + .csrf(ServerHttpSecurity.CsrfSpec::disable) + .build(); + } @Bean - public SecurityWebFilterChain configure(ServerHttpSecurity http) { - log.info("Configuring OAUTH2 authentication."); - http.authorizeExchange() - .pathMatchers(AUTH_WHITELIST) - .permitAll() - .anyExchange() - .authenticated(); - - if (IS_OAUTH2_PRESENT && OAuth2ClasspathGuard.shouldConfigure(this.context)) { - OAuth2ClasspathGuard.configure(http); - } + public ReactiveOAuth2UserService customOidcUserService(AccessControlService acs) { + final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService(); + return request -> delegate.loadUser(request) + .flatMap(user -> { + var provider = getProviderByProviderId(request.getClientRegistration().getRegistrationId()); + final var extractor = getExtractor(provider, acs); + if (extractor == null) { + return Mono.just(user); + } - return http.csrf().disable().build(); + return extractor.extract(acs, user, Map.of("request", request, "provider", provider)) + .map(groups -> new RbacOidcUser(user, groups)); + }); } - private static class OAuth2ClasspathGuard { - static void configure(ServerHttpSecurity http) { - http - .oauth2Login() - .and() - .oauth2Client(); - } + @Bean + public ReactiveOAuth2UserService customOauth2UserService(AccessControlService acs) { + final DefaultReactiveOAuth2UserService delegate = new DefaultReactiveOAuth2UserService(); + return request -> delegate.loadUser(request) + .flatMap(user -> { + var provider = getProviderByProviderId(request.getClientRegistration().getRegistrationId()); + final var extractor = getExtractor(provider, acs); + if (extractor == null) { + return Mono.just(user); + } - static boolean shouldConfigure(ApplicationContext context) { - ClassLoader loader = context.getClassLoader(); - Class reactiveClientRegistrationRepositoryClass = - ClassUtils.resolveClassName(REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME, loader); - return context.getBeanNamesForType(reactiveClientRegistrationRepositoryClass).length == 1; + return extractor.extract(acs, user, Map.of("request", request, "provider", provider)) + .map(groups -> new RbacOAuth2User(user, groups)); + }); + } + + @Bean + public InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() { + final OAuth2ClientProperties props = OAuthPropertiesConverter.convertProperties(properties); + final List registrations = + new ArrayList<>(new OAuth2ClientPropertiesMapper(props).asClientRegistrations().values()); + if (registrations.isEmpty()) { + throw new IllegalArgumentException("OAuth2 authentication is enabled but no providers specified."); } + return new InMemoryReactiveClientRegistrationRepository(registrations); + } + + @Bean + public ServerLogoutSuccessHandler defaultOidcLogoutHandler(final ReactiveClientRegistrationRepository repository) { + return new OidcClientInitiatedServerLogoutSuccessHandler(repository); } + @Nullable + private ProviderAuthorityExtractor getExtractor(final OAuthProperties.OAuth2Provider provider, + AccessControlService acs) { + Optional extractor = acs.getOauthExtractors() + .stream() + .filter(e -> e.isApplicable(provider.getProvider(), provider.getCustomParams())) + .findFirst(); + + return extractor.orElse(null); + } + + private OAuthProperties.OAuth2Provider getProviderByProviderId(final String providerId) { + return properties.getClient().get(providerId); + } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacLdapUser.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacLdapUser.java new file mode 100644 index 00000000000..037d2fd3020 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacLdapUser.java @@ -0,0 +1,60 @@ +package com.provectus.kafka.ui.config.auth; + +import java.util.Collection; +import java.util.stream.Collectors; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +public class RbacLdapUser implements UserDetails, RbacUser { + + private final UserDetails userDetails; + + public RbacLdapUser(UserDetails userDetails) { + this.userDetails = userDetails; + } + + @Override + public String name() { + return userDetails.getUsername(); + } + + @Override + public Collection groups() { + return userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet()); + } + + @Override + public Collection getAuthorities() { + return userDetails.getAuthorities(); + } + + @Override + public String getPassword() { + return userDetails.getPassword(); + } + + @Override + public String getUsername() { + return userDetails.getUsername(); + } + + @Override + public boolean isAccountNonExpired() { + return userDetails.isAccountNonExpired(); + } + + @Override + public boolean isAccountNonLocked() { + return userDetails.isAccountNonLocked(); + } + + @Override + public boolean isCredentialsNonExpired() { + return userDetails.isCredentialsNonExpired(); + } + + @Override + public boolean isEnabled() { + return userDetails.isEnabled(); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacOAuth2User.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacOAuth2User.java new file mode 100644 index 00000000000..ce0622cc18e --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacOAuth2User.java @@ -0,0 +1,29 @@ +package com.provectus.kafka.ui.config.auth; + +import java.util.Collection; +import java.util.Map; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; + +public record RbacOAuth2User(OAuth2User user, Collection groups) implements RbacUser, OAuth2User { + + @Override + public Map getAttributes() { + return user.getAttributes(); + } + + @Override + public Collection getAuthorities() { + return user.getAuthorities(); + } + + @Override + public String getName() { + return user.getName(); + } + + @Override + public String name() { + return user.getName(); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacOidcUser.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacOidcUser.java new file mode 100644 index 00000000000..9642883f432 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacOidcUser.java @@ -0,0 +1,46 @@ +package com.provectus.kafka.ui.config.auth; + +import java.util.Collection; +import java.util.Map; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; + +public record RbacOidcUser(OidcUser user, Collection groups) implements RbacUser, OidcUser { + + @Override + public Map getClaims() { + return user.getClaims(); + } + + @Override + public OidcUserInfo getUserInfo() { + return user.getUserInfo(); + } + + @Override + public OidcIdToken getIdToken() { + return user.getIdToken(); + } + + @Override + public Map getAttributes() { + return user.getAttributes(); + } + + @Override + public Collection getAuthorities() { + return user.getAuthorities(); + } + + @Override + public String getName() { + return user.getName(); + } + + @Override + public String name() { + return user.getName(); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacUser.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacUser.java new file mode 100644 index 00000000000..f3229f37a47 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacUser.java @@ -0,0 +1,10 @@ +package com.provectus.kafka.ui.config.auth; + +import java.util.Collection; + +public interface RbacUser { + String name(); + + Collection groups(); + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RoleBasedAccessControlProperties.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RoleBasedAccessControlProperties.java new file mode 100644 index 00000000000..cf1cdb5b704 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RoleBasedAccessControlProperties.java @@ -0,0 +1,23 @@ +package com.provectus.kafka.ui.config.auth; + +import com.provectus.kafka.ui.model.rbac.Role; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.PostConstruct; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("rbac") +public class RoleBasedAccessControlProperties { + + private final List roles = new ArrayList<>(); + + @PostConstruct + public void init() { + roles.forEach(Role::validate); + } + + public List getRoles() { + return roles; + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/condition/ActiveDirectoryCondition.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/condition/ActiveDirectoryCondition.java new file mode 100644 index 00000000000..c38e83238af --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/condition/ActiveDirectoryCondition.java @@ -0,0 +1,21 @@ +package com.provectus.kafka.ui.config.auth.condition; + +import org.springframework.boot.autoconfigure.condition.AllNestedConditions; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +public class ActiveDirectoryCondition extends AllNestedConditions { + + public ActiveDirectoryCondition() { + super(ConfigurationPhase.PARSE_CONFIGURATION); + } + + @ConditionalOnProperty(value = "auth.type", havingValue = "LDAP") + public static class OnAuthType { + + } + + @ConditionalOnProperty(value = "${oauth2.ldap.activeDirectory}:false", havingValue = "true", matchIfMissing = false) + public static class OnActiveDirectory { + + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/condition/CognitoCondition.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/condition/CognitoCondition.java new file mode 100644 index 00000000000..c3699858722 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/condition/CognitoCondition.java @@ -0,0 +1,14 @@ +package com.provectus.kafka.ui.config.auth.condition; + +import com.provectus.kafka.ui.service.rbac.AbstractProviderCondition; +import org.jetbrains.annotations.NotNull; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +public class CognitoCondition extends AbstractProviderCondition implements Condition { + @Override + public boolean matches(final ConditionContext context, final @NotNull AnnotatedTypeMetadata metadata) { + return getRegisteredProvidersTypes(context.getEnvironment()).stream().anyMatch(a -> a.equalsIgnoreCase("cognito")); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/CognitoLogoutSuccessHandler.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/CognitoLogoutSuccessHandler.java new file mode 100644 index 00000000000..3d0da9d05a6 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/CognitoLogoutSuccessHandler.java @@ -0,0 +1,64 @@ +package com.provectus.kafka.ui.config.auth.logout; + +import com.provectus.kafka.ui.config.auth.OAuthProperties; +import com.provectus.kafka.ui.config.auth.condition.CognitoCondition; +import com.provectus.kafka.ui.model.rbac.provider.Provider; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import org.springframework.context.annotation.Conditional; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.WebFilterExchange; +import org.springframework.security.web.util.UrlUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.web.server.WebSession; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; +import reactor.core.publisher.Mono; + +@Component +@Conditional(CognitoCondition.class) +public class CognitoLogoutSuccessHandler implements LogoutSuccessHandler { + + @Override + public boolean isApplicable(String provider) { + return Provider.Name.COGNITO.equalsIgnoreCase(provider); + } + + @Override + public Mono handle(WebFilterExchange exchange, Authentication authentication, + OAuthProperties.OAuth2Provider provider) { + final ServerHttpResponse response = exchange.getExchange().getResponse(); + response.setStatusCode(HttpStatus.FOUND); + + final var requestUri = exchange.getExchange().getRequest().getURI(); + + final var fullUrl = UrlUtils.buildFullRequestUrl(requestUri.getScheme(), + requestUri.getHost(), requestUri.getPort(), + requestUri.getPath(), requestUri.getQuery()); + + final UriComponents baseUrl = UriComponentsBuilder + .fromHttpUrl(fullUrl) + .replacePath("/") + .replaceQuery(null) + .fragment(null) + .build(); + + Assert.isTrue(provider.getCustomParams().containsKey("logoutUrl"), + "Custom params should contain 'logoutUrl'"); + final var uri = UriComponentsBuilder + .fromUri(URI.create(provider.getCustomParams().get("logoutUrl"))) + .queryParam("client_id", provider.getClientId()) + .queryParam("logout_uri", baseUrl) + .encode(StandardCharsets.UTF_8) + .build() + .toUri(); + + response.getHeaders().setLocation(uri); + return exchange.getExchange().getSession().flatMap(WebSession::invalidate); + } + +} + diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/LogoutSuccessHandler.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/LogoutSuccessHandler.java new file mode 100644 index 00000000000..6a2d90b1342 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/LogoutSuccessHandler.java @@ -0,0 +1,15 @@ +package com.provectus.kafka.ui.config.auth.logout; + +import com.provectus.kafka.ui.config.auth.OAuthProperties; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.WebFilterExchange; +import reactor.core.publisher.Mono; + +public interface LogoutSuccessHandler { + + boolean isApplicable(final String provider); + + Mono handle(final WebFilterExchange exchange, + final Authentication authentication, + final OAuthProperties.OAuth2Provider provider); +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/OAuthLogoutSuccessHandler.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/OAuthLogoutSuccessHandler.java new file mode 100644 index 00000000000..a2b6a8abbc6 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/OAuthLogoutSuccessHandler.java @@ -0,0 +1,46 @@ +package com.provectus.kafka.ui.config.auth.logout; + +import com.provectus.kafka.ui.config.auth.OAuthProperties; +import java.util.List; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.web.server.WebFilterExchange; +import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +@Component +@ConditionalOnProperty(value = "auth.type", havingValue = "OAUTH2") +public class OAuthLogoutSuccessHandler implements ServerLogoutSuccessHandler { + private final OAuthProperties properties; + private final List logoutSuccessHandlers; + private final ServerLogoutSuccessHandler defaultOidcLogoutHandler; + + public OAuthLogoutSuccessHandler(final OAuthProperties properties, + final List logoutSuccessHandlers, + final @Qualifier("defaultOidcLogoutHandler") ServerLogoutSuccessHandler handler) { + this.properties = properties; + this.logoutSuccessHandlers = logoutSuccessHandlers; + this.defaultOidcLogoutHandler = handler; + } + + @Override + public Mono onLogoutSuccess(final WebFilterExchange exchange, + final Authentication authentication) { + final OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication; + final String providerId = oauthToken.getAuthorizedClientRegistrationId(); + final OAuthProperties.OAuth2Provider oAuth2Provider = properties.getClient().get(providerId); + return getLogoutHandler(oAuth2Provider.getProvider()) + .map(handler -> handler.handle(exchange, authentication, oAuth2Provider)) + .orElseGet(() -> defaultOidcLogoutHandler.onLogoutSuccess(exchange, authentication)); + } + + private Optional getLogoutHandler(final String provider) { + return logoutSuccessHandlers.stream() + .filter(h -> h.isApplicable(provider)) + .findFirst(); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AbstractController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AbstractController.java index fd323d55a14..e4dbb3cfcf7 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AbstractController.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AbstractController.java @@ -2,12 +2,19 @@ import com.provectus.kafka.ui.exception.ClusterNotFoundException; import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.rbac.AccessContext; import com.provectus.kafka.ui.service.ClustersStorage; +import com.provectus.kafka.ui.service.audit.AuditService; +import com.provectus.kafka.ui.service.rbac.AccessControlService; import org.springframework.beans.factory.annotation.Autowired; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Signal; public abstract class AbstractController { - private ClustersStorage clustersStorage; + protected ClustersStorage clustersStorage; + protected AccessControlService accessControlService; + protected AuditService auditService; protected KafkaCluster getCluster(String name) { return clustersStorage.getClusterByName(name) @@ -15,8 +22,26 @@ protected KafkaCluster getCluster(String name) { String.format("Cluster with name '%s' not found", name))); } + protected Mono validateAccess(AccessContext context) { + return accessControlService.validateAccess(context); + } + + protected void audit(AccessContext acxt, Signal sig) { + auditService.audit(acxt, sig); + } + @Autowired public void setClustersStorage(ClustersStorage clustersStorage) { this.clustersStorage = clustersStorage; } + + @Autowired + public void setAccessControlService(AccessControlService accessControlService) { + this.accessControlService = accessControlService; + } + + @Autowired + public void setAuditService(AuditService auditService) { + this.auditService = auditService; + } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AccessController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AccessController.java new file mode 100644 index 00000000000..e6f2e3fdfdd --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AccessController.java @@ -0,0 +1,88 @@ +package com.provectus.kafka.ui.controller; + +import com.provectus.kafka.ui.api.AuthorizationApi; +import com.provectus.kafka.ui.model.ActionDTO; +import com.provectus.kafka.ui.model.AuthenticationInfoDTO; +import com.provectus.kafka.ui.model.ResourceTypeDTO; +import com.provectus.kafka.ui.model.UserInfoDTO; +import com.provectus.kafka.ui.model.UserPermissionDTO; +import com.provectus.kafka.ui.model.rbac.Permission; +import com.provectus.kafka.ui.service.rbac.AccessControlService; +import java.security.Principal; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nullable; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +@RestController +@RequiredArgsConstructor +@Slf4j +public class AccessController implements AuthorizationApi { + + private final AccessControlService accessControlService; + + public Mono> getUserAuthInfo(ServerWebExchange exchange) { + Mono> permissions = accessControlService.getUser() + .map(user -> accessControlService.getRoles() + .stream() + .filter(role -> user.groups().contains(role.getName())) + .map(role -> mapPermissions(role.getPermissions(), role.getClusters())) + .flatMap(Collection::stream) + .toList() + ) + .switchIfEmpty(Mono.just(Collections.emptyList())); + + Mono userName = ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .map(Principal::getName); + + return userName + .zipWith(permissions) + .map(data -> { + var dto = new AuthenticationInfoDTO(accessControlService.isRbacEnabled()); + dto.setUserInfo(new UserInfoDTO(data.getT1(), data.getT2())); + return dto; + }) + .switchIfEmpty(Mono.just(new AuthenticationInfoDTO(accessControlService.isRbacEnabled()))) + .map(ResponseEntity::ok); + } + + private List mapPermissions(List permissions, List clusters) { + return permissions + .stream() + .map(permission -> { + UserPermissionDTO dto = new UserPermissionDTO(); + dto.setClusters(clusters); + dto.setResource(ResourceTypeDTO.fromValue(permission.getResource().toString().toUpperCase())); + dto.setValue(permission.getValue()); + dto.setActions(permission.getActions() + .stream() + .map(String::toUpperCase) + .map(this::mapAction) + .filter(Objects::nonNull) + .toList()); + return dto; + }) + .toList(); + } + + @Nullable + private ActionDTO mapAction(String name) { + try { + return ActionDTO.fromValue(name); + } catch (IllegalArgumentException e) { + log.warn("Unknown Action [{}], skipping", name); + return null; + } + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AclsController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AclsController.java new file mode 100644 index 00000000000..2ba0add5be0 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AclsController.java @@ -0,0 +1,176 @@ +package com.provectus.kafka.ui.controller; + +import com.provectus.kafka.ui.api.AclsApi; +import com.provectus.kafka.ui.mapper.ClusterMapper; +import com.provectus.kafka.ui.model.CreateConsumerAclDTO; +import com.provectus.kafka.ui.model.CreateProducerAclDTO; +import com.provectus.kafka.ui.model.CreateStreamAppAclDTO; +import com.provectus.kafka.ui.model.KafkaAclDTO; +import com.provectus.kafka.ui.model.KafkaAclNamePatternTypeDTO; +import com.provectus.kafka.ui.model.KafkaAclResourceTypeDTO; +import com.provectus.kafka.ui.model.rbac.AccessContext; +import com.provectus.kafka.ui.model.rbac.permission.AclAction; +import com.provectus.kafka.ui.service.acl.AclsService; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.apache.kafka.common.resource.PatternType; +import org.apache.kafka.common.resource.ResourcePatternFilter; +import org.apache.kafka.common.resource.ResourceType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@RestController +@RequiredArgsConstructor +public class AclsController extends AbstractController implements AclsApi { + + private final AclsService aclsService; + + @Override + public Mono> createAcl(String clusterName, Mono kafkaAclDto, + ServerWebExchange exchange) { + AccessContext context = AccessContext.builder() + .cluster(clusterName) + .aclActions(AclAction.EDIT) + .operationName("createAcl") + .build(); + + return validateAccess(context) + .then(kafkaAclDto) + .map(ClusterMapper::toAclBinding) + .flatMap(binding -> aclsService.createAcl(getCluster(clusterName), binding)) + .doOnEach(sig -> audit(context, sig)) + .thenReturn(ResponseEntity.ok().build()); + } + + @Override + public Mono> deleteAcl(String clusterName, Mono kafkaAclDto, + ServerWebExchange exchange) { + AccessContext context = AccessContext.builder() + .cluster(clusterName) + .aclActions(AclAction.EDIT) + .operationName("deleteAcl") + .build(); + + return validateAccess(context) + .then(kafkaAclDto) + .map(ClusterMapper::toAclBinding) + .flatMap(binding -> aclsService.deleteAcl(getCluster(clusterName), binding)) + .doOnEach(sig -> audit(context, sig)) + .thenReturn(ResponseEntity.ok().build()); + } + + @Override + public Mono>> listAcls(String clusterName, + KafkaAclResourceTypeDTO resourceTypeDto, + String resourceName, + KafkaAclNamePatternTypeDTO namePatternTypeDto, + ServerWebExchange exchange) { + AccessContext context = AccessContext.builder() + .cluster(clusterName) + .aclActions(AclAction.VIEW) + .operationName("listAcls") + .build(); + + var resourceType = Optional.ofNullable(resourceTypeDto) + .map(ClusterMapper::mapAclResourceTypeDto) + .orElse(ResourceType.ANY); + + var namePatternType = Optional.ofNullable(namePatternTypeDto) + .map(ClusterMapper::mapPatternTypeDto) + .orElse(PatternType.ANY); + + var filter = new ResourcePatternFilter(resourceType, resourceName, namePatternType); + + return validateAccess(context).then( + Mono.just( + ResponseEntity.ok( + aclsService.listAcls(getCluster(clusterName), filter) + .map(ClusterMapper::toKafkaAclDto))) + ).doOnEach(sig -> audit(context, sig)); + } + + @Override + public Mono> getAclAsCsv(String clusterName, ServerWebExchange exchange) { + AccessContext context = AccessContext.builder() + .cluster(clusterName) + .aclActions(AclAction.VIEW) + .operationName("getAclAsCsv") + .build(); + + return validateAccess(context).then( + aclsService.getAclAsCsvString(getCluster(clusterName)) + .map(ResponseEntity::ok) + .flatMap(Mono::just) + .doOnEach(sig -> audit(context, sig)) + ); + } + + @Override + public Mono> syncAclsCsv(String clusterName, Mono csvMono, ServerWebExchange exchange) { + AccessContext context = AccessContext.builder() + .cluster(clusterName) + .aclActions(AclAction.EDIT) + .operationName("syncAclsCsv") + .build(); + + return validateAccess(context) + .then(csvMono) + .flatMap(csv -> aclsService.syncAclWithAclCsv(getCluster(clusterName), csv)) + .doOnEach(sig -> audit(context, sig)) + .thenReturn(ResponseEntity.ok().build()); + } + + @Override + public Mono> createConsumerAcl(String clusterName, + Mono createConsumerAclDto, + ServerWebExchange exchange) { + AccessContext context = AccessContext.builder() + .cluster(clusterName) + .aclActions(AclAction.EDIT) + .operationName("createConsumerAcl") + .build(); + + return validateAccess(context) + .then(createConsumerAclDto) + .flatMap(req -> aclsService.createConsumerAcl(getCluster(clusterName), req)) + .doOnEach(sig -> audit(context, sig)) + .thenReturn(ResponseEntity.ok().build()); + } + + @Override + public Mono> createProducerAcl(String clusterName, + Mono createProducerAclDto, + ServerWebExchange exchange) { + AccessContext context = AccessContext.builder() + .cluster(clusterName) + .aclActions(AclAction.EDIT) + .operationName("createProducerAcl") + .build(); + + return validateAccess(context) + .then(createProducerAclDto) + .flatMap(req -> aclsService.createProducerAcl(getCluster(clusterName), req)) + .doOnEach(sig -> audit(context, sig)) + .thenReturn(ResponseEntity.ok().build()); + } + + @Override + public Mono> createStreamAppAcl(String clusterName, + Mono createStreamAppAclDto, + ServerWebExchange exchange) { + AccessContext context = AccessContext.builder() + .cluster(clusterName) + .aclActions(AclAction.EDIT) + .operationName("createStreamAppAcl") + .build(); + + return validateAccess(context) + .then(createStreamAppAclDto) + .flatMap(req -> aclsService.createStreamAppAcl(getCluster(clusterName), req)) + .doOnEach(sig -> audit(context, sig)) + .thenReturn(ResponseEntity.ok().build()); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ApplicationConfigController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ApplicationConfigController.java new file mode 100644 index 00000000000..6699225310a --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ApplicationConfigController.java @@ -0,0 +1,139 @@ +package com.provectus.kafka.ui.controller; + +import static com.provectus.kafka.ui.model.rbac.permission.ApplicationConfigAction.EDIT; +import static com.provectus.kafka.ui.model.rbac.permission.ApplicationConfigAction.VIEW; + +import com.provectus.kafka.ui.api.ApplicationConfigApi; +import com.provectus.kafka.ui.config.ClustersProperties; +import com.provectus.kafka.ui.model.ApplicationConfigDTO; +import com.provectus.kafka.ui.model.ApplicationConfigPropertiesDTO; +import com.provectus.kafka.ui.model.ApplicationConfigValidationDTO; +import com.provectus.kafka.ui.model.ApplicationInfoDTO; +import com.provectus.kafka.ui.model.ClusterConfigValidationDTO; +import com.provectus.kafka.ui.model.RestartRequestDTO; +import com.provectus.kafka.ui.model.UploadedFileInfoDTO; +import com.provectus.kafka.ui.model.rbac.AccessContext; +import com.provectus.kafka.ui.service.ApplicationInfoService; +import com.provectus.kafka.ui.service.KafkaClusterFactory; +import com.provectus.kafka.ui.util.ApplicationRestarter; +import com.provectus.kafka.ui.util.DynamicConfigOperations; +import com.provectus.kafka.ui.util.DynamicConfigOperations.PropertiesStructure; +import java.util.Map; +import javax.annotation.Nullable; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; +import org.springframework.http.ResponseEntity; +import org.springframework.http.codec.multipart.FilePart; +import org.springframework.http.codec.multipart.Part; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +@Slf4j +@RestController +@RequiredArgsConstructor +public class ApplicationConfigController extends AbstractController implements ApplicationConfigApi { + + private static final PropertiesMapper MAPPER = Mappers.getMapper(PropertiesMapper.class); + + @Mapper + interface PropertiesMapper { + + PropertiesStructure fromDto(ApplicationConfigPropertiesDTO dto); + + ApplicationConfigPropertiesDTO toDto(PropertiesStructure propertiesStructure); + } + + private final DynamicConfigOperations dynamicConfigOperations; + private final ApplicationRestarter restarter; + private final KafkaClusterFactory kafkaClusterFactory; + private final ApplicationInfoService applicationInfoService; + + @Override + public Mono> getApplicationInfo(ServerWebExchange exchange) { + return Mono.just(applicationInfoService.getApplicationInfo()).map(ResponseEntity::ok); + } + + @Override + public Mono> getCurrentConfig(ServerWebExchange exchange) { + var context = AccessContext.builder() + .applicationConfigActions(VIEW) + .operationName("getCurrentConfig") + .build(); + return validateAccess(context) + .then(Mono.fromSupplier(() -> ResponseEntity.ok( + new ApplicationConfigDTO() + .properties(MAPPER.toDto(dynamicConfigOperations.getCurrentProperties())) + ))) + .doOnEach(sig -> audit(context, sig)); + } + + @Override + public Mono> restartWithConfig(Mono restartRequestDto, + ServerWebExchange exchange) { + var context = AccessContext.builder() + .applicationConfigActions(EDIT) + .operationName("restartWithConfig") + .build(); + return validateAccess(context) + .then(restartRequestDto) + .doOnNext(restartDto -> { + var newConfig = MAPPER.fromDto(restartDto.getConfig().getProperties()); + dynamicConfigOperations.persist(newConfig); + }) + .doOnEach(sig -> audit(context, sig)) + .doOnSuccess(dto -> restarter.requestRestart()) + .map(dto -> ResponseEntity.ok().build()); + } + + @Override + public Mono> uploadConfigRelatedFile(Flux fileFlux, + ServerWebExchange exchange) { + var context = AccessContext.builder() + .applicationConfigActions(EDIT) + .operationName("uploadConfigRelatedFile") + .build(); + return validateAccess(context) + .then(fileFlux.single()) + .flatMap(file -> + dynamicConfigOperations.uploadConfigRelatedFile((FilePart) file) + .map(path -> new UploadedFileInfoDTO().location(path.toString())) + .map(ResponseEntity::ok)) + .doOnEach(sig -> audit(context, sig)); + } + + @Override + public Mono> validateConfig(Mono configDto, + ServerWebExchange exchange) { + var context = AccessContext.builder() + .applicationConfigActions(EDIT) + .operationName("validateConfig") + .build(); + return validateAccess(context) + .then(configDto) + .flatMap(config -> { + PropertiesStructure newConfig = MAPPER.fromDto(config.getProperties()); + ClustersProperties clustersProperties = newConfig.getKafka(); + return validateClustersConfig(clustersProperties) + .map(validations -> new ApplicationConfigValidationDTO().clusters(validations)); + }) + .map(ResponseEntity::ok) + .doOnEach(sig -> audit(context, sig)); + } + + private Mono> validateClustersConfig( + @Nullable ClustersProperties properties) { + if (properties == null || properties.getClusters() == null) { + return Mono.just(Map.of()); + } + properties.validateAndSetDefaults(); + return Flux.fromIterable(properties.getClusters()) + .flatMap(c -> kafkaClusterFactory.validate(c).map(v -> Tuples.of(c.getName(), v))) + .collectMap(Tuple2::getT1, Tuple2::getT2); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AuthController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AuthController.java index acc30c59658..da453eb79a6 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AuthController.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AuthController.java @@ -36,10 +36,10 @@ private byte[] createPage(ServerWebExchange exchange, String csrfTokenHtmlInput) + " \n" + " \n" + " Please sign in\n" - + " \n" - + " \n" + " \n" + " \n" diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/BrokersController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/BrokersController.java index df809d615ac..31f02e2b4c8 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/BrokersController.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/BrokersController.java @@ -8,8 +8,12 @@ import com.provectus.kafka.ui.model.BrokerLogdirUpdateDTO; import com.provectus.kafka.ui.model.BrokerMetricsDTO; import com.provectus.kafka.ui.model.BrokersLogdirsDTO; +import com.provectus.kafka.ui.model.rbac.AccessContext; +import com.provectus.kafka.ui.model.rbac.permission.ClusterConfigAction; import com.provectus.kafka.ui.service.BrokerService; import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; @@ -22,48 +26,98 @@ @RequiredArgsConstructor @Slf4j public class BrokersController extends AbstractController implements BrokersApi { + private static final String BROKER_ID = "brokerId"; + private final BrokerService brokerService; private final ClusterMapper clusterMapper; @Override - public Mono> getBrokersMetrics(String clusterName, Integer id, - ServerWebExchange exchange) { - return brokerService.getBrokerMetrics(getCluster(clusterName), id) - .map(clusterMapper::toBrokerMetrics) - .map(ResponseEntity::ok) - .onErrorReturn(ResponseEntity.notFound().build()); + public Mono>> getBrokers(String clusterName, + ServerWebExchange exchange) { + var context = AccessContext.builder() + .cluster(clusterName) + .operationName("getBrokers") + .build(); + + var job = brokerService.getBrokers(getCluster(clusterName)).map(clusterMapper::toBrokerDto); + return validateAccess(context) + .thenReturn(ResponseEntity.ok(job)) + .doOnEach(sig -> audit(context, sig)); } @Override - public Mono>> getBrokers(String clusterName, - ServerWebExchange exchange) { - return Mono.just(ResponseEntity.ok(brokerService.getBrokers(getCluster(clusterName)))); + public Mono> getBrokersMetrics(String clusterName, Integer id, + ServerWebExchange exchange) { + var context = AccessContext.builder() + .cluster(clusterName) + .operationName("getBrokersMetrics") + .operationParams(Map.of("id", id)) + .build(); + + return validateAccess(context) + .then( + brokerService.getBrokerMetrics(getCluster(clusterName), id) + .map(clusterMapper::toBrokerMetrics) + .map(ResponseEntity::ok) + .onErrorReturn(ResponseEntity.notFound().build()) + ) + .doOnEach(sig -> audit(context, sig)); } @Override public Mono>> getAllBrokersLogdirs(String clusterName, - List brokers, - ServerWebExchange exchange - ) { - return Mono.just(ResponseEntity.ok( - brokerService.getAllBrokersLogdirs(getCluster(clusterName), brokers))); + @Nullable List brokers, + ServerWebExchange exchange) { + + List brokerIds = brokers == null ? List.of() : brokers; + + var context = AccessContext.builder() + .cluster(clusterName) + .operationName("getAllBrokersLogdirs") + .operationParams(Map.of("brokerIds", brokerIds)) + .build(); + + return validateAccess(context) + .thenReturn(ResponseEntity.ok( + brokerService.getAllBrokersLogdirs(getCluster(clusterName), brokerIds))) + .doOnEach(sig -> audit(context, sig)); } @Override - public Mono>> getBrokerConfig(String clusterName, Integer id, + public Mono>> getBrokerConfig(String clusterName, + Integer id, ServerWebExchange exchange) { - return Mono.just(ResponseEntity.ok( - brokerService.getBrokerConfig(getCluster(clusterName), id) - .map(clusterMapper::toBrokerConfig))); + var context = AccessContext.builder() + .cluster(clusterName) + .clusterConfigActions(ClusterConfigAction.VIEW) + .operationName("getBrokerConfig") + .operationParams(Map.of(BROKER_ID, id)) + .build(); + + return validateAccess(context).thenReturn( + ResponseEntity.ok( + brokerService.getBrokerConfig(getCluster(clusterName), id) + .map(clusterMapper::toBrokerConfig)) + ).doOnEach(sig -> audit(context, sig)); } @Override - public Mono> updateBrokerTopicPartitionLogDir( - String clusterName, Integer id, Mono brokerLogdir, - ServerWebExchange exchange) { - return brokerLogdir - .flatMap(bld -> brokerService.updateBrokerLogDir(getCluster(clusterName), id, bld)) - .map(ResponseEntity::ok); + public Mono> updateBrokerTopicPartitionLogDir(String clusterName, + Integer id, + Mono brokerLogdir, + ServerWebExchange exchange) { + var context = AccessContext.builder() + .cluster(clusterName) + .clusterConfigActions(ClusterConfigAction.VIEW, ClusterConfigAction.EDIT) + .operationName("updateBrokerTopicPartitionLogDir") + .operationParams(Map.of(BROKER_ID, id)) + .build(); + + return validateAccess(context).then( + brokerLogdir + .flatMap(bld -> brokerService.updateBrokerLogDir(getCluster(clusterName), id, bld)) + .map(ResponseEntity::ok) + ).doOnEach(sig -> audit(context, sig)); } @Override @@ -72,9 +126,18 @@ public Mono> updateBrokerConfigByName(String clusterName, String name, Mono brokerConfig, ServerWebExchange exchange) { - return brokerConfig - .flatMap(bci -> brokerService.updateBrokerConfigByName( - getCluster(clusterName), id, name, bci.getValue())) - .map(ResponseEntity::ok); + var context = AccessContext.builder() + .cluster(clusterName) + .clusterConfigActions(ClusterConfigAction.VIEW, ClusterConfigAction.EDIT) + .operationName("updateBrokerConfigByName") + .operationParams(Map.of(BROKER_ID, id)) + .build(); + + return validateAccess(context).then( + brokerConfig + .flatMap(bci -> brokerService.updateBrokerConfigByName( + getCluster(clusterName), id, name, bci.getValue())) + .map(ResponseEntity::ok) + ).doOnEach(sig -> audit(context, sig)); } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ClustersController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ClustersController.java index 38b2b1dc654..7b12b30644f 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ClustersController.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ClustersController.java @@ -4,6 +4,7 @@ import com.provectus.kafka.ui.model.ClusterDTO; import com.provectus.kafka.ui.model.ClusterMetricsDTO; import com.provectus.kafka.ui.model.ClusterStatsDTO; +import com.provectus.kafka.ui.model.rbac.AccessContext; import com.provectus.kafka.ui.service.ClusterService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -19,30 +20,59 @@ public class ClustersController extends AbstractController implements ClustersApi { private final ClusterService clusterService; + @Override + public Mono>> getClusters(ServerWebExchange exchange) { + Flux job = Flux.fromIterable(clusterService.getClusters()) + .filterWhen(accessControlService::isClusterAccessible); + + return Mono.just(ResponseEntity.ok(job)); + } + @Override public Mono> getClusterMetrics(String clusterName, ServerWebExchange exchange) { - return clusterService.getClusterMetrics(getCluster(clusterName)) - .map(ResponseEntity::ok) - .onErrorReturn(ResponseEntity.notFound().build()); + AccessContext context = AccessContext.builder() + .cluster(clusterName) + .operationName("getClusterMetrics") + .build(); + + return validateAccess(context) + .then( + clusterService.getClusterMetrics(getCluster(clusterName)) + .map(ResponseEntity::ok) + .onErrorReturn(ResponseEntity.notFound().build()) + ) + .doOnEach(sig -> audit(context, sig)); } @Override public Mono> getClusterStats(String clusterName, ServerWebExchange exchange) { - return clusterService.getClusterStats(getCluster(clusterName)) - .map(ResponseEntity::ok) - .onErrorReturn(ResponseEntity.notFound().build()); - } + AccessContext context = AccessContext.builder() + .cluster(clusterName) + .operationName("getClusterStats") + .build(); - @Override - public Mono>> getClusters(ServerWebExchange exchange) { - return Mono.just(ResponseEntity.ok(Flux.fromIterable(clusterService.getClusters()))); + return validateAccess(context) + .then( + clusterService.getClusterStats(getCluster(clusterName)) + .map(ResponseEntity::ok) + .onErrorReturn(ResponseEntity.notFound().build()) + ) + .doOnEach(sig -> audit(context, sig)); } @Override public Mono> updateClusterInfo(String clusterName, ServerWebExchange exchange) { - return clusterService.updateCluster(getCluster(clusterName)).map(ResponseEntity::ok); + + AccessContext context = AccessContext.builder() + .cluster(clusterName) + .operationName("updateClusterInfo") + .build(); + + return validateAccess(context) + .then(clusterService.updateCluster(getCluster(clusterName)).map(ResponseEntity::ok)) + .doOnEach(sig -> audit(context, sig)); } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ConsumerGroupsController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ConsumerGroupsController.java index c0d4d0296cf..d4214e28770 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ConsumerGroupsController.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ConsumerGroupsController.java @@ -1,5 +1,8 @@ package com.provectus.kafka.ui.controller; +import static com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction.DELETE; +import static com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction.RESET_OFFSETS; +import static com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction.VIEW; import static java.util.stream.Collectors.toMap; import com.provectus.kafka.ui.api.ConsumerGroupsApi; @@ -12,11 +15,13 @@ import com.provectus.kafka.ui.model.ConsumerGroupsPageResponseDTO; import com.provectus.kafka.ui.model.PartitionOffsetDTO; import com.provectus.kafka.ui.model.SortOrderDTO; +import com.provectus.kafka.ui.model.rbac.AccessContext; +import com.provectus.kafka.ui.model.rbac.permission.TopicAction; import com.provectus.kafka.ui.service.ConsumerGroupService; import com.provectus.kafka.ui.service.OffsetsResetService; import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; +import java.util.function.Supplier; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -39,39 +44,64 @@ public class ConsumerGroupsController extends AbstractController implements Cons private int defaultConsumerGroupsPageSize; @Override - public Mono> deleteConsumerGroup(String clusterName, String id, + public Mono> deleteConsumerGroup(String clusterName, + String id, ServerWebExchange exchange) { - return consumerGroupService.deleteConsumerGroupById(getCluster(clusterName), id) + var context = AccessContext.builder() + .cluster(clusterName) + .consumerGroup(id) + .consumerGroupActions(DELETE) + .operationName("deleteConsumerGroup") + .build(); + + return validateAccess(context) + .then(consumerGroupService.deleteConsumerGroupById(getCluster(clusterName), id)) + .doOnEach(sig -> audit(context, sig)) .thenReturn(ResponseEntity.ok().build()); } @Override - public Mono> getConsumerGroup( - String clusterName, String consumerGroupId, ServerWebExchange exchange) { - return consumerGroupService.getConsumerGroupDetail(getCluster(clusterName), consumerGroupId) - .map(ConsumerGroupMapper::toDetailsDto) - .map(ResponseEntity::ok); - } - - - @Override - public Mono>> getConsumerGroups(String clusterName, + public Mono> getConsumerGroup(String clusterName, + String consumerGroupId, ServerWebExchange exchange) { - return consumerGroupService.getAllConsumerGroups(getCluster(clusterName)) - .map(Flux::fromIterable) - .map(f -> f.map(ConsumerGroupMapper::toDto)) - .map(ResponseEntity::ok) - .switchIfEmpty(Mono.just(ResponseEntity.notFound().build())); + var context = AccessContext.builder() + .cluster(clusterName) + .consumerGroup(consumerGroupId) + .consumerGroupActions(VIEW) + .operationName("getConsumerGroup") + .build(); + + return validateAccess(context) + .then(consumerGroupService.getConsumerGroupDetail(getCluster(clusterName), consumerGroupId) + .map(ConsumerGroupMapper::toDetailsDto) + .map(ResponseEntity::ok)) + .doOnEach(sig -> audit(context, sig)); } @Override - public Mono>> getTopicConsumerGroups( - String clusterName, String topicName, ServerWebExchange exchange) { - return consumerGroupService.getConsumerGroupsForTopic(getCluster(clusterName), topicName) - .map(Flux::fromIterable) - .map(f -> f.map(ConsumerGroupMapper::toDto)) - .map(ResponseEntity::ok) - .switchIfEmpty(Mono.just(ResponseEntity.notFound().build())); + public Mono>> getTopicConsumerGroups(String clusterName, + String topicName, + ServerWebExchange exchange) { + var context = AccessContext.builder() + .cluster(clusterName) + .topic(topicName) + .topicActions(TopicAction.VIEW) + .operationName("getTopicConsumerGroups") + .build(); + + Mono>> job = + consumerGroupService.getConsumerGroupsForTopic(getCluster(clusterName), topicName) + .flatMapMany(Flux::fromIterable) + .filterWhen(cg -> accessControlService.isConsumerGroupAccessible(cg.getGroupId(), clusterName)) + .map(ConsumerGroupMapper::toDto) + .collectList() + .map(Flux::fromIterable) + .map(ResponseEntity::ok) + .switchIfEmpty(Mono.just(ResponseEntity.notFound().build())); + + return validateAccess(context) + .then(job) + .doOnEach(sig -> audit(context, sig)); } @Override @@ -83,70 +113,93 @@ public Mono> getConsumerGroupsPage ConsumerGroupOrderingDTO orderBy, SortOrderDTO sortOrderDto, ServerWebExchange exchange) { - return consumerGroupService.getConsumerGroupsPage( - getCluster(clusterName), - Optional.ofNullable(page).filter(i -> i > 0).orElse(1), - Optional.ofNullable(perPage).filter(i -> i > 0).orElse(defaultConsumerGroupsPageSize), - search, - Optional.ofNullable(orderBy).orElse(ConsumerGroupOrderingDTO.NAME), - Optional.ofNullable(sortOrderDto).orElse(SortOrderDTO.ASC) - ) - .map(this::convertPage) - .map(ResponseEntity::ok); - } - private ConsumerGroupsPageResponseDTO convertPage(ConsumerGroupService.ConsumerGroupsPage - consumerGroupConsumerGroupsPage) { - return new ConsumerGroupsPageResponseDTO() - .pageCount(consumerGroupConsumerGroupsPage.getTotalPages()) - .consumerGroups(consumerGroupConsumerGroupsPage.getConsumerGroups() - .stream() - .map(ConsumerGroupMapper::toDto) - .collect(Collectors.toList())); + var context = AccessContext.builder() + .cluster(clusterName) + // consumer group access validation is within the service + .operationName("getConsumerGroupsPage") + .build(); + + return validateAccess(context).then( + consumerGroupService.getConsumerGroupsPage( + getCluster(clusterName), + Optional.ofNullable(page).filter(i -> i > 0).orElse(1), + Optional.ofNullable(perPage).filter(i -> i > 0).orElse(defaultConsumerGroupsPageSize), + search, + Optional.ofNullable(orderBy).orElse(ConsumerGroupOrderingDTO.NAME), + Optional.ofNullable(sortOrderDto).orElse(SortOrderDTO.ASC) + ) + .map(this::convertPage) + .map(ResponseEntity::ok) + ).doOnEach(sig -> audit(context, sig)); } @Override - public Mono> resetConsumerGroupOffsets(String clusterName, String group, - Mono - consumerGroupOffsetsReset, + public Mono> resetConsumerGroupOffsets(String clusterName, + String group, + Mono resetDto, ServerWebExchange exchange) { - return consumerGroupOffsetsReset.flatMap(reset -> { - var cluster = getCluster(clusterName); - switch (reset.getResetType()) { - case EARLIEST: - return offsetsResetService - .resetToEarliest(cluster, group, reset.getTopic(), reset.getPartitions()); - case LATEST: - return offsetsResetService - .resetToLatest(cluster, group, reset.getTopic(), reset.getPartitions()); - case TIMESTAMP: - if (reset.getResetToTimestamp() == null) { - return Mono.error( - new ValidationException( - "resetToTimestamp is required when TIMESTAMP reset type used" - ) - ); - } - return offsetsResetService - .resetToTimestamp(cluster, group, reset.getTopic(), reset.getPartitions(), - reset.getResetToTimestamp()); - case OFFSET: - if (CollectionUtils.isEmpty(reset.getPartitionsOffsets())) { + return resetDto.flatMap(reset -> { + var context = AccessContext.builder() + .cluster(clusterName) + .topic(reset.getTopic()) + .topicActions(TopicAction.VIEW) + .consumerGroupActions(RESET_OFFSETS) + .operationName("resetConsumerGroupOffsets") + .build(); + + Supplier> mono = () -> { + var cluster = getCluster(clusterName); + switch (reset.getResetType()) { + case EARLIEST: + return offsetsResetService + .resetToEarliest(cluster, group, reset.getTopic(), reset.getPartitions()); + case LATEST: + return offsetsResetService + .resetToLatest(cluster, group, reset.getTopic(), reset.getPartitions()); + case TIMESTAMP: + if (reset.getResetToTimestamp() == null) { + return Mono.error( + new ValidationException( + "resetToTimestamp is required when TIMESTAMP reset type used" + ) + ); + } + return offsetsResetService + .resetToTimestamp(cluster, group, reset.getTopic(), reset.getPartitions(), + reset.getResetToTimestamp()); + case OFFSET: + if (CollectionUtils.isEmpty(reset.getPartitionsOffsets())) { + return Mono.error( + new ValidationException( + "partitionsOffsets is required when OFFSET reset type used" + ) + ); + } + Map offsets = reset.getPartitionsOffsets().stream() + .collect(toMap(PartitionOffsetDTO::getPartition, PartitionOffsetDTO::getOffset)); + return offsetsResetService.resetToOffsets(cluster, group, reset.getTopic(), offsets); + default: return Mono.error( - new ValidationException( - "partitionsOffsets is required when OFFSET reset type used" - ) + new ValidationException("Unknown resetType " + reset.getResetType()) ); - } - Map offsets = reset.getPartitionsOffsets().stream() - .collect(toMap(PartitionOffsetDTO::getPartition, PartitionOffsetDTO::getOffset)); - return offsetsResetService.resetToOffsets(cluster, group, reset.getTopic(), offsets); - default: - return Mono.error( - new ValidationException("Unknown resetType " + reset.getResetType()) - ); - } + } + }; + + return validateAccess(context) + .then(mono.get()) + .doOnEach(sig -> audit(context, sig)); }).thenReturn(ResponseEntity.ok().build()); } + private ConsumerGroupsPageResponseDTO convertPage(ConsumerGroupService.ConsumerGroupsPage + consumerGroupConsumerGroupsPage) { + return new ConsumerGroupsPageResponseDTO() + .pageCount(consumerGroupConsumerGroupsPage.totalPages()) + .consumerGroups(consumerGroupConsumerGroupsPage.consumerGroups() + .stream() + .map(ConsumerGroupMapper::toDto) + .toList()); + } + } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KafkaConnectController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KafkaConnectController.java index 8011fe8e5fc..eb215b10992 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KafkaConnectController.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KafkaConnectController.java @@ -1,16 +1,26 @@ package com.provectus.kafka.ui.controller; +import static com.provectus.kafka.ui.model.ConnectorActionDTO.RESTART; +import static com.provectus.kafka.ui.model.ConnectorActionDTO.RESTART_ALL_TASKS; +import static com.provectus.kafka.ui.model.ConnectorActionDTO.RESTART_FAILED_TASKS; + import com.provectus.kafka.ui.api.KafkaConnectApi; import com.provectus.kafka.ui.model.ConnectDTO; import com.provectus.kafka.ui.model.ConnectorActionDTO; +import com.provectus.kafka.ui.model.ConnectorColumnsToSortDTO; import com.provectus.kafka.ui.model.ConnectorDTO; import com.provectus.kafka.ui.model.ConnectorPluginConfigValidationResponseDTO; import com.provectus.kafka.ui.model.ConnectorPluginDTO; import com.provectus.kafka.ui.model.FullConnectorInfoDTO; import com.provectus.kafka.ui.model.NewConnectorDTO; +import com.provectus.kafka.ui.model.SortOrderDTO; import com.provectus.kafka.ui.model.TaskDTO; +import com.provectus.kafka.ui.model.rbac.AccessContext; +import com.provectus.kafka.ui.model.rbac.permission.ConnectAction; import com.provectus.kafka.ui.service.KafkaConnectService; +import java.util.Comparator; import java.util.Map; +import java.util.Set; import javax.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -24,43 +34,92 @@ @RequiredArgsConstructor @Slf4j public class KafkaConnectController extends AbstractController implements KafkaConnectApi { + private static final Set RESTART_ACTIONS + = Set.of(RESTART, RESTART_FAILED_TASKS, RESTART_ALL_TASKS); + private static final String CONNECTOR_NAME = "connectorName"; + private final KafkaConnectService kafkaConnectService; @Override public Mono>> getConnects(String clusterName, ServerWebExchange exchange) { - return kafkaConnectService.getConnects(getCluster(clusterName)).map(ResponseEntity::ok); + + Flux availableConnects = kafkaConnectService.getConnects(getCluster(clusterName)) + .filterWhen(dto -> accessControlService.isConnectAccessible(dto, clusterName)); + + return Mono.just(ResponseEntity.ok(availableConnects)); } @Override public Mono>> getConnectors(String clusterName, String connectName, ServerWebExchange exchange) { - var connectors = kafkaConnectService.getConnectors(getCluster(clusterName), connectName); - return Mono.just(ResponseEntity.ok(connectors)); + + var context = AccessContext.builder() + .cluster(clusterName) + .connect(connectName) + .connectActions(ConnectAction.VIEW) + .operationName("getConnectors") + .build(); + + return validateAccess(context) + .thenReturn(ResponseEntity.ok(kafkaConnectService.getConnectorNames(getCluster(clusterName), connectName))) + .doOnEach(sig -> audit(context, sig)); } @Override public Mono> createConnector(String clusterName, String connectName, @Valid Mono connector, ServerWebExchange exchange) { - return kafkaConnectService.createConnector(getCluster(clusterName), connectName, connector) - .map(ResponseEntity::ok); + + var context = AccessContext.builder() + .cluster(clusterName) + .connect(connectName) + .connectActions(ConnectAction.VIEW, ConnectAction.CREATE) + .operationName("createConnector") + .build(); + + return validateAccess(context).then( + kafkaConnectService.createConnector(getCluster(clusterName), connectName, connector) + .map(ResponseEntity::ok) + ).doOnEach(sig -> audit(context, sig)); } @Override public Mono> getConnector(String clusterName, String connectName, String connectorName, ServerWebExchange exchange) { - return kafkaConnectService.getConnector(getCluster(clusterName), connectName, connectorName) - .map(ResponseEntity::ok); + + var context = AccessContext.builder() + .cluster(clusterName) + .connect(connectName) + .connectActions(ConnectAction.VIEW) + .connector(connectorName) + .operationName("getConnector") + .build(); + + return validateAccess(context).then( + kafkaConnectService.getConnector(getCluster(clusterName), connectName, connectorName) + .map(ResponseEntity::ok) + ).doOnEach(sig -> audit(context, sig)); } @Override public Mono> deleteConnector(String clusterName, String connectName, String connectorName, ServerWebExchange exchange) { - return kafkaConnectService.deleteConnector(getCluster(clusterName), connectName, connectorName) - .map(ResponseEntity::ok); + + var context = AccessContext.builder() + .cluster(clusterName) + .connect(connectName) + .connectActions(ConnectAction.VIEW, ConnectAction.EDIT) + .operationName("deleteConnector") + .operationParams(Map.of(CONNECTOR_NAME, connectName)) + .build(); + + return validateAccess(context).then( + kafkaConnectService.deleteConnector(getCluster(clusterName), connectName, connectorName) + .map(ResponseEntity::ok) + ).doOnEach(sig -> audit(context, sig)); } @@ -68,10 +127,27 @@ public Mono> deleteConnector(String clusterName, String con public Mono>> getAllConnectors( String clusterName, String search, + ConnectorColumnsToSortDTO orderBy, + SortOrderDTO sortOrder, ServerWebExchange exchange ) { - return Mono.just(ResponseEntity.ok( - kafkaConnectService.getAllConnectors(getCluster(clusterName), search))); + var context = AccessContext.builder() + .cluster(clusterName) + .connectActions(ConnectAction.VIEW, ConnectAction.EDIT) + .operationName("getAllConnectors") + .build(); + + var comparator = sortOrder == null || sortOrder.equals(SortOrderDTO.ASC) + ? getConnectorsComparator(orderBy) + : getConnectorsComparator(orderBy).reversed(); + + Flux job = kafkaConnectService.getAllConnectors(getCluster(clusterName), search) + .filterWhen(dto -> accessControlService.isConnectAccessible(dto.getConnect(), clusterName)) + .filterWhen(dto -> accessControlService.isConnectorAccessible(dto.getConnect(), dto.getName(), clusterName)) + .sort(comparator); + + return Mono.just(ResponseEntity.ok(job)) + .doOnEach(sig -> audit(context, sig)); } @Override @@ -79,20 +155,40 @@ public Mono>> getConnectorConfig(String clust String connectName, String connectorName, ServerWebExchange exchange) { - return kafkaConnectService - .getConnectorConfig(getCluster(clusterName), connectName, connectorName) - .map(ResponseEntity::ok); + + var context = AccessContext.builder() + .cluster(clusterName) + .connect(connectName) + .connectActions(ConnectAction.VIEW) + .operationName("getConnectorConfig") + .build(); + + return validateAccess(context).then( + kafkaConnectService + .getConnectorConfig(getCluster(clusterName), connectName, connectorName) + .map(ResponseEntity::ok) + ).doOnEach(sig -> audit(context, sig)); } @Override - public Mono> setConnectorConfig(String clusterName, - String connectName, + public Mono> setConnectorConfig(String clusterName, String connectName, String connectorName, - @Valid Mono requestBody, + Mono> requestBody, ServerWebExchange exchange) { - return kafkaConnectService - .setConnectorConfig(getCluster(clusterName), connectName, connectorName, requestBody) - .map(ResponseEntity::ok); + + var context = AccessContext.builder() + .cluster(clusterName) + .connect(connectName) + .connectActions(ConnectAction.VIEW, ConnectAction.EDIT) + .operationName("setConnectorConfig") + .operationParams(Map.of(CONNECTOR_NAME, connectorName)) + .build(); + + return validateAccess(context).then( + kafkaConnectService + .setConnectorConfig(getCluster(clusterName), connectName, connectorName, requestBody) + .map(ResponseEntity::ok)) + .doOnEach(sig -> audit(context, sig)); } @Override @@ -100,9 +196,26 @@ public Mono> updateConnectorState(String clusterName, Strin String connectorName, ConnectorActionDTO action, ServerWebExchange exchange) { - return kafkaConnectService - .updateConnectorState(getCluster(clusterName), connectName, connectorName, action) - .map(ResponseEntity::ok); + ConnectAction[] connectActions; + if (RESTART_ACTIONS.contains(action)) { + connectActions = new ConnectAction[] {ConnectAction.VIEW, ConnectAction.RESTART}; + } else { + connectActions = new ConnectAction[] {ConnectAction.VIEW, ConnectAction.EDIT}; + } + + var context = AccessContext.builder() + .cluster(clusterName) + .connect(connectName) + .connectActions(connectActions) + .operationName("updateConnectorState") + .operationParams(Map.of(CONNECTOR_NAME, connectorName)) + .build(); + + return validateAccess(context).then( + kafkaConnectService + .updateConnectorState(getCluster(clusterName), connectName, connectorName, action) + .map(ResponseEntity::ok) + ).doOnEach(sig -> audit(context, sig)); } @Override @@ -110,36 +223,79 @@ public Mono>> getConnectorTasks(String clusterName, String connectName, String connectorName, ServerWebExchange exchange) { - return Mono.just(ResponseEntity - .ok(kafkaConnectService - .getConnectorTasks(getCluster(clusterName), connectName, connectorName))); + var context = AccessContext.builder() + .cluster(clusterName) + .connect(connectName) + .connectActions(ConnectAction.VIEW) + .operationName("getConnectorTasks") + .operationParams(Map.of(CONNECTOR_NAME, connectorName)) + .build(); + + return validateAccess(context).thenReturn( + ResponseEntity + .ok(kafkaConnectService + .getConnectorTasks(getCluster(clusterName), connectName, connectorName)) + ).doOnEach(sig -> audit(context, sig)); } @Override public Mono> restartConnectorTask(String clusterName, String connectName, String connectorName, Integer taskId, ServerWebExchange exchange) { - return kafkaConnectService - .restartConnectorTask(getCluster(clusterName), connectName, connectorName, taskId) - .map(ResponseEntity::ok); + + var context = AccessContext.builder() + .cluster(clusterName) + .connect(connectName) + .connectActions(ConnectAction.VIEW, ConnectAction.RESTART) + .operationName("restartConnectorTask") + .operationParams(Map.of(CONNECTOR_NAME, connectorName)) + .build(); + + return validateAccess(context).then( + kafkaConnectService + .restartConnectorTask(getCluster(clusterName), connectName, connectorName, taskId) + .map(ResponseEntity::ok) + ).doOnEach(sig -> audit(context, sig)); } @Override public Mono>> getConnectorPlugins( String clusterName, String connectName, ServerWebExchange exchange) { - return kafkaConnectService - .getConnectorPlugins(getCluster(clusterName), connectName) - .map(ResponseEntity::ok); + + var context = AccessContext.builder() + .cluster(clusterName) + .connect(connectName) + .connectActions(ConnectAction.VIEW) + .operationName("getConnectorPlugins") + .build(); + + return validateAccess(context).then( + Mono.just( + ResponseEntity.ok( + kafkaConnectService.getConnectorPlugins(getCluster(clusterName), connectName))) + ).doOnEach(sig -> audit(context, sig)); } @Override - public Mono> - validateConnectorPluginConfig( - String clusterName, String connectName, String pluginName, @Valid Mono requestBody, + public Mono> validateConnectorPluginConfig( + String clusterName, String connectName, String pluginName, @Valid Mono> requestBody, ServerWebExchange exchange) { return kafkaConnectService .validateConnectorPluginConfig( getCluster(clusterName), connectName, pluginName, requestBody) .map(ResponseEntity::ok); } + + private Comparator getConnectorsComparator(ConnectorColumnsToSortDTO orderBy) { + var defaultComparator = Comparator.comparing(FullConnectorInfoDTO::getName); + if (orderBy == null) { + return defaultComparator; + } + return switch (orderBy) { + case CONNECT -> Comparator.comparing(FullConnectorInfoDTO::getConnect); + case TYPE -> Comparator.comparing(FullConnectorInfoDTO::getType); + case STATUS -> Comparator.comparing(fullConnectorInfoDTO -> fullConnectorInfoDTO.getStatus().getState()); + default -> defaultComparator; + }; + } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KsqlController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KsqlController.java index 62dc24fab28..c15f36488ec 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KsqlController.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KsqlController.java @@ -1,15 +1,14 @@ package com.provectus.kafka.ui.controller; import com.provectus.kafka.ui.api.KsqlApi; -import com.provectus.kafka.ui.model.KsqlCommandDTO; -import com.provectus.kafka.ui.model.KsqlCommandResponseDTO; import com.provectus.kafka.ui.model.KsqlCommandV2DTO; import com.provectus.kafka.ui.model.KsqlCommandV2ResponseDTO; import com.provectus.kafka.ui.model.KsqlResponseDTO; import com.provectus.kafka.ui.model.KsqlStreamDescriptionDTO; import com.provectus.kafka.ui.model.KsqlTableDescriptionDTO; import com.provectus.kafka.ui.model.KsqlTableResponseDTO; -import com.provectus.kafka.ui.service.KsqlService; +import com.provectus.kafka.ui.model.rbac.AccessContext; +import com.provectus.kafka.ui.model.rbac.permission.KsqlAction; import com.provectus.kafka.ui.service.ksql.KsqlServiceV2; import java.util.List; import java.util.Map; @@ -22,60 +21,83 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; - @RestController @RequiredArgsConstructor @Slf4j public class KsqlController extends AbstractController implements KsqlApi { - private final KsqlService ksqlService; - private final KsqlServiceV2 ksqlServiceV2; - @Override - public Mono> executeKsqlCommand(String clusterName, - Mono - ksqlCommand, - ServerWebExchange exchange) { - return ksqlService.executeKsqlCommand(getCluster(clusterName), ksqlCommand) - .map(ResponseEntity::ok); - } + private final KsqlServiceV2 ksqlServiceV2; @Override public Mono> executeKsql(String clusterName, - Mono - ksqlCommand2Dto, + Mono ksqlCmdDo, ServerWebExchange exchange) { - return ksqlCommand2Dto.map(dto -> { - var id = ksqlServiceV2.registerCommand( - getCluster(clusterName), - dto.getKsql(), - Optional.ofNullable(dto.getStreamsProperties()).orElse(Map.of())); - return new KsqlCommandV2ResponseDTO().pipeId(id); - }).map(ResponseEntity::ok); + return ksqlCmdDo.flatMap( + command -> { + var context = AccessContext.builder() + .cluster(clusterName) + .ksqlActions(KsqlAction.EXECUTE) + .operationName("executeKsql") + .operationParams(command) + .build(); + return validateAccess(context).thenReturn( + new KsqlCommandV2ResponseDTO().pipeId( + ksqlServiceV2.registerCommand( + getCluster(clusterName), + command.getKsql(), + Optional.ofNullable(command.getStreamsProperties()).orElse(Map.of())))) + .doOnEach(sig -> audit(context, sig)); + } + ) + .map(ResponseEntity::ok); } @Override public Mono>> openKsqlResponsePipe(String clusterName, String pipeId, ServerWebExchange exchange) { - return Mono.just( + var context = AccessContext.builder() + .cluster(clusterName) + .ksqlActions(KsqlAction.EXECUTE) + .operationName("openKsqlResponsePipe") + .build(); + + return validateAccess(context).thenReturn( ResponseEntity.ok(ksqlServiceV2.execute(pipeId) .map(table -> new KsqlResponseDTO() .table( new KsqlTableResponseDTO() .header(table.getHeader()) .columnNames(table.getColumnNames()) - .values((List>) ((List) (table.getValues()))))))); + .values((List>) ((List) (table.getValues())))))) + ); } @Override public Mono>> listStreams(String clusterName, - ServerWebExchange exchange) { - return Mono.just(ResponseEntity.ok(ksqlServiceV2.listStreams(getCluster(clusterName)))); + ServerWebExchange exchange) { + var context = AccessContext.builder() + .cluster(clusterName) + .ksqlActions(KsqlAction.EXECUTE) + .operationName("listStreams") + .build(); + + return validateAccess(context) + .thenReturn(ResponseEntity.ok(ksqlServiceV2.listStreams(getCluster(clusterName)))) + .doOnEach(sig -> audit(context, sig)); } @Override public Mono>> listTables(String clusterName, ServerWebExchange exchange) { - return Mono.just(ResponseEntity.ok(ksqlServiceV2.listTables(getCluster(clusterName)))); + var context = AccessContext.builder() + .cluster(clusterName) + .ksqlActions(KsqlAction.EXECUTE) + .operationName("listTables") + .build(); + + return validateAccess(context) + .thenReturn(ResponseEntity.ok(ksqlServiceV2.listTables(getCluster(clusterName)))) + .doOnEach(sig -> audit(context, sig)); } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/MessagesController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/MessagesController.java index 4a43523e3d6..50b36e14703 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/MessagesController.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/MessagesController.java @@ -1,23 +1,38 @@ package com.provectus.kafka.ui.controller; +import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.MESSAGES_DELETE; +import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.MESSAGES_PRODUCE; +import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.MESSAGES_READ; +import static com.provectus.kafka.ui.serde.api.Serde.Target.KEY; +import static com.provectus.kafka.ui.serde.api.Serde.Target.VALUE; import static java.util.stream.Collectors.toMap; import com.provectus.kafka.ui.api.MessagesApi; +import com.provectus.kafka.ui.exception.ValidationException; import com.provectus.kafka.ui.model.ConsumerPosition; import com.provectus.kafka.ui.model.CreateTopicMessageDTO; import com.provectus.kafka.ui.model.MessageFilterTypeDTO; import com.provectus.kafka.ui.model.SeekDirectionDTO; import com.provectus.kafka.ui.model.SeekTypeDTO; +import com.provectus.kafka.ui.model.SerdeUsageDTO; +import com.provectus.kafka.ui.model.SmartFilterTestExecutionDTO; +import com.provectus.kafka.ui.model.SmartFilterTestExecutionResultDTO; import com.provectus.kafka.ui.model.TopicMessageEventDTO; -import com.provectus.kafka.ui.model.TopicMessageSchemaDTO; +import com.provectus.kafka.ui.model.TopicSerdeSuggestionDTO; +import com.provectus.kafka.ui.model.rbac.AccessContext; +import com.provectus.kafka.ui.model.rbac.permission.AuditAction; +import com.provectus.kafka.ui.model.rbac.permission.TopicAction; +import com.provectus.kafka.ui.service.DeserializationService; import com.provectus.kafka.ui.service.MessagesService; -import com.provectus.kafka.ui.service.TopicsService; +import com.provectus.kafka.ui.util.DynamicConfigOperations; import java.util.List; import java.util.Map; import java.util.Optional; +import javax.annotation.Nullable; import javax.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.kafka.common.TopicPartition; import org.springframework.http.ResponseEntity; @@ -25,73 +40,124 @@ import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; @RestController @RequiredArgsConstructor @Slf4j public class MessagesController extends AbstractController implements MessagesApi { - private static final int MAX_LOAD_RECORD_LIMIT = 100; - private static final int DEFAULT_LOAD_RECORD_LIMIT = 20; - private final MessagesService messagesService; - private final TopicsService topicsService; + private final DeserializationService deserializationService; + private final DynamicConfigOperations dynamicConfigOperations; @Override public Mono> deleteTopicMessages( String clusterName, String topicName, @Valid List partitions, ServerWebExchange exchange) { - return messagesService.deleteTopicMessages( - getCluster(clusterName), - topicName, - Optional.ofNullable(partitions).orElse(List.of()) - ).thenReturn(ResponseEntity.ok().build()); + + var context = AccessContext.builder() + .cluster(clusterName) + .topic(topicName) + .topicActions(MESSAGES_DELETE) + .build(); + + return validateAccess(context).>then( + messagesService.deleteTopicMessages( + getCluster(clusterName), + topicName, + Optional.ofNullable(partitions).orElse(List.of()) + ).thenReturn(ResponseEntity.ok().build()) + ).doOnEach(sig -> audit(context, sig)); + } + + @Override + public Mono> executeSmartFilterTest( + Mono smartFilterTestExecutionDto, ServerWebExchange exchange) { + return smartFilterTestExecutionDto + .map(MessagesService::execSmartFilterTest) + .map(ResponseEntity::ok); } @Override - public Mono>> getTopicMessages( - String clusterName, String topicName, SeekTypeDTO seekType, List seekTo, - Integer limit, String q, MessageFilterTypeDTO filterQueryType, - SeekDirectionDTO seekDirection, ServerWebExchange exchange) { + public Mono>> getTopicMessages(String clusterName, + String topicName, + SeekTypeDTO seekType, + List seekTo, + Integer limit, + String q, + MessageFilterTypeDTO filterQueryType, + SeekDirectionDTO seekDirection, + String keySerde, + String valueSerde, + ServerWebExchange exchange) { + var contextBuilder = AccessContext.builder() + .cluster(clusterName) + .topic(topicName) + .topicActions(MESSAGES_READ) + .operationName("getTopicMessages"); + + if (StringUtils.isNoneEmpty(q) && MessageFilterTypeDTO.GROOVY_SCRIPT == filterQueryType) { + dynamicConfigOperations.checkIfFilteringGroovyEnabled(); + } + + if (auditService.isAuditTopic(getCluster(clusterName), topicName)) { + contextBuilder.auditActions(AuditAction.VIEW); + } + + seekType = seekType != null ? seekType : SeekTypeDTO.BEGINNING; + seekDirection = seekDirection != null ? seekDirection : SeekDirectionDTO.FORWARD; + filterQueryType = filterQueryType != null ? filterQueryType : MessageFilterTypeDTO.STRING_CONTAINS; + var positions = new ConsumerPosition( - seekType != null ? seekType : SeekTypeDTO.BEGINNING, - parseSeekTo(topicName, seekTo), - seekDirection + seekType, + topicName, + parseSeekTo(topicName, seekType, seekTo) ); - int recordsLimit = Optional.ofNullable(limit) - .map(s -> Math.min(s, MAX_LOAD_RECORD_LIMIT)) - .orElse(DEFAULT_LOAD_RECORD_LIMIT); - return Mono.just( + Mono>> job = Mono.just( ResponseEntity.ok( messagesService.loadMessages( - getCluster(clusterName), topicName, positions, q, filterQueryType, recordsLimit) + getCluster(clusterName), topicName, positions, q, filterQueryType, + limit, seekDirection, keySerde, valueSerde) ) ); - } - @Override - public Mono> getTopicSchema( - String clusterName, String topicName, ServerWebExchange exchange) { - return Mono.just(topicsService.getTopicSchema(getCluster(clusterName), topicName)) - .map(ResponseEntity::ok); + var context = contextBuilder.build(); + return validateAccess(context) + .then(job) + .doOnEach(sig -> audit(context, sig)); } @Override public Mono> sendTopicMessages( String clusterName, String topicName, @Valid Mono createTopicMessage, ServerWebExchange exchange) { - return createTopicMessage.flatMap(msg -> - messagesService.sendMessage(getCluster(clusterName), topicName, msg).then() - ).map(ResponseEntity::ok); + + var context = AccessContext.builder() + .cluster(clusterName) + .topic(topicName) + .topicActions(MESSAGES_PRODUCE) + .operationName("sendTopicMessages") + .build(); + + return validateAccess(context).then( + createTopicMessage.flatMap(msg -> + messagesService.sendMessage(getCluster(clusterName), topicName, msg).then() + ).map(ResponseEntity::ok) + ).doOnEach(sig -> audit(context, sig)); } /** * The format is [partition]::[offset] for specifying offsets * or [partition]::[timestamp in millis] for specifying timestamps. */ - private Map parseSeekTo(String topic, List seekTo) { + @Nullable + private Map parseSeekTo(String topic, SeekTypeDTO seekType, List seekTo) { if (seekTo == null || seekTo.isEmpty()) { - return Map.of(); + if (seekType == SeekTypeDTO.LATEST || seekType == SeekTypeDTO.BEGINNING) { + return null; + } + throw new ValidationException("seekTo should be set if seekType is " + seekType); } return seekTo.stream() .map(p -> { @@ -109,4 +175,34 @@ private Map parseSeekTo(String topic, List seekTo) .collect(toMap(Pair::getKey, Pair::getValue)); } + @Override + public Mono> getSerdes(String clusterName, + String topicName, + SerdeUsageDTO use, + ServerWebExchange exchange) { + var context = AccessContext.builder() + .cluster(clusterName) + .topic(topicName) + .topicActions(TopicAction.VIEW) + .operationName("getSerdes") + .build(); + + TopicSerdeSuggestionDTO dto = new TopicSerdeSuggestionDTO() + .key(use == SerdeUsageDTO.SERIALIZE + ? deserializationService.getSerdesForSerialize(getCluster(clusterName), topicName, KEY) + : deserializationService.getSerdesForDeserialize(getCluster(clusterName), topicName, KEY)) + .value(use == SerdeUsageDTO.SERIALIZE + ? deserializationService.getSerdesForSerialize(getCluster(clusterName), topicName, VALUE) + : deserializationService.getSerdesForDeserialize(getCluster(clusterName), topicName, VALUE)); + + return validateAccess(context).then( + Mono.just(dto) + .subscribeOn(Schedulers.boundedElastic()) + .map(ResponseEntity::ok) + ); + } + + + + } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/SchemasController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/SchemasController.java index 55187903a65..481ff4c75b5 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/SchemasController.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/SchemasController.java @@ -2,17 +2,19 @@ import com.provectus.kafka.ui.api.SchemasApi; import com.provectus.kafka.ui.exception.ValidationException; -import com.provectus.kafka.ui.mapper.ClusterMapper; +import com.provectus.kafka.ui.mapper.KafkaSrMapper; +import com.provectus.kafka.ui.mapper.KafkaSrMapperImpl; import com.provectus.kafka.ui.model.CompatibilityCheckResponseDTO; import com.provectus.kafka.ui.model.CompatibilityLevelDTO; import com.provectus.kafka.ui.model.KafkaCluster; import com.provectus.kafka.ui.model.NewSchemaSubjectDTO; import com.provectus.kafka.ui.model.SchemaSubjectDTO; import com.provectus.kafka.ui.model.SchemaSubjectsResponseDTO; +import com.provectus.kafka.ui.model.rbac.AccessContext; +import com.provectus.kafka.ui.model.rbac.permission.SchemaAction; import com.provectus.kafka.ui.service.SchemaRegistryService; -import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; +import java.util.Map; import javax.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -30,14 +32,14 @@ public class SchemasController extends AbstractController implements SchemasApi private static final Integer DEFAULT_PAGE_SIZE = 25; - private final ClusterMapper mapper; + private final KafkaSrMapper kafkaSrMapper = new KafkaSrMapperImpl(); private final SchemaRegistryService schemaRegistryService; @Override protected KafkaCluster getCluster(String clusterName) { var c = super.getCluster(clusterName); - if (c.getSchemaRegistry() == null) { + if (c.getSchemaRegistryClient() == null) { throw new ValidationException("Schema Registry is not set for cluster " + clusterName); } return c; @@ -45,74 +47,163 @@ protected KafkaCluster getCluster(String clusterName) { @Override public Mono> checkSchemaCompatibility( - String clusterName, String subject, @Valid Mono newSchemaSubject, + String clusterName, String subject, @Valid Mono newSchemaSubjectMono, ServerWebExchange exchange) { - return schemaRegistryService.checksSchemaCompatibility( - getCluster(clusterName), subject, newSchemaSubject) - .map(mapper::toCompatibilityCheckResponse) - .map(ResponseEntity::ok); + var context = AccessContext.builder() + .cluster(clusterName) + .schema(subject) + .schemaActions(SchemaAction.VIEW) + .operationName("checkSchemaCompatibility") + .build(); + + return validateAccess(context).then( + newSchemaSubjectMono.flatMap(subjectDTO -> + schemaRegistryService.checksSchemaCompatibility( + getCluster(clusterName), + subject, + kafkaSrMapper.fromDto(subjectDTO) + )) + .map(kafkaSrMapper::toDto) + .map(ResponseEntity::ok) + ).doOnEach(sig -> audit(context, sig)); } @Override public Mono> createNewSchema( - String clusterName, @Valid Mono newSchemaSubject, + String clusterName, @Valid Mono newSchemaSubjectMono, ServerWebExchange exchange) { - return schemaRegistryService - .registerNewSchema(getCluster(clusterName), newSchemaSubject) - .map(ResponseEntity::ok); + var context = AccessContext.builder() + .cluster(clusterName) + .schemaActions(SchemaAction.CREATE) + .operationName("createNewSchema") + .build(); + + return validateAccess(context).then( + newSchemaSubjectMono.flatMap(newSubject -> + schemaRegistryService.registerNewSchema( + getCluster(clusterName), + newSubject.getSubject(), + kafkaSrMapper.fromDto(newSubject) + ) + ).map(kafkaSrMapper::toDto) + .map(ResponseEntity::ok) + ).doOnEach(sig -> audit(context, sig)); } @Override public Mono> deleteLatestSchema( String clusterName, String subject, ServerWebExchange exchange) { - return schemaRegistryService.deleteLatestSchemaSubject(getCluster(clusterName), subject) - .thenReturn(ResponseEntity.ok().build()); + var context = AccessContext.builder() + .cluster(clusterName) + .schema(subject) + .schemaActions(SchemaAction.DELETE) + .operationName("deleteLatestSchema") + .build(); + + return validateAccess(context).then( + schemaRegistryService.deleteLatestSchemaSubject(getCluster(clusterName), subject) + .doOnEach(sig -> audit(context, sig)) + .thenReturn(ResponseEntity.ok().build()) + ); } @Override public Mono> deleteSchema( - String clusterName, String subjectName, ServerWebExchange exchange) { - return schemaRegistryService.deleteSchemaSubjectEntirely(getCluster(clusterName), subjectName) - .thenReturn(ResponseEntity.ok().build()); + String clusterName, String subject, ServerWebExchange exchange) { + var context = AccessContext.builder() + .cluster(clusterName) + .schema(subject) + .schemaActions(SchemaAction.DELETE) + .operationName("deleteSchema") + .build(); + + return validateAccess(context).then( + schemaRegistryService.deleteSchemaSubjectEntirely(getCluster(clusterName), subject) + .doOnEach(sig -> audit(context, sig)) + .thenReturn(ResponseEntity.ok().build()) + ); } @Override public Mono> deleteSchemaByVersion( String clusterName, String subjectName, Integer version, ServerWebExchange exchange) { - return schemaRegistryService.deleteSchemaSubjectByVersion(getCluster(clusterName), subjectName, version) - .thenReturn(ResponseEntity.ok().build()); + var context = AccessContext.builder() + .cluster(clusterName) + .schema(subjectName) + .schemaActions(SchemaAction.DELETE) + .operationName("deleteSchemaByVersion") + .build(); + + return validateAccess(context).then( + schemaRegistryService.deleteSchemaSubjectByVersion(getCluster(clusterName), subjectName, version) + .doOnEach(sig -> audit(context, sig)) + .thenReturn(ResponseEntity.ok().build()) + ); } @Override public Mono>> getAllVersionsBySubject( String clusterName, String subjectName, ServerWebExchange exchange) { + var context = AccessContext.builder() + .cluster(clusterName) + .schema(subjectName) + .schemaActions(SchemaAction.VIEW) + .operationName("getAllVersionsBySubject") + .build(); + Flux schemas = - schemaRegistryService.getAllVersionsBySubject(getCluster(clusterName), subjectName); - return Mono.just(ResponseEntity.ok(schemas)); + schemaRegistryService.getAllVersionsBySubject(getCluster(clusterName), subjectName) + .map(kafkaSrMapper::toDto); + + return validateAccess(context) + .thenReturn(ResponseEntity.ok(schemas)) + .doOnEach(sig -> audit(context, sig)); } @Override public Mono> getGlobalSchemaCompatibilityLevel( String clusterName, ServerWebExchange exchange) { return schemaRegistryService.getGlobalSchemaCompatibilityLevel(getCluster(clusterName)) - .map(mapper::toCompatibilityLevelDto) + .map(c -> new CompatibilityLevelDTO().compatibility(kafkaSrMapper.toDto(c))) .map(ResponseEntity::ok) .defaultIfEmpty(ResponseEntity.notFound().build()); } @Override - public Mono> getLatestSchema(String clusterName, String subject, + public Mono> getLatestSchema(String clusterName, + String subject, ServerWebExchange exchange) { - return schemaRegistryService.getLatestSchemaVersionBySubject(getCluster(clusterName), subject) - .map(ResponseEntity::ok); + var context = AccessContext.builder() + .cluster(clusterName) + .schema(subject) + .schemaActions(SchemaAction.VIEW) + .operationName("getLatestSchema") + .build(); + + return validateAccess(context).then( + schemaRegistryService.getLatestSchemaVersionBySubject(getCluster(clusterName), subject) + .map(kafkaSrMapper::toDto) + .map(ResponseEntity::ok) + ).doOnEach(sig -> audit(context, sig)); } @Override public Mono> getSchemaByVersion( String clusterName, String subject, Integer version, ServerWebExchange exchange) { - return schemaRegistryService.getSchemaSubjectByVersion( - getCluster(clusterName), subject, version) - .map(ResponseEntity::ok); + var context = AccessContext.builder() + .cluster(clusterName) + .schema(subject) + .schemaActions(SchemaAction.VIEW) + .operationName("getSchemaByVersion") + .operationParams(Map.of("subject", subject, "version", version)) + .build(); + + return validateAccess(context).then( + schemaRegistryService.getSchemaSubjectByVersion( + getCluster(clusterName), subject, version) + .map(kafkaSrMapper::toDto) + .map(ResponseEntity::ok) + ).doOnEach(sig -> audit(context, sig)); } @Override @@ -121,43 +212,79 @@ public Mono> getSchemas(String cluster @Valid Integer perPage, @Valid String search, ServerWebExchange serverWebExchange) { + var context = AccessContext.builder() + .cluster(clusterName) + .operationName("getSchemas") + .build(); + return schemaRegistryService .getAllSubjectNames(getCluster(clusterName)) + .flatMapIterable(l -> l) + .filterWhen(schema -> accessControlService.isSchemaAccessible(schema, clusterName)) + .collectList() .flatMap(subjects -> { int pageSize = perPage != null && perPage > 0 ? perPage : DEFAULT_PAGE_SIZE; int subjectToSkip = ((pageNum != null && pageNum > 0 ? pageNum : 1) - 1) * pageSize; - List filteredSubjects = Arrays.stream(subjects) + List filteredSubjects = subjects + .stream() .filter(subj -> search == null || StringUtils.containsIgnoreCase(subj, search)) - .sorted() - .collect(Collectors.toList()); + .sorted().toList(); var totalPages = (filteredSubjects.size() / pageSize) + (filteredSubjects.size() % pageSize == 0 ? 0 : 1); List subjectsToRender = filteredSubjects.stream() .skip(subjectToSkip) .limit(pageSize) - .collect(Collectors.toList()); + .toList(); return schemaRegistryService.getAllLatestVersionSchemas(getCluster(clusterName), subjectsToRender) - .map(a -> new SchemaSubjectsResponseDTO().pageCount(totalPages).schemas(a)); - }).map(ResponseEntity::ok); + .map(subjs -> subjs.stream().map(kafkaSrMapper::toDto).toList()) + .map(subjs -> new SchemaSubjectsResponseDTO().pageCount(totalPages).schemas(subjs)); + }).map(ResponseEntity::ok) + .doOnEach(sig -> audit(context, sig)); } @Override public Mono> updateGlobalSchemaCompatibilityLevel( - String clusterName, @Valid Mono compatibilityLevel, + String clusterName, @Valid Mono compatibilityLevelMono, ServerWebExchange exchange) { - log.info("Updating schema compatibility globally"); - return schemaRegistryService.updateSchemaCompatibility( - getCluster(clusterName), compatibilityLevel) - .map(ResponseEntity::ok); + var context = AccessContext.builder() + .cluster(clusterName) + .schemaActions(SchemaAction.MODIFY_GLOBAL_COMPATIBILITY) + .operationName("updateGlobalSchemaCompatibilityLevel") + .build(); + + return validateAccess(context).then( + compatibilityLevelMono + .flatMap(compatibilityLevelDTO -> + schemaRegistryService.updateGlobalSchemaCompatibility( + getCluster(clusterName), + kafkaSrMapper.fromDto(compatibilityLevelDTO.getCompatibility()) + )) + .doOnEach(sig -> audit(context, sig)) + .thenReturn(ResponseEntity.ok().build()) + ); } @Override public Mono> updateSchemaCompatibilityLevel( - String clusterName, String subject, @Valid Mono compatibilityLevel, + String clusterName, String subject, @Valid Mono compatibilityLevelMono, ServerWebExchange exchange) { - log.info("Updating schema compatibility for subject: {}", subject); - return schemaRegistryService.updateSchemaCompatibility( - getCluster(clusterName), subject, compatibilityLevel) - .map(ResponseEntity::ok); + var context = AccessContext.builder() + .cluster(clusterName) + .schemaActions(SchemaAction.EDIT) + .operationName("updateSchemaCompatibilityLevel") + .operationParams(Map.of("subject", subject)) + .build(); + + return validateAccess(context).then( + compatibilityLevelMono + .flatMap(compatibilityLevelDTO -> + schemaRegistryService.updateSchemaCompatibility( + getCluster(clusterName), + subject, + kafkaSrMapper.fromDto(compatibilityLevelDTO.getCompatibility()) + )) + .doOnEach(sig -> audit(context, sig)) + .thenReturn(ResponseEntity.ok().build()) + ); } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/StaticController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/StaticController.java index c2d6a10b366..72138c8010c 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/StaticController.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/StaticController.java @@ -20,21 +20,30 @@ public class StaticController { @Value("classpath:static/index.html") private Resource indexFile; + @Value("classpath:static/manifest.json") + private Resource manifestFile; + private final AtomicReference renderedIndexFile = new AtomicReference<>(); + private final AtomicReference renderedManifestFile = new AtomicReference<>(); @GetMapping(value = "/index.html", produces = {"text/html"}) public Mono> getIndex(ServerWebExchange exchange) { - return Mono.just(ResponseEntity.ok(getRenderedIndexFile(exchange))); + return Mono.just(ResponseEntity.ok(getRenderedFile(exchange, renderedIndexFile, indexFile))); + } + + @GetMapping(value = "/manifest.json", produces = {"application/json"}) + public Mono> getManifest(ServerWebExchange exchange) { + return Mono.just(ResponseEntity.ok(getRenderedFile(exchange, renderedManifestFile, manifestFile))); } - public String getRenderedIndexFile(ServerWebExchange exchange) { - String rendered = renderedIndexFile.get(); + public String getRenderedFile(ServerWebExchange exchange, AtomicReference renderedFile, Resource file) { + String rendered = renderedFile.get(); if (rendered == null) { - rendered = buildIndexFile(exchange.getRequest().getPath().contextPath().value()); - if (renderedIndexFile.compareAndSet(null, rendered)) { + rendered = buildFile(file, exchange.getRequest().getPath().contextPath().value()); + if (renderedFile.compareAndSet(null, rendered)) { return rendered; } else { - return renderedIndexFile.get(); + return renderedFile.get(); } } else { return rendered; @@ -42,11 +51,9 @@ public String getRenderedIndexFile(ServerWebExchange exchange) { } @SneakyThrows - private String buildIndexFile(String contextPath) { - final String staticPath = contextPath + "/static"; - return ResourceUtil.readAsString(indexFile) - .replace("href=\"./static", "href=\"" + staticPath) - .replace("src=\"./static", "src=\"" + staticPath) - .replace("window.basePath=\"\"", "window.basePath=\"" + contextPath + "\""); + private String buildFile(Resource file, String contextPath) { + return ResourceUtil.readAsString(file) + .replace("\"assets/", "\"" + contextPath + "/assets/") + .replace("PUBLIC-PATH-VARIABLE", contextPath); } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/TopicsController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/TopicsController.java index e30a540c903..d24044717f3 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/TopicsController.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/TopicsController.java @@ -1,5 +1,10 @@ package com.provectus.kafka.ui.controller; +import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.CREATE; +import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.DELETE; +import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.EDIT; +import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.MESSAGES_READ; +import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.VIEW; import static java.util.stream.Collectors.toList; import com.provectus.kafka.ui.api.TopicsApi; @@ -11,16 +16,21 @@ import com.provectus.kafka.ui.model.ReplicationFactorChangeDTO; import com.provectus.kafka.ui.model.ReplicationFactorChangeResponseDTO; import com.provectus.kafka.ui.model.SortOrderDTO; +import com.provectus.kafka.ui.model.TopicAnalysisDTO; import com.provectus.kafka.ui.model.TopicColumnsToSortDTO; import com.provectus.kafka.ui.model.TopicConfigDTO; import com.provectus.kafka.ui.model.TopicCreationDTO; import com.provectus.kafka.ui.model.TopicDTO; import com.provectus.kafka.ui.model.TopicDetailsDTO; +import com.provectus.kafka.ui.model.TopicProducerStateDTO; import com.provectus.kafka.ui.model.TopicUpdateDTO; import com.provectus.kafka.ui.model.TopicsResponseDTO; +import com.provectus.kafka.ui.model.rbac.AccessContext; import com.provectus.kafka.ui.service.TopicsService; +import com.provectus.kafka.ui.service.analyze.TopicAnalysisService; import java.util.Comparator; import java.util.List; +import java.util.Map; import javax.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -40,79 +50,152 @@ public class TopicsController extends AbstractController implements TopicsApi { private static final Integer DEFAULT_PAGE_SIZE = 25; private final TopicsService topicsService; + private final TopicAnalysisService topicAnalysisService; private final ClusterMapper clusterMapper; @Override public Mono> createTopic( - String clusterName, @Valid Mono topicCreation, ServerWebExchange exchange) { - return topicsService.createTopic(getCluster(clusterName), topicCreation) - .map(clusterMapper::toTopic) - .map(s -> new ResponseEntity<>(s, HttpStatus.OK)) - .switchIfEmpty(Mono.just(ResponseEntity.notFound().build())); + String clusterName, @Valid Mono topicCreationMono, ServerWebExchange exchange) { + return topicCreationMono.flatMap(topicCreation -> { + var context = AccessContext.builder() + .cluster(clusterName) + .topicActions(CREATE) + .operationName("createTopic") + .operationParams(topicCreation) + .build(); + + return validateAccess(context) + .then(topicsService.createTopic(getCluster(clusterName), topicCreation)) + .map(clusterMapper::toTopic) + .map(s -> new ResponseEntity<>(s, HttpStatus.OK)) + .switchIfEmpty(Mono.just(ResponseEntity.notFound().build())) + .doOnEach(sig -> audit(context, sig)); + }); } @Override public Mono> recreateTopic(String clusterName, - String topicName, ServerWebExchange serverWebExchange) { - return topicsService.recreateTopic(getCluster(clusterName), topicName) - .map(clusterMapper::toTopic) - .map(s -> new ResponseEntity<>(s, HttpStatus.CREATED)); + String topicName, ServerWebExchange exchange) { + var context = AccessContext.builder() + .cluster(clusterName) + .topic(topicName) + .topicActions(VIEW, CREATE, DELETE) + .operationName("recreateTopic") + .build(); + + return validateAccess(context).then( + topicsService.recreateTopic(getCluster(clusterName), topicName) + .map(clusterMapper::toTopic) + .map(s -> new ResponseEntity<>(s, HttpStatus.CREATED)) + ).doOnEach(sig -> audit(context, sig)); } @Override public Mono> cloneTopic( String clusterName, String topicName, String newTopicName, ServerWebExchange exchange) { - return topicsService.cloneTopic(getCluster(clusterName), topicName, newTopicName) - .map(clusterMapper::toTopic) - .map(s -> new ResponseEntity<>(s, HttpStatus.CREATED)); + + var context = AccessContext.builder() + .cluster(clusterName) + .topic(topicName) + .topicActions(VIEW, CREATE) + .operationName("cloneTopic") + .operationParams(Map.of("newTopicName", newTopicName)) + .build(); + + return validateAccess(context) + .then(topicsService.cloneTopic(getCluster(clusterName), topicName, newTopicName) + .map(clusterMapper::toTopic) + .map(s -> new ResponseEntity<>(s, HttpStatus.CREATED)) + ).doOnEach(sig -> audit(context, sig)); } @Override public Mono> deleteTopic( String clusterName, String topicName, ServerWebExchange exchange) { - return topicsService.deleteTopic(getCluster(clusterName), topicName).map(ResponseEntity::ok); + + var context = AccessContext.builder() + .cluster(clusterName) + .topic(topicName) + .topicActions(DELETE) + .operationName("deleteTopic") + .build(); + + return validateAccess(context) + .then( + topicsService.deleteTopic(getCluster(clusterName), topicName) + .thenReturn(ResponseEntity.ok().build()) + ).doOnEach(sig -> audit(context, sig)); } @Override public Mono>> getTopicConfigs( String clusterName, String topicName, ServerWebExchange exchange) { - return topicsService.getTopicConfigs(getCluster(clusterName), topicName) - .map(lst -> lst.stream() - .map(InternalTopicConfig::from) - .map(clusterMapper::toTopicConfig) - .collect(toList())) - .map(Flux::fromIterable) - .map(ResponseEntity::ok); + + var context = AccessContext.builder() + .cluster(clusterName) + .topic(topicName) + .topicActions(VIEW) + .operationName("getTopicConfigs") + .build(); + + return validateAccess(context).then( + topicsService.getTopicConfigs(getCluster(clusterName), topicName) + .map(lst -> lst.stream() + .map(InternalTopicConfig::from) + .map(clusterMapper::toTopicConfig) + .toList()) + .map(Flux::fromIterable) + .map(ResponseEntity::ok) + ).doOnEach(sig -> audit(context, sig)); } @Override public Mono> getTopicDetails( String clusterName, String topicName, ServerWebExchange exchange) { - return topicsService.getTopicDetails(getCluster(clusterName), topicName) - .map(clusterMapper::toTopicDetails) - .map(ResponseEntity::ok); + + var context = AccessContext.builder() + .cluster(clusterName) + .topic(topicName) + .topicActions(VIEW) + .operationName("getTopicDetails") + .build(); + + return validateAccess(context).then( + topicsService.getTopicDetails(getCluster(clusterName), topicName) + .map(clusterMapper::toTopicDetails) + .map(ResponseEntity::ok) + ).doOnEach(sig -> audit(context, sig)); } - public Mono> getTopics(String clusterName, @Valid Integer page, + @Override + public Mono> getTopics(String clusterName, + @Valid Integer page, @Valid Integer perPage, @Valid Boolean showInternal, @Valid String search, @Valid TopicColumnsToSortDTO orderBy, @Valid SortOrderDTO sortOrder, ServerWebExchange exchange) { + + AccessContext context = AccessContext.builder() + .cluster(clusterName) + .operationName("getTopics") + .build(); + return topicsService.getTopicsForPagination(getCluster(clusterName)) - .flatMap(existingTopics -> { + .flatMap(topics -> accessControlService.filterViewableTopics(topics, clusterName)) + .flatMap(topics -> { int pageSize = perPage != null && perPage > 0 ? perPage : DEFAULT_PAGE_SIZE; var topicsToSkip = ((page != null && page > 0 ? page : 1) - 1) * pageSize; var comparator = sortOrder == null || !sortOrder.equals(SortOrderDTO.DESC) ? getComparatorForTopic(orderBy) : getComparatorForTopic(orderBy).reversed(); - List filtered = existingTopics.stream() + List filtered = topics.stream() .filter(topic -> !topic.isInternal() || showInternal != null && showInternal) - .filter(topic -> search == null || StringUtils.contains(topic.getName(), search)) + .filter(topic -> search == null || StringUtils.containsIgnoreCase(topic.getName(), search)) .sorted(comparator) - .collect(toList()); + .toList(); var totalPages = (filtered.size() / pageSize) + (filtered.size() % pageSize == 0 ? 0 : 1); @@ -125,9 +208,152 @@ public Mono> getTopics(String clusterName, @Va return topicsService.loadTopics(getCluster(clusterName), topicsPage) .map(topicsToRender -> new TopicsResponseDTO() - .topics(topicsToRender.stream().map(clusterMapper::toTopic).collect(toList())) + .topics(topicsToRender.stream().map(clusterMapper::toTopic).toList()) .pageCount(totalPages)); - }).map(ResponseEntity::ok); + }) + .map(ResponseEntity::ok) + .doOnEach(sig -> audit(context, sig)); + } + + @Override + public Mono> updateTopic( + String clusterName, String topicName, @Valid Mono topicUpdate, + ServerWebExchange exchange) { + + var context = AccessContext.builder() + .cluster(clusterName) + .topic(topicName) + .topicActions(VIEW, EDIT) + .operationName("updateTopic") + .build(); + + return validateAccess(context).then( + topicsService + .updateTopic(getCluster(clusterName), topicName, topicUpdate) + .map(clusterMapper::toTopic) + .map(ResponseEntity::ok) + ).doOnEach(sig -> audit(context, sig)); + } + + @Override + public Mono> increaseTopicPartitions( + String clusterName, String topicName, + Mono partitionsIncrease, + ServerWebExchange exchange) { + + var context = AccessContext.builder() + .cluster(clusterName) + .topic(topicName) + .topicActions(VIEW, EDIT) + .build(); + + return validateAccess(context).then( + partitionsIncrease.flatMap(partitions -> + topicsService.increaseTopicPartitions(getCluster(clusterName), topicName, partitions) + ).map(ResponseEntity::ok) + ).doOnEach(sig -> audit(context, sig)); + } + + @Override + public Mono> changeReplicationFactor( + String clusterName, String topicName, + Mono replicationFactorChange, + ServerWebExchange exchange) { + + var context = AccessContext.builder() + .cluster(clusterName) + .topic(topicName) + .topicActions(VIEW, EDIT) + .operationName("changeReplicationFactor") + .build(); + + return validateAccess(context).then( + replicationFactorChange + .flatMap(rfc -> + topicsService.changeReplicationFactor(getCluster(clusterName), topicName, rfc)) + .map(ResponseEntity::ok) + ).doOnEach(sig -> audit(context, sig)); + } + + @Override + public Mono> analyzeTopic(String clusterName, String topicName, ServerWebExchange exchange) { + + var context = AccessContext.builder() + .cluster(clusterName) + .topic(topicName) + .topicActions(MESSAGES_READ) + .operationName("analyzeTopic") + .build(); + + return validateAccess(context).then( + topicAnalysisService.analyze(getCluster(clusterName), topicName) + .doOnEach(sig -> audit(context, sig)) + .thenReturn(ResponseEntity.ok().build()) + ); + } + + @Override + public Mono> cancelTopicAnalysis(String clusterName, String topicName, + ServerWebExchange exchange) { + var context = AccessContext.builder() + .cluster(clusterName) + .topic(topicName) + .topicActions(MESSAGES_READ) + .operationName("cancelTopicAnalysis") + .build(); + + return validateAccess(context) + .then(Mono.fromRunnable(() -> topicAnalysisService.cancelAnalysis(getCluster(clusterName), topicName))) + .doOnEach(sig -> audit(context, sig)) + .thenReturn(ResponseEntity.ok().build()); + } + + + @Override + public Mono> getTopicAnalysis(String clusterName, + String topicName, + ServerWebExchange exchange) { + + var context = AccessContext.builder() + .cluster(clusterName) + .topic(topicName) + .topicActions(MESSAGES_READ) + .operationName("getTopicAnalysis") + .build(); + + return validateAccess(context) + .thenReturn(topicAnalysisService.getTopicAnalysis(getCluster(clusterName), topicName) + .map(ResponseEntity::ok) + .orElseGet(() -> ResponseEntity.notFound().build())) + .doOnEach(sig -> audit(context, sig)); + } + + @Override + public Mono>> getActiveProducerStates(String clusterName, + String topicName, + ServerWebExchange exchange) { + var context = AccessContext.builder() + .cluster(clusterName) + .topic(topicName) + .topicActions(VIEW) + .operationName("getActiveProducerStates") + .build(); + + Comparator ordering = + Comparator.comparingInt(TopicProducerStateDTO::getPartition) + .thenComparing(Comparator.comparing(TopicProducerStateDTO::getProducerId).reversed()); + + Flux states = topicsService.getActiveProducersState(getCluster(clusterName), topicName) + .flatMapMany(statesMap -> + Flux.fromStream( + statesMap.entrySet().stream() + .flatMap(e -> e.getValue().stream().map(p -> clusterMapper.map(e.getKey().partition(), p))) + .sorted(ordering))); + + return validateAccess(context) + .thenReturn(states) + .map(ResponseEntity::ok) + .doOnEach(sig -> audit(context, sig)); } private Comparator getComparatorForTopic( @@ -150,35 +376,4 @@ private Comparator getComparatorForTopic( return defaultComparator; } } - - @Override - public Mono> updateTopic( - String clusterId, String topicName, @Valid Mono topicUpdate, - ServerWebExchange exchange) { - return topicsService - .updateTopic(getCluster(clusterId), topicName, topicUpdate) - .map(clusterMapper::toTopic) - .map(ResponseEntity::ok); - } - - @Override - public Mono> increaseTopicPartitions( - String clusterName, String topicName, - Mono partitionsIncrease, - ServerWebExchange exchange) { - return partitionsIncrease.flatMap(partitions -> - topicsService.increaseTopicPartitions(getCluster(clusterName), topicName, partitions) - ).map(ResponseEntity::ok); - } - - @Override - public Mono> changeReplicationFactor( - String clusterName, String topicName, - Mono replicationFactorChange, - ServerWebExchange exchange) { - return replicationFactorChange - .flatMap(rfc -> - topicsService.changeReplicationFactor(getCluster(clusterName), topicName, rfc)) - .map(ResponseEntity::ok); - } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/AbstractEmitter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/AbstractEmitter.java index 84228c41342..ec576a1d1a6 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/AbstractEmitter.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/AbstractEmitter.java @@ -1,63 +1,44 @@ package com.provectus.kafka.ui.emitter; -import com.provectus.kafka.ui.model.TopicMessageDTO; import com.provectus.kafka.ui.model.TopicMessageEventDTO; -import com.provectus.kafka.ui.model.TopicMessagePhaseDTO; -import com.provectus.kafka.ui.serde.RecordSerDe; -import com.provectus.kafka.ui.util.ClusterUtil; -import java.time.Duration; -import java.time.Instant; -import org.apache.kafka.clients.consumer.Consumer; import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.common.utils.Bytes; import reactor.core.publisher.FluxSink; -public abstract class AbstractEmitter { - private static final Duration DEFAULT_POLL_TIMEOUT_MS = Duration.ofMillis(1000L); +abstract class AbstractEmitter implements java.util.function.Consumer> { - private final RecordSerDe recordDeserializer; - private final ConsumingStats consumingStats = new ConsumingStats(); + private final MessagesProcessing messagesProcessing; + private final PollingSettings pollingSettings; - protected AbstractEmitter(RecordSerDe recordDeserializer) { - this.recordDeserializer = recordDeserializer; + protected AbstractEmitter(MessagesProcessing messagesProcessing, PollingSettings pollingSettings) { + this.messagesProcessing = messagesProcessing; + this.pollingSettings = pollingSettings; } - protected ConsumerRecords poll( - FluxSink sink, Consumer consumer) { - return poll(sink, consumer, DEFAULT_POLL_TIMEOUT_MS); + protected PolledRecords poll(FluxSink sink, EnhancedConsumer consumer) { + var records = consumer.pollEnhanced(pollingSettings.getPollTimeout()); + sendConsuming(sink, records); + return records; } - protected ConsumerRecords poll( - FluxSink sink, Consumer consumer, Duration timeout) { - Instant start = Instant.now(); - ConsumerRecords records = consumer.poll(timeout); - Instant finish = Instant.now(); - sendConsuming(sink, records, Duration.between(start, finish).toMillis()); - return records; + protected boolean sendLimitReached() { + return messagesProcessing.limitReached(); } - protected void sendMessage(FluxSink sink, - ConsumerRecord msg) { - final TopicMessageDTO topicMessage = ClusterUtil.mapToTopicMessage(msg, recordDeserializer); - sink.next( - new TopicMessageEventDTO() - .type(TopicMessageEventDTO.TypeEnum.MESSAGE) - .message(topicMessage) - ); + protected void send(FluxSink sink, Iterable> records) { + messagesProcessing.send(sink, records); } protected void sendPhase(FluxSink sink, String name) { - sink.next( - new TopicMessageEventDTO() - .type(TopicMessageEventDTO.TypeEnum.PHASE) - .phase(new TopicMessagePhaseDTO().name(name)) - ); + messagesProcessing.sendPhase(sink, name); + } + + protected void sendConsuming(FluxSink sink, PolledRecords records) { + messagesProcessing.sentConsumingInfo(sink, records); } - protected void sendConsuming(FluxSink sink, - ConsumerRecords records, - long elapsed) { - consumingStats.sendConsumingEvt(sink, records, elapsed); + protected void sendFinishStatsAndCompleteSink(FluxSink sink) { + messagesProcessing.sendFinishEvent(sink); + sink.complete(); } -} \ No newline at end of file +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/BackwardEmitter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/BackwardEmitter.java new file mode 100644 index 00000000000..cdc45336e46 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/BackwardEmitter.java @@ -0,0 +1,60 @@ +package com.provectus.kafka.ui.emitter; + +import com.provectus.kafka.ui.model.ConsumerPosition; +import com.provectus.kafka.ui.model.TopicMessageDTO; +import com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer; +import java.util.Comparator; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.apache.kafka.common.TopicPartition; + +public class BackwardEmitter extends RangePollingEmitter { + + public BackwardEmitter(Supplier consumerSupplier, + ConsumerPosition consumerPosition, + int messagesPerPage, + ConsumerRecordDeserializer deserializer, + Predicate filter, + PollingSettings pollingSettings) { + super( + consumerSupplier, + consumerPosition, + messagesPerPage, + new MessagesProcessing( + deserializer, + filter, + false, + messagesPerPage + ), + pollingSettings + ); + } + + @Override + protected TreeMap nextPollingRange(TreeMap prevRange, + SeekOperations seekOperations) { + TreeMap readToOffsets = new TreeMap<>(Comparator.comparingInt(TopicPartition::partition)); + if (prevRange.isEmpty()) { + readToOffsets.putAll(seekOperations.getOffsetsForSeek()); + } else { + readToOffsets.putAll( + prevRange.entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().from())) + ); + } + + int msgsToPollPerPartition = (int) Math.ceil((double) messagesPerPage / readToOffsets.size()); + TreeMap result = new TreeMap<>(Comparator.comparingInt(TopicPartition::partition)); + readToOffsets.forEach((tp, toOffset) -> { + long tpStartOffset = seekOperations.getBeginOffsets().get(tp); + if (toOffset > tpStartOffset) { + result.put(tp, new FromToOffset(Math.max(tpStartOffset, toOffset - msgsToPollPerPartition), toOffset)); + } + }); + return result; + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/BackwardRecordEmitter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/BackwardRecordEmitter.java deleted file mode 100644 index ff29110c973..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/BackwardRecordEmitter.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.provectus.kafka.ui.emitter; - -import com.provectus.kafka.ui.model.TopicMessageEventDTO; -import com.provectus.kafka.ui.serde.RecordSerDe; -import com.provectus.kafka.ui.util.OffsetsSeekBackward; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.SortedMap; -import java.util.TreeMap; -import java.util.function.Function; -import java.util.stream.Collectors; -import lombok.extern.slf4j.Slf4j; -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.consumer.KafkaConsumer; -import org.apache.kafka.common.TopicPartition; -import org.apache.kafka.common.utils.Bytes; -import reactor.core.publisher.FluxSink; - -@Slf4j -public class BackwardRecordEmitter - extends AbstractEmitter - implements java.util.function.Consumer> { - - private static final Duration POLL_TIMEOUT = Duration.ofMillis(200); - - private final Function, KafkaConsumer> consumerSupplier; - private final OffsetsSeekBackward offsetsSeek; - - public BackwardRecordEmitter( - Function, KafkaConsumer> consumerSupplier, - OffsetsSeekBackward offsetsSeek, - RecordSerDe recordDeserializer) { - super(recordDeserializer); - this.offsetsSeek = offsetsSeek; - this.consumerSupplier = consumerSupplier; - } - - @Override - public void accept(FluxSink sink) { - try (KafkaConsumer configConsumer = consumerSupplier.apply(Map.of())) { - final List requestedPartitions = - offsetsSeek.getRequestedPartitions(configConsumer); - sendPhase(sink, "Request partitions"); - final int msgsPerPartition = offsetsSeek.msgsPerPartition(requestedPartitions.size()); - try (KafkaConsumer consumer = - consumerSupplier.apply( - Map.of(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, msgsPerPartition) - ) - ) { - sendPhase(sink, "Created consumer"); - - SortedMap readUntilOffsets = - new TreeMap<>(Comparator.comparingInt(TopicPartition::partition)); - readUntilOffsets.putAll(offsetsSeek.getPartitionsOffsets(consumer)); - - sendPhase(sink, "Requested partitions offsets"); - log.debug("partition offsets: {}", readUntilOffsets); - var waitingOffsets = - offsetsSeek.waitingOffsets(consumer, readUntilOffsets.keySet()); - log.debug("waiting offsets {} {}", - waitingOffsets.getBeginOffsets(), - waitingOffsets.getEndOffsets() - ); - - while (!sink.isCancelled() && !waitingOffsets.beginReached()) { - new TreeMap<>(readUntilOffsets).forEach((tp, readToOffset) -> { - long lowestOffset = waitingOffsets.getBeginOffsets().get(tp.partition()); - long readFromOffset = Math.max(lowestOffset, readToOffset - msgsPerPartition); - - partitionPollIteration(tp, readFromOffset, readToOffset, consumer, sink) - .stream() - .filter(r -> !sink.isCancelled()) - .forEach(r -> sendMessage(sink, r)); - - waitingOffsets.markPolled(tp.partition(), readFromOffset); - if (waitingOffsets.getBeginOffsets().get(tp.partition()) == null) { - // we fully read this partition -> removing it from polling iterations - readUntilOffsets.remove(tp); - } else { - readUntilOffsets.put(tp, readFromOffset); - } - }); - - if (waitingOffsets.beginReached()) { - log.debug("begin reached after partitions poll iteration"); - } else if (sink.isCancelled()) { - log.debug("sink is cancelled after partitions poll iteration"); - } - } - sink.complete(); - log.debug("Polling finished"); - } - } catch (Exception e) { - log.error("Error occurred while consuming records", e); - sink.error(e); - } - } - - - private List> partitionPollIteration( - TopicPartition tp, - long fromOffset, - long toOffset, - Consumer consumer, - FluxSink sink - ) { - consumer.assign(Collections.singleton(tp)); - consumer.seek(tp, fromOffset); - sendPhase(sink, String.format("Polling partition: %s from offset %s", tp, fromOffset)); - int desiredMsgsToPoll = (int) (toOffset - fromOffset); - - var recordsToSend = new ArrayList>(); - - // we use empty polls counting to verify that partition was fully read - for (int emptyPolls = 0; recordsToSend.size() < desiredMsgsToPoll && emptyPolls < 3; ) { - var polledRecords = poll(sink, consumer, POLL_TIMEOUT); - log.debug("{} records polled from {}", polledRecords.count(), tp); - - // counting sequential empty polls - emptyPolls = polledRecords.isEmpty() ? emptyPolls + 1 : 0; - - var filteredRecords = polledRecords.records(tp).stream() - .filter(r -> r.offset() < toOffset) - .collect(Collectors.toList()); - - if (!polledRecords.isEmpty() && filteredRecords.isEmpty()) { - // we already read all messages in target offsets interval - break; - } - recordsToSend.addAll(filteredRecords); - } - log.debug("{} records to send", recordsToSend.size()); - Collections.reverse(recordsToSend); - return recordsToSend; - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ConsumingStats.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ConsumingStats.java index 830eb87320b..b0737e1cb9c 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ConsumingStats.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ConsumingStats.java @@ -2,10 +2,6 @@ import com.provectus.kafka.ui.model.TopicMessageConsumingDTO; import com.provectus.kafka.ui.model.TopicMessageEventDTO; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.consumer.ConsumerRecords; -import org.apache.kafka.common.header.Header; -import org.apache.kafka.common.utils.Bytes; import reactor.core.publisher.FluxSink; class ConsumingStats { @@ -13,29 +9,37 @@ class ConsumingStats { private long bytes = 0; private int records = 0; private long elapsed = 0; + private int filterApplyErrors = 0; - void sendConsumingEvt(FluxSink sink, - ConsumerRecords polledRecords, - long elapsed) { - for (ConsumerRecord rec : polledRecords) { - for (Header header : rec.headers()) { - bytes += - (header.key() != null ? header.key().getBytes().length : 0L) - + (header.value() != null ? header.value().length : 0L); - } - bytes += rec.serializedKeySize() + rec.serializedValueSize(); - } - this.records += polledRecords.count(); - this.elapsed += elapsed; - final TopicMessageConsumingDTO consuming = new TopicMessageConsumingDTO() - .bytesConsumed(this.bytes) - .elapsedMs(this.elapsed) - .isCancelled(sink.isCancelled()) - .messagesConsumed(this.records); + void sendConsumingEvt(FluxSink sink, PolledRecords polledRecords) { + bytes += polledRecords.bytes(); + records += polledRecords.count(); + elapsed += polledRecords.elapsed().toMillis(); sink.next( new TopicMessageEventDTO() .type(TopicMessageEventDTO.TypeEnum.CONSUMING) - .consuming(consuming) + .consuming(createConsumingStats()) ); } + + void incFilterApplyError() { + filterApplyErrors++; + } + + void sendFinishEvent(FluxSink sink) { + sink.next( + new TopicMessageEventDTO() + .type(TopicMessageEventDTO.TypeEnum.DONE) + .consuming(createConsumingStats()) + ); + } + + private TopicMessageConsumingDTO createConsumingStats() { + return new TopicMessageConsumingDTO() + .bytesConsumed(bytes) + .elapsedMs(elapsed) + .isCancelled(false) + .filterApplyErrors(filterApplyErrors) + .messagesConsumed(records); + } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/EnhancedConsumer.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/EnhancedConsumer.java new file mode 100644 index 00000000000..be849c7888e --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/EnhancedConsumer.java @@ -0,0 +1,82 @@ +package com.provectus.kafka.ui.emitter; + +import com.google.common.base.Preconditions; +import com.google.common.base.Stopwatch; +import com.provectus.kafka.ui.util.ApplicationMetrics; +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Delegate; +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.common.serialization.BytesDeserializer; +import org.apache.kafka.common.utils.Bytes; + + +public class EnhancedConsumer extends KafkaConsumer { + + private final PollingThrottler throttler; + private final ApplicationMetrics metrics; + private String pollingTopic; + + public EnhancedConsumer(Properties properties, + PollingThrottler throttler, + ApplicationMetrics metrics) { + super(properties, new BytesDeserializer(), new BytesDeserializer()); + this.throttler = throttler; + this.metrics = metrics; + metrics.activeConsumers().incrementAndGet(); + } + + public PolledRecords pollEnhanced(Duration dur) { + var stopwatch = Stopwatch.createStarted(); + ConsumerRecords polled = poll(dur); + PolledRecords polledEnhanced = PolledRecords.create(polled, stopwatch.elapsed()); + var throttled = throttler.throttleAfterPoll(polledEnhanced.bytes()); + metrics.meterPolledRecords(pollingTopic, polledEnhanced, throttled); + return polledEnhanced; + } + + @Override + public void assign(Collection partitions) { + super.assign(partitions); + Set assignedTopics = partitions.stream().map(TopicPartition::topic).collect(Collectors.toSet()); + Preconditions.checkState(assignedTopics.size() == 1); + this.pollingTopic = assignedTopics.iterator().next(); + } + + @Override + public void subscribe(Pattern pattern) { + throw new UnsupportedOperationException(); + } + + @Override + public void subscribe(Collection topics) { + throw new UnsupportedOperationException(); + } + + @Override + public void subscribe(Pattern pattern, ConsumerRebalanceListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public void subscribe(Collection topics, ConsumerRebalanceListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public void close(Duration timeout) { + metrics.activeConsumers().decrementAndGet(); + super.close(timeout); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ForwardEmitter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ForwardEmitter.java new file mode 100644 index 00000000000..5c915fb2e8c --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ForwardEmitter.java @@ -0,0 +1,61 @@ +package com.provectus.kafka.ui.emitter; + +import com.provectus.kafka.ui.model.ConsumerPosition; +import com.provectus.kafka.ui.model.TopicMessageDTO; +import com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer; +import java.util.Comparator; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.apache.kafka.common.TopicPartition; + +public class ForwardEmitter extends RangePollingEmitter { + + public ForwardEmitter(Supplier consumerSupplier, + ConsumerPosition consumerPosition, + int messagesPerPage, + ConsumerRecordDeserializer deserializer, + Predicate filter, + PollingSettings pollingSettings) { + super( + consumerSupplier, + consumerPosition, + messagesPerPage, + new MessagesProcessing( + deserializer, + filter, + true, + messagesPerPage + ), + pollingSettings + ); + } + + @Override + protected TreeMap nextPollingRange(TreeMap prevRange, + SeekOperations seekOperations) { + TreeMap readFromOffsets = new TreeMap<>(Comparator.comparingInt(TopicPartition::partition)); + if (prevRange.isEmpty()) { + readFromOffsets.putAll(seekOperations.getOffsetsForSeek()); + } else { + readFromOffsets.putAll( + prevRange.entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().to())) + ); + } + + int msgsToPollPerPartition = (int) Math.ceil((double) messagesPerPage / readFromOffsets.size()); + TreeMap result = new TreeMap<>(Comparator.comparingInt(TopicPartition::partition)); + readFromOffsets.forEach((tp, fromOffset) -> { + long tpEndOffset = seekOperations.getEndOffsets().get(tp); + if (fromOffset < tpEndOffset) { + result.put(tp, new FromToOffset(fromOffset, Math.min(tpEndOffset, fromOffset + msgsToPollPerPartition))); + } + }); + return result; + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ForwardRecordEmitter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ForwardRecordEmitter.java deleted file mode 100644 index 27a6bbec8ca..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ForwardRecordEmitter.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.provectus.kafka.ui.emitter; - -import com.provectus.kafka.ui.model.TopicMessageEventDTO; -import com.provectus.kafka.ui.serde.RecordSerDe; -import com.provectus.kafka.ui.util.OffsetsSeek; -import java.util.function.Supplier; -import lombok.extern.slf4j.Slf4j; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.consumer.ConsumerRecords; -import org.apache.kafka.clients.consumer.KafkaConsumer; -import org.apache.kafka.common.utils.Bytes; -import reactor.core.publisher.FluxSink; - -@Slf4j -public class ForwardRecordEmitter - extends AbstractEmitter - implements java.util.function.Consumer> { - - private final Supplier> consumerSupplier; - private final OffsetsSeek offsetsSeek; - - public ForwardRecordEmitter( - Supplier> consumerSupplier, - OffsetsSeek offsetsSeek, - RecordSerDe recordDeserializer) { - super(recordDeserializer); - this.consumerSupplier = consumerSupplier; - this.offsetsSeek = offsetsSeek; - } - - @Override - public void accept(FluxSink sink) { - try (KafkaConsumer consumer = consumerSupplier.get()) { - sendPhase(sink, "Assigning partitions"); - var waitingOffsets = offsetsSeek.assignAndSeek(consumer); - while (!sink.isCancelled() && !waitingOffsets.endReached()) { - sendPhase(sink, "Polling"); - ConsumerRecords records = poll(sink, consumer); - log.info("{} records polled", records.count()); - - for (ConsumerRecord msg : records) { - if (!sink.isCancelled() && !waitingOffsets.endReached()) { - sendMessage(sink, msg); - waitingOffsets.markPolled(msg); - } else { - break; - } - } - } - sink.complete(); - log.info("Polling finished"); - } catch (Exception e) { - log.error("Error occurred while consuming records", e); - sink.error(e); - } - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/MessageFilters.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/MessageFilters.java index 446f166bfeb..6e9f8a8bbe3 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/MessageFilters.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/MessageFilters.java @@ -9,6 +9,7 @@ import javax.script.CompiledScript; import javax.script.ScriptEngineManager; import javax.script.ScriptException; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.codehaus.groovy.jsr223.GroovyScriptEngineImpl; @@ -38,39 +39,42 @@ static Predicate containsStringFilter(String string) { } static Predicate groovyScriptFilter(String script) { - var compiledScript = compileScript(script); + var engine = getGroovyEngine(); + var compiledScript = compileScript(engine, script); var jsonSlurper = new JsonSlurper(); - return msg -> { - var bindings = getGroovyEngine().createBindings(); - bindings.put("partition", msg.getPartition()); - bindings.put("timestampMs", msg.getTimestamp().toInstant().toEpochMilli()); - bindings.put("keyAsText", msg.getKey()); - bindings.put("valueAsText", msg.getContent()); - bindings.put("headers", msg.getHeaders()); - bindings.put("key", parseToJsonOrReturnNull(jsonSlurper, msg.getKey())); - bindings.put("value", parseToJsonOrReturnNull(jsonSlurper, msg.getContent())); - try { + return new Predicate() { + @SneakyThrows + @Override + public boolean test(TopicMessageDTO msg) { + var bindings = engine.createBindings(); + bindings.put("partition", msg.getPartition()); + bindings.put("offset", msg.getOffset()); + bindings.put("timestampMs", msg.getTimestamp().toInstant().toEpochMilli()); + bindings.put("keyAsText", msg.getKey()); + bindings.put("valueAsText", msg.getContent()); + bindings.put("headers", msg.getHeaders()); + bindings.put("key", parseToJsonOrReturnAsIs(jsonSlurper, msg.getKey())); + bindings.put("value", parseToJsonOrReturnAsIs(jsonSlurper, msg.getContent())); var result = compiledScript.eval(bindings); if (result instanceof Boolean) { return (Boolean) result; + } else { + throw new ValidationException( + "Unexpected script result: %s, Boolean should be returned instead".formatted(result)); } - return false; - } catch (Exception e) { - log.trace("Error executing filter script '{}' on message '{}' ", script, msg, e); - return false; } }; } @Nullable - private static Object parseToJsonOrReturnNull(JsonSlurper parser, @Nullable String str) { + private static Object parseToJsonOrReturnAsIs(JsonSlurper parser, @Nullable String str) { if (str == null) { return null; } try { return parser.parseText(str); } catch (Exception e) { - return null; + return str; } } @@ -83,9 +87,9 @@ private static synchronized GroovyScriptEngineImpl getGroovyEngine() { return GROOVY_ENGINE; } - private static CompiledScript compileScript(String script) { + private static CompiledScript compileScript(GroovyScriptEngineImpl engine, String script) { try { - return getGroovyEngine().compile(script); + return engine.compile(script); } catch (ScriptException e) { throw new ValidationException("Script syntax error: " + e.getMessage()); } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/MessagesProcessing.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/MessagesProcessing.java new file mode 100644 index 00000000000..df8505a2e9a --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/MessagesProcessing.java @@ -0,0 +1,112 @@ +package com.provectus.kafka.ui.emitter; + +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toList; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Iterables; +import com.google.common.collect.Streams; +import com.provectus.kafka.ui.model.TopicMessageDTO; +import com.provectus.kafka.ui.model.TopicMessageEventDTO; +import com.provectus.kafka.ui.model.TopicMessagePhaseDTO; +import com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Predicate; +import javax.annotation.Nullable; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.common.utils.Bytes; +import reactor.core.publisher.FluxSink; + +@Slf4j +@RequiredArgsConstructor +class MessagesProcessing { + + private final ConsumingStats consumingStats = new ConsumingStats(); + private long sentMessages = 0; + + private final ConsumerRecordDeserializer deserializer; + private final Predicate filter; + private final boolean ascendingSortBeforeSend; + private final @Nullable Integer limit; + + boolean limitReached() { + return limit != null && sentMessages >= limit; + } + + void send(FluxSink sink, Iterable> polled) { + sortForSending(polled, ascendingSortBeforeSend) + .forEach(rec -> { + if (!limitReached() && !sink.isCancelled()) { + TopicMessageDTO topicMessage = deserializer.deserialize(rec); + try { + if (filter.test(topicMessage)) { + sink.next( + new TopicMessageEventDTO() + .type(TopicMessageEventDTO.TypeEnum.MESSAGE) + .message(topicMessage) + ); + sentMessages++; + } + } catch (Exception e) { + consumingStats.incFilterApplyError(); + log.trace("Error applying filter for message {}", topicMessage); + } + } + }); + } + + void sentConsumingInfo(FluxSink sink, PolledRecords polledRecords) { + if (!sink.isCancelled()) { + consumingStats.sendConsumingEvt(sink, polledRecords); + } + } + + void sendFinishEvent(FluxSink sink) { + if (!sink.isCancelled()) { + consumingStats.sendFinishEvent(sink); + } + } + + void sendPhase(FluxSink sink, String name) { + if (!sink.isCancelled()) { + sink.next( + new TopicMessageEventDTO() + .type(TopicMessageEventDTO.TypeEnum.PHASE) + .phase(new TopicMessagePhaseDTO().name(name)) + ); + } + } + + /* + * Sorting by timestamps, BUT requesting that records within same partitions should be ordered by offsets. + */ + @VisibleForTesting + static Iterable> sortForSending(Iterable> records, + boolean asc) { + Comparator offsetComparator = asc + ? Comparator.comparingLong(ConsumerRecord::offset) + : Comparator.comparingLong(ConsumerRecord::offset).reversed(); + + // partition -> sorted by offsets records + Map>> perPartition = Streams.stream(records) + .collect( + groupingBy( + ConsumerRecord::partition, + TreeMap::new, + collectingAndThen(toList(), lst -> lst.stream().sorted(offsetComparator).toList()))); + + Comparator tsComparator = asc + ? Comparator.comparing(ConsumerRecord::timestamp) + : Comparator.comparingLong(ConsumerRecord::timestamp).reversed(); + + // merge-sorting records from partitions one by one using timestamp comparator + return Iterables.mergeSorted(perPartition.values(), tsComparator); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/OffsetsInfo.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/OffsetsInfo.java new file mode 100644 index 00000000000..85802724178 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/OffsetsInfo.java @@ -0,0 +1,64 @@ +package com.provectus.kafka.ui.emitter; + +import com.google.common.base.Preconditions; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.mutable.MutableLong; +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.common.TopicPartition; + +@Slf4j +@Getter +class OffsetsInfo { + + private final Consumer consumer; + + private final Map beginOffsets; + private final Map endOffsets; + + private final Set nonEmptyPartitions = new HashSet<>(); + private final Set emptyPartitions = new HashSet<>(); + + OffsetsInfo(Consumer consumer, String topic) { + this(consumer, + consumer.partitionsFor(topic).stream() + .map(pi -> new TopicPartition(topic, pi.partition())) + .toList() + ); + } + + OffsetsInfo(Consumer consumer, Collection targetPartitions) { + this.consumer = consumer; + this.beginOffsets = consumer.beginningOffsets(targetPartitions); + this.endOffsets = consumer.endOffsets(targetPartitions); + endOffsets.forEach((tp, endOffset) -> { + var beginningOffset = beginOffsets.get(tp); + if (endOffset > beginningOffset) { + nonEmptyPartitions.add(tp); + } else { + emptyPartitions.add(tp); + } + }); + } + + boolean assignedPartitionsFullyPolled() { + for (var tp : consumer.assignment()) { + Preconditions.checkArgument(endOffsets.containsKey(tp)); + if (endOffsets.get(tp) > consumer.position(tp)) { + return false; + } + } + return true; + } + + long summaryOffsetsRange() { + MutableLong cnt = new MutableLong(); + nonEmptyPartitions.forEach(tp -> cnt.add(endOffsets.get(tp) - beginOffsets.get(tp))); + return cnt.getValue(); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/PolledRecords.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/PolledRecords.java new file mode 100644 index 00000000000..bc6bd95d5f6 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/PolledRecords.java @@ -0,0 +1,48 @@ +package com.provectus.kafka.ui.emitter; + +import java.time.Duration; +import java.util.Iterator; +import java.util.List; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.common.header.Header; +import org.apache.kafka.common.utils.Bytes; + +public record PolledRecords(int count, + int bytes, + Duration elapsed, + ConsumerRecords records) implements Iterable> { + + static PolledRecords create(ConsumerRecords polled, Duration pollDuration) { + return new PolledRecords( + polled.count(), + calculatePolledRecSize(polled), + pollDuration, + polled + ); + } + + public List> records(TopicPartition tp) { + return records.records(tp); + } + + @Override + public Iterator> iterator() { + return records.iterator(); + } + + private static int calculatePolledRecSize(Iterable> recs) { + int polledBytes = 0; + for (ConsumerRecord rec : recs) { + for (Header header : rec.headers()) { + polledBytes += + (header.key() != null ? header.key().getBytes().length : 0) + + (header.value() != null ? header.value().length : 0); + } + polledBytes += rec.key() == null ? 0 : rec.serializedKeySize(); + polledBytes += rec.value() == null ? 0 : rec.serializedValueSize(); + } + return polledBytes; + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/PollingSettings.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/PollingSettings.java new file mode 100644 index 00000000000..eb38851d4cc --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/PollingSettings.java @@ -0,0 +1,50 @@ +package com.provectus.kafka.ui.emitter; + +import com.provectus.kafka.ui.config.ClustersProperties; +import java.time.Duration; +import java.util.Optional; +import java.util.function.Supplier; + +public class PollingSettings { + + private static final Duration DEFAULT_POLL_TIMEOUT = Duration.ofMillis(1_000); + + private final Duration pollTimeout; + private final Supplier throttlerSupplier; + + public static PollingSettings create(ClustersProperties.Cluster cluster, + ClustersProperties clustersProperties) { + var pollingProps = Optional.ofNullable(clustersProperties.getPolling()) + .orElseGet(ClustersProperties.PollingProperties::new); + + var pollTimeout = pollingProps.getPollTimeoutMs() != null + ? Duration.ofMillis(pollingProps.getPollTimeoutMs()) + : DEFAULT_POLL_TIMEOUT; + + return new PollingSettings( + pollTimeout, + PollingThrottler.throttlerSupplier(cluster) + ); + } + + public static PollingSettings createDefault() { + return new PollingSettings( + DEFAULT_POLL_TIMEOUT, + PollingThrottler::noop + ); + } + + private PollingSettings(Duration pollTimeout, + Supplier throttlerSupplier) { + this.pollTimeout = pollTimeout; + this.throttlerSupplier = throttlerSupplier; + } + + public Duration getPollTimeout() { + return pollTimeout; + } + + public PollingThrottler getPollingThrottler() { + return throttlerSupplier.get(); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/PollingThrottler.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/PollingThrottler.java new file mode 100644 index 00000000000..4cde50cdef2 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/PollingThrottler.java @@ -0,0 +1,49 @@ +package com.provectus.kafka.ui.emitter; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.RateLimiter; +import com.provectus.kafka.ui.config.ClustersProperties; +import java.util.function.Supplier; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PollingThrottler { + + public static Supplier throttlerSupplier(ClustersProperties.Cluster cluster) { + Long rate = cluster.getPollingThrottleRate(); + if (rate == null || rate <= 0) { + return PollingThrottler::noop; + } + // RateLimiter instance should be shared across all created throttlers + var rateLimiter = RateLimiter.create(rate); + return () -> new PollingThrottler(cluster.getName(), rateLimiter); + } + + private final String clusterName; + private final RateLimiter rateLimiter; + private boolean throttled; + + @VisibleForTesting + public PollingThrottler(String clusterName, RateLimiter rateLimiter) { + this.clusterName = clusterName; + this.rateLimiter = rateLimiter; + } + + public static PollingThrottler noop() { + return new PollingThrottler("noop", RateLimiter.create(Long.MAX_VALUE)); + } + + //returns true if polling was throttled + public boolean throttleAfterPoll(int polledBytes) { + if (polledBytes > 0) { + double sleptSeconds = rateLimiter.acquire(polledBytes); + if (!throttled && sleptSeconds > 0.0) { + throttled = true; + log.debug("Polling throttling enabled for cluster {} at rate {} bytes/sec", clusterName, rateLimiter.getRate()); + return true; + } + } + return false; + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/RangePollingEmitter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/RangePollingEmitter.java new file mode 100644 index 00000000000..af6dc7d0693 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/RangePollingEmitter.java @@ -0,0 +1,98 @@ +package com.provectus.kafka.ui.emitter; + +import com.provectus.kafka.ui.model.ConsumerPosition; +import com.provectus.kafka.ui.model.TopicMessageEventDTO; +import java.util.ArrayList; +import java.util.List; +import java.util.TreeMap; +import java.util.function.Supplier; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.common.errors.InterruptException; +import org.apache.kafka.common.utils.Bytes; +import reactor.core.publisher.FluxSink; + +@Slf4j +abstract class RangePollingEmitter extends AbstractEmitter { + + private final Supplier consumerSupplier; + protected final ConsumerPosition consumerPosition; + protected final int messagesPerPage; + + protected RangePollingEmitter(Supplier consumerSupplier, + ConsumerPosition consumerPosition, + int messagesPerPage, + MessagesProcessing messagesProcessing, + PollingSettings pollingSettings) { + super(messagesProcessing, pollingSettings); + this.consumerPosition = consumerPosition; + this.messagesPerPage = messagesPerPage; + this.consumerSupplier = consumerSupplier; + } + + protected record FromToOffset(/*inclusive*/ long from, /*exclusive*/ long to) { + } + + //should return empty map if polling should be stopped + protected abstract TreeMap nextPollingRange( + TreeMap prevRange, //empty on start + SeekOperations seekOperations + ); + + @Override + public void accept(FluxSink sink) { + log.debug("Starting polling for {}", consumerPosition); + try (EnhancedConsumer consumer = consumerSupplier.get()) { + sendPhase(sink, "Consumer created"); + var seekOperations = SeekOperations.create(consumer, consumerPosition); + TreeMap pollRange = nextPollingRange(new TreeMap<>(), seekOperations); + log.debug("Starting from offsets {}", pollRange); + + while (!sink.isCancelled() && !pollRange.isEmpty() && !sendLimitReached()) { + var polled = poll(consumer, sink, pollRange); + send(sink, polled); + pollRange = nextPollingRange(pollRange, seekOperations); + } + if (sink.isCancelled()) { + log.debug("Polling finished due to sink cancellation"); + } + sendFinishStatsAndCompleteSink(sink); + log.debug("Polling finished"); + } catch (InterruptException kafkaInterruptException) { + log.debug("Polling finished due to thread interruption"); + sink.complete(); + } catch (Exception e) { + log.error("Error occurred while consuming records", e); + sink.error(e); + } + } + + private List> poll(EnhancedConsumer consumer, + FluxSink sink, + TreeMap range) { + log.trace("Polling range {}", range); + sendPhase(sink, + "Polling partitions: %s".formatted(range.keySet().stream().map(TopicPartition::partition).sorted().toList())); + + consumer.assign(range.keySet()); + range.forEach((tp, fromTo) -> consumer.seek(tp, fromTo.from)); + + List> result = new ArrayList<>(); + while (!sink.isCancelled() && consumer.paused().size() < range.size()) { + var polledRecords = poll(sink, consumer); + range.forEach((tp, fromTo) -> { + polledRecords.records(tp).stream() + .filter(r -> r.offset() < fromTo.to) + .forEach(result::add); + + //next position is out of target range -> pausing partition + if (consumer.position(tp) >= fromTo.to) { + consumer.pause(List.of(tp)); + } + }); + } + consumer.resume(consumer.paused()); + return result; + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ResultSizeLimiter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ResultSizeLimiter.java similarity index 93% rename from kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ResultSizeLimiter.java rename to kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ResultSizeLimiter.java index 64fcb215090..a0fa5bcb938 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ResultSizeLimiter.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ResultSizeLimiter.java @@ -1,4 +1,4 @@ -package com.provectus.kafka.ui.util; +package com.provectus.kafka.ui.emitter; import com.provectus.kafka.ui.model.TopicMessageEventDTO; import java.util.concurrent.atomic.AtomicInteger; diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/SeekOperations.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/SeekOperations.java new file mode 100644 index 00000000000..4de027bdb23 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/SeekOperations.java @@ -0,0 +1,124 @@ +package com.provectus.kafka.ui.emitter; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.provectus.kafka.ui.model.ConsumerPosition; +import com.provectus.kafka.ui.model.SeekTypeDTO; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.mutable.MutableLong; +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.common.TopicPartition; + +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +public class SeekOperations { + + private final Consumer consumer; + private final OffsetsInfo offsetsInfo; + private final Map offsetsForSeek; //only contains non-empty partitions! + + public static SeekOperations create(Consumer consumer, ConsumerPosition consumerPosition) { + OffsetsInfo offsetsInfo; + if (consumerPosition.getSeekTo() == null) { + offsetsInfo = new OffsetsInfo(consumer, consumerPosition.getTopic()); + } else { + offsetsInfo = new OffsetsInfo(consumer, consumerPosition.getSeekTo().keySet()); + } + return new SeekOperations( + consumer, + offsetsInfo, + getOffsetsForSeek(consumer, offsetsInfo, consumerPosition.getSeekType(), consumerPosition.getSeekTo()) + ); + } + + public void assignAndSeekNonEmptyPartitions() { + consumer.assign(offsetsForSeek.keySet()); + offsetsForSeek.forEach(consumer::seek); + } + + public Map getBeginOffsets() { + return offsetsInfo.getBeginOffsets(); + } + + public Map getEndOffsets() { + return offsetsInfo.getEndOffsets(); + } + + public boolean assignedPartitionsFullyPolled() { + return offsetsInfo.assignedPartitionsFullyPolled(); + } + + // sum of (end - start) offsets for all partitions + public long summaryOffsetsRange() { + return offsetsInfo.summaryOffsetsRange(); + } + + // sum of differences between initial consumer seek and current consumer position (across all partitions) + public long offsetsProcessedFromSeek() { + MutableLong count = new MutableLong(); + offsetsForSeek.forEach((tp, initialOffset) -> count.add(consumer.position(tp) - initialOffset)); + return count.getValue(); + } + + // Get offsets to seek to. NOTE: offsets do not contain empty partitions offsets + public Map getOffsetsForSeek() { + return offsetsForSeek; + } + + /** + * Finds offsets for ConsumerPosition. Note: will return empty map if no offsets found for desired criteria. + */ + @VisibleForTesting + static Map getOffsetsForSeek(Consumer consumer, + OffsetsInfo offsetsInfo, + SeekTypeDTO seekType, + @Nullable Map seekTo) { + switch (seekType) { + case LATEST: + return consumer.endOffsets(offsetsInfo.getNonEmptyPartitions()); + case BEGINNING: + return consumer.beginningOffsets(offsetsInfo.getNonEmptyPartitions()); + case OFFSET: + Preconditions.checkNotNull(seekTo); + return fixOffsets(offsetsInfo, seekTo); + case TIMESTAMP: + Preconditions.checkNotNull(seekTo); + return offsetsForTimestamp(consumer, offsetsInfo, seekTo); + default: + throw new IllegalStateException(); + } + } + + private static Map fixOffsets(OffsetsInfo offsetsInfo, Map offsets) { + offsets = new HashMap<>(offsets); + offsets.keySet().retainAll(offsetsInfo.getNonEmptyPartitions()); + + Map result = new HashMap<>(); + offsets.forEach((tp, targetOffset) -> { + long endOffset = offsetsInfo.getEndOffsets().get(tp); + long beginningOffset = offsetsInfo.getBeginOffsets().get(tp); + // fixing offsets with min - max bounds + if (targetOffset > endOffset) { + targetOffset = endOffset; + } else if (targetOffset < beginningOffset) { + targetOffset = beginningOffset; + } + result.put(tp, targetOffset); + }); + return result; + } + + private static Map offsetsForTimestamp(Consumer consumer, OffsetsInfo offsetsInfo, + Map timestamps) { + timestamps = new HashMap<>(timestamps); + timestamps.keySet().retainAll(offsetsInfo.getNonEmptyPartitions()); + + return consumer.offsetsForTimes(timestamps).entrySet().stream() + .filter(e -> e.getValue() != null) + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().offset())); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/TailingEmitter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/TailingEmitter.java index d033e85e6b9..c3f04fe8cc2 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/TailingEmitter.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/TailingEmitter.java @@ -1,48 +1,59 @@ package com.provectus.kafka.ui.emitter; +import com.provectus.kafka.ui.model.ConsumerPosition; +import com.provectus.kafka.ui.model.TopicMessageDTO; import com.provectus.kafka.ui.model.TopicMessageEventDTO; -import com.provectus.kafka.ui.serde.RecordSerDe; -import com.provectus.kafka.ui.util.OffsetsSeek; +import com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer; +import java.util.HashMap; +import java.util.function.Predicate; import java.util.function.Supplier; import lombok.extern.slf4j.Slf4j; -import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.common.errors.InterruptException; -import org.apache.kafka.common.utils.Bytes; import reactor.core.publisher.FluxSink; @Slf4j -public class TailingEmitter extends AbstractEmitter - implements java.util.function.Consumer> { +public class TailingEmitter extends AbstractEmitter { - private final Supplier> consumerSupplier; - private final OffsetsSeek offsetsSeek; + private final Supplier consumerSupplier; + private final ConsumerPosition consumerPosition; - public TailingEmitter(RecordSerDe recordDeserializer, - Supplier> consumerSupplier, - OffsetsSeek offsetsSeek) { - super(recordDeserializer); + public TailingEmitter(Supplier consumerSupplier, + ConsumerPosition consumerPosition, + ConsumerRecordDeserializer deserializer, + Predicate filter, + PollingSettings pollingSettings) { + super(new MessagesProcessing(deserializer, filter, false, null), pollingSettings); this.consumerSupplier = consumerSupplier; - this.offsetsSeek = offsetsSeek; + this.consumerPosition = consumerPosition; } @Override public void accept(FluxSink sink) { - try (KafkaConsumer consumer = consumerSupplier.get()) { - log.debug("Starting topic tailing"); - offsetsSeek.assignAndSeek(consumer); + log.debug("Starting tailing polling for {}", consumerPosition); + try (EnhancedConsumer consumer = consumerSupplier.get()) { + assignAndSeek(consumer); while (!sink.isCancelled()) { sendPhase(sink, "Polling"); var polled = poll(sink, consumer); - polled.forEach(r -> sendMessage(sink, r)); + send(sink, polled); } sink.complete(); log.debug("Tailing finished"); } catch (InterruptException kafkaInterruptException) { + log.debug("Tailing finished due to thread interruption"); sink.complete(); } catch (Exception e) { - log.error("Error consuming {}", offsetsSeek.getConsumerPosition(), e); + log.error("Error consuming {}", consumerPosition, e); sink.error(e); } } + private void assignAndSeek(EnhancedConsumer consumer) { + var seekOperations = SeekOperations.create(consumer, consumerPosition); + var seekOffsets = new HashMap<>(seekOperations.getEndOffsets()); // defaulting offsets to topic end + seekOffsets.putAll(seekOperations.getOffsetsForSeek()); // this will only set non-empty partitions + consumer.assign(seekOffsets.keySet()); + seekOffsets.forEach(consumer::seek); + } + } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ErrorCode.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ErrorCode.java index 2c801d3d05c..61be8155e82 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ErrorCode.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ErrorCode.java @@ -7,6 +7,8 @@ public enum ErrorCode { + FORBIDDEN(403, HttpStatus.FORBIDDEN), + UNEXPECTED(5000, HttpStatus.INTERNAL_SERVER_ERROR), KSQL_API_ERROR(5001, HttpStatus.INTERNAL_SERVER_ERROR), BINDING_FAIL(4001, HttpStatus.BAD_REQUEST), @@ -26,7 +28,10 @@ public enum ErrorCode { INVALID_REQUEST(4014, HttpStatus.BAD_REQUEST), RECREATE_TOPIC_TIMEOUT(4015, HttpStatus.REQUEST_TIMEOUT), INVALID_ENTITY_STATE(4016, HttpStatus.BAD_REQUEST), - SCHEMA_NOT_DELETED(4017, HttpStatus.INTERNAL_SERVER_ERROR); + SCHEMA_NOT_DELETED(4017, HttpStatus.INTERNAL_SERVER_ERROR), + TOPIC_ANALYSIS_ERROR(4018, HttpStatus.BAD_REQUEST), + FILE_UPLOAD_EXCEPTION(4019, HttpStatus.INTERNAL_SERVER_ERROR), + ; static { // codes uniqueness check diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/FileUploadException.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/FileUploadException.java new file mode 100644 index 00000000000..e5e410d64a3 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/FileUploadException.java @@ -0,0 +1,19 @@ +package com.provectus.kafka.ui.exception; + +import java.nio.file.Path; + +public class FileUploadException extends CustomBaseException { + + public FileUploadException(String msg, Throwable cause) { + super(msg, cause); + } + + public FileUploadException(Path path, Throwable cause) { + super("Error uploading file %s".formatted(path), cause); + } + + @Override + public ErrorCode getErrorCode() { + return ErrorCode.FILE_UPLOAD_EXCEPTION; + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/GlobalErrorWebExceptionHandler.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/GlobalErrorWebExceptionHandler.java index 394e2aa730b..1d7cddf423f 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/GlobalErrorWebExceptionHandler.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/GlobalErrorWebExceptionHandler.java @@ -106,7 +106,7 @@ private Mono render(WebExchangeBindException exception, ServerRe err.setFieldName(e.getKey()); err.setRestrictions(List.copyOf(e.getValue())); return err; - }).collect(Collectors.toList()); + }).toList(); var message = fieldsErrors.isEmpty() ? exception.getMessage() @@ -134,7 +134,7 @@ private Mono render(ResponseStatusException exception, ServerReq .timestamp(currentTimestamp()) .stackTrace(Throwables.getStackTraceAsString(exception)); return ServerResponse - .status(exception.getStatus()) + .status(exception.getStatusCode()) .contentType(MediaType.APPLICATION_JSON) .bodyValue(response); } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/JsonAvroConversionException.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/JsonAvroConversionException.java new file mode 100644 index 00000000000..ef658031e56 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/JsonAvroConversionException.java @@ -0,0 +1,7 @@ +package com.provectus.kafka.ui.exception; + +public class JsonAvroConversionException extends ValidationException { + public JsonAvroConversionException(String message) { + super(message); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/SchemaCompatibilityException.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/SchemaCompatibilityException.java index 6ea5d12d81b..bed98d63eaa 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/SchemaCompatibilityException.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/SchemaCompatibilityException.java @@ -1,8 +1,8 @@ package com.provectus.kafka.ui.exception; public class SchemaCompatibilityException extends CustomBaseException { - public SchemaCompatibilityException(String message) { - super(message); + public SchemaCompatibilityException() { + super("Schema being registered is incompatible with an earlier schema"); } @Override diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/SchemaTypeNotSupportedException.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/SchemaTypeNotSupportedException.java deleted file mode 100644 index 9fd7a06af87..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/SchemaTypeNotSupportedException.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.provectus.kafka.ui.exception; - -public class SchemaTypeNotSupportedException extends UnprocessableEntityException { - - private static final String REQUIRED_SCHEMA_REGISTRY_VERSION = "5.5.0"; - - public SchemaTypeNotSupportedException() { - super(String.format("Current version of Schema Registry does " - + "not support provided schema type," - + " version %s or later is required here.", REQUIRED_SCHEMA_REGISTRY_VERSION)); - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/TopicAnalysisException.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/TopicAnalysisException.java new file mode 100644 index 00000000000..ecf80febc1d --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/TopicAnalysisException.java @@ -0,0 +1,13 @@ +package com.provectus.kafka.ui.exception; + +public class TopicAnalysisException extends CustomBaseException { + + public TopicAnalysisException(String message) { + super(message); + } + + @Override + public ErrorCode getErrorCode() { + return ErrorCode.TOPIC_ANALYSIS_ERROR; + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/TopicMetadataException.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/TopicMetadataException.java index 7ccceefe614..a659f94f971 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/TopicMetadataException.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/TopicMetadataException.java @@ -6,6 +6,10 @@ public TopicMetadataException(String message) { super(message); } + public TopicMetadataException(String message, Throwable cause) { + super(message, cause); + } + @Override public ErrorCode getErrorCode() { return ErrorCode.INVALID_ENTITY_STATE; diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ValidationException.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ValidationException.java index 7b964fbca53..01eac145ff0 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ValidationException.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ValidationException.java @@ -6,6 +6,10 @@ public ValidationException(String message) { super(message); } + public ValidationException(String message, Throwable cause) { + super(message, cause); + } + @Override public ErrorCode getErrorCode() { return ErrorCode.VALIDATION_FAIL; diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/ClusterMapper.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/ClusterMapper.java index 1eb6199f96c..7fb7bc5c48e 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/ClusterMapper.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/ClusterMapper.java @@ -2,66 +2,73 @@ import com.provectus.kafka.ui.config.ClustersProperties; import com.provectus.kafka.ui.model.BrokerConfigDTO; +import com.provectus.kafka.ui.model.BrokerDTO; import com.provectus.kafka.ui.model.BrokerDiskUsageDTO; import com.provectus.kafka.ui.model.BrokerMetricsDTO; import com.provectus.kafka.ui.model.ClusterDTO; +import com.provectus.kafka.ui.model.ClusterFeature; import com.provectus.kafka.ui.model.ClusterMetricsDTO; import com.provectus.kafka.ui.model.ClusterStatsDTO; -import com.provectus.kafka.ui.model.CompatibilityCheckResponseDTO; -import com.provectus.kafka.ui.model.CompatibilityLevelDTO; import com.provectus.kafka.ui.model.ConfigSourceDTO; import com.provectus.kafka.ui.model.ConfigSynonymDTO; import com.provectus.kafka.ui.model.ConnectDTO; -import com.provectus.kafka.ui.model.FailoverUrlList; -import com.provectus.kafka.ui.model.Feature; +import com.provectus.kafka.ui.model.InternalBroker; import com.provectus.kafka.ui.model.InternalBrokerConfig; import com.provectus.kafka.ui.model.InternalBrokerDiskUsage; import com.provectus.kafka.ui.model.InternalClusterState; import com.provectus.kafka.ui.model.InternalPartition; import com.provectus.kafka.ui.model.InternalReplica; -import com.provectus.kafka.ui.model.InternalSchemaRegistry; import com.provectus.kafka.ui.model.InternalTopic; import com.provectus.kafka.ui.model.InternalTopicConfig; -import com.provectus.kafka.ui.model.JmxBrokerMetrics; -import com.provectus.kafka.ui.model.KafkaCluster; -import com.provectus.kafka.ui.model.KafkaConnectCluster; +import com.provectus.kafka.ui.model.KafkaAclDTO; +import com.provectus.kafka.ui.model.KafkaAclNamePatternTypeDTO; +import com.provectus.kafka.ui.model.KafkaAclResourceTypeDTO; +import com.provectus.kafka.ui.model.MetricDTO; +import com.provectus.kafka.ui.model.Metrics; import com.provectus.kafka.ui.model.PartitionDTO; import com.provectus.kafka.ui.model.ReplicaDTO; import com.provectus.kafka.ui.model.TopicConfigDTO; import com.provectus.kafka.ui.model.TopicDTO; import com.provectus.kafka.ui.model.TopicDetailsDTO; -import com.provectus.kafka.ui.model.schemaregistry.InternalCompatibilityCheck; -import com.provectus.kafka.ui.model.schemaregistry.InternalCompatibilityLevel; -import com.provectus.kafka.ui.util.JmxClusterUtil; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.Collections; +import com.provectus.kafka.ui.model.TopicProducerStateDTO; +import com.provectus.kafka.ui.service.metrics.RawMetric; import java.util.List; import java.util.Map; -import java.util.Properties; -import java.util.stream.Collectors; import org.apache.kafka.clients.admin.ConfigEntry; +import org.apache.kafka.clients.admin.ProducerState; +import org.apache.kafka.common.acl.AccessControlEntry; +import org.apache.kafka.common.acl.AclBinding; +import org.apache.kafka.common.acl.AclOperation; +import org.apache.kafka.common.acl.AclPermissionType; +import org.apache.kafka.common.resource.PatternType; +import org.apache.kafka.common.resource.ResourcePattern; +import org.apache.kafka.common.resource.ResourceType; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.mapstruct.Named; @Mapper(componentModel = "spring") public interface ClusterMapper { ClusterDTO toCluster(InternalClusterState clusterState); - @Mapping(target = "protobufFile", source = "protobufFile", qualifiedByName = "resolvePath") - @Mapping(target = "properties", source = "properties", qualifiedByName = "setProperties") - @Mapping(target = "schemaRegistry", source = ".", qualifiedByName = "setSchemaRegistry") - KafkaCluster toKafkaCluster(ClustersProperties.Cluster clusterProperties); - ClusterStatsDTO toClusterStats(InternalClusterState clusterState); - default ClusterMetricsDTO toClusterMetrics(JmxClusterUtil.JmxMetrics jmxMetrics) { - return new ClusterMetricsDTO().items(jmxMetrics.getMetrics()); + default ClusterMetricsDTO toClusterMetrics(Metrics metrics) { + return new ClusterMetricsDTO() + .items(metrics.getSummarizedMetrics().map(this::convert).toList()); + } + + private MetricDTO convert(RawMetric rawMetric) { + return new MetricDTO() + .name(rawMetric.name()) + .labels(rawMetric.labels()) + .value(rawMetric.value()); } - BrokerMetricsDTO toBrokerMetrics(JmxBrokerMetrics metrics); + default BrokerMetricsDTO toBrokerMetrics(List metrics) { + return new BrokerMetricsDTO() + .metrics(metrics.stream().map(this::convert).toList()); + } @Mapping(target = "isSensitive", source = "sensitive") @Mapping(target = "isReadOnly", source = "readOnly") @@ -86,29 +93,7 @@ default ConfigSynonymDTO toConfigSynonym(ConfigEntry.ConfigSynonym config) { PartitionDTO toPartition(InternalPartition topic); - @Named("setSchemaRegistry") - default InternalSchemaRegistry setSchemaRegistry(ClustersProperties.Cluster clusterProperties) { - if (clusterProperties == null - || clusterProperties.getSchemaRegistry() == null) { - return null; - } - - InternalSchemaRegistry.InternalSchemaRegistryBuilder internalSchemaRegistry = - InternalSchemaRegistry.builder(); - - internalSchemaRegistry.url( - clusterProperties.getSchemaRegistry() != null - ? new FailoverUrlList(Arrays.asList(clusterProperties.getSchemaRegistry().split(","))) - : new FailoverUrlList(Collections.emptyList()) - ); - - if (clusterProperties.getSchemaRegistryAuth() != null) { - internalSchemaRegistry.username(clusterProperties.getSchemaRegistryAuth().getUsername()); - internalSchemaRegistry.password(clusterProperties.getSchemaRegistryAuth().getPassword()); - } - - return internalSchemaRegistry.build(); - } + BrokerDTO toBrokerDto(InternalBroker broker); TopicDetailsDTO toTopicDetails(InternalTopic topic); @@ -118,18 +103,12 @@ default InternalSchemaRegistry setSchemaRegistry(ClustersProperties.Cluster clus ReplicaDTO toReplica(InternalReplica replica); - ConnectDTO toKafkaConnect(KafkaConnectCluster connect); - - List toFeaturesEnum(List features); - - @Mapping(target = "isCompatible", source = "compatible") - CompatibilityCheckResponseDTO toCompatibilityCheckResponse(InternalCompatibilityCheck dto); + ConnectDTO toKafkaConnect(ClustersProperties.ConnectCluster connect); - @Mapping(target = "compatibility", source = "compatibilityLevel") - CompatibilityLevelDTO toCompatibilityLevelDto(InternalCompatibilityLevel dto); + List toFeaturesEnum(List features); default List map(Map map) { - return map.values().stream().map(this::toPartition).collect(Collectors.toList()); + return map.values().stream().map(this::toPartition).toList(); } default BrokerDiskUsageDTO map(Integer id, InternalBrokerDiskUsage internalBrokerDiskUsage) { @@ -140,22 +119,85 @@ default BrokerDiskUsageDTO map(Integer id, InternalBrokerDiskUsage internalBroke return brokerDiskUsage; } - @Named("resolvePath") - default Path resolvePath(String path) { - if (path != null) { - return Path.of(path); - } else { - return null; - } + default TopicProducerStateDTO map(int partition, ProducerState state) { + return new TopicProducerStateDTO() + .partition(partition) + .producerId(state.producerId()) + .producerEpoch(state.producerEpoch()) + .lastSequence(state.lastSequence()) + .lastTimestampMs(state.lastTimestamp()) + .coordinatorEpoch(state.coordinatorEpoch().stream().boxed().findAny().orElse(null)) + .currentTransactionStartOffset(state.currentTransactionStartOffset().stream().boxed().findAny().orElse(null)); } - @Named("setProperties") - default Properties setProperties(Properties properties) { - Properties copy = new Properties(); - if (properties != null) { - copy.putAll(properties); - } - return copy; + static KafkaAclDTO.OperationEnum mapAclOperation(AclOperation operation) { + return switch (operation) { + case ALL -> KafkaAclDTO.OperationEnum.ALL; + case READ -> KafkaAclDTO.OperationEnum.READ; + case WRITE -> KafkaAclDTO.OperationEnum.WRITE; + case CREATE -> KafkaAclDTO.OperationEnum.CREATE; + case DELETE -> KafkaAclDTO.OperationEnum.DELETE; + case ALTER -> KafkaAclDTO.OperationEnum.ALTER; + case DESCRIBE -> KafkaAclDTO.OperationEnum.DESCRIBE; + case CLUSTER_ACTION -> KafkaAclDTO.OperationEnum.CLUSTER_ACTION; + case DESCRIBE_CONFIGS -> KafkaAclDTO.OperationEnum.DESCRIBE_CONFIGS; + case ALTER_CONFIGS -> KafkaAclDTO.OperationEnum.ALTER_CONFIGS; + case IDEMPOTENT_WRITE -> KafkaAclDTO.OperationEnum.IDEMPOTENT_WRITE; + case CREATE_TOKENS -> KafkaAclDTO.OperationEnum.CREATE_TOKENS; + case DESCRIBE_TOKENS -> KafkaAclDTO.OperationEnum.DESCRIBE_TOKENS; + case ANY -> throw new IllegalArgumentException("ANY operation can be only part of filter"); + case UNKNOWN -> KafkaAclDTO.OperationEnum.UNKNOWN; + }; + } + + static KafkaAclResourceTypeDTO mapAclResourceType(ResourceType resourceType) { + return switch (resourceType) { + case CLUSTER -> KafkaAclResourceTypeDTO.CLUSTER; + case TOPIC -> KafkaAclResourceTypeDTO.TOPIC; + case GROUP -> KafkaAclResourceTypeDTO.GROUP; + case DELEGATION_TOKEN -> KafkaAclResourceTypeDTO.DELEGATION_TOKEN; + case TRANSACTIONAL_ID -> KafkaAclResourceTypeDTO.TRANSACTIONAL_ID; + case USER -> KafkaAclResourceTypeDTO.USER; + case ANY -> throw new IllegalArgumentException("ANY type can be only part of filter"); + case UNKNOWN -> KafkaAclResourceTypeDTO.UNKNOWN; + }; + } + + static ResourceType mapAclResourceTypeDto(KafkaAclResourceTypeDTO dto) { + return ResourceType.valueOf(dto.name()); + } + + static PatternType mapPatternTypeDto(KafkaAclNamePatternTypeDTO dto) { + return PatternType.valueOf(dto.name()); + } + + static AclBinding toAclBinding(KafkaAclDTO dto) { + return new AclBinding( + new ResourcePattern( + mapAclResourceTypeDto(dto.getResourceType()), + dto.getResourceName(), + mapPatternTypeDto(dto.getNamePatternType()) + ), + new AccessControlEntry( + dto.getPrincipal(), + dto.getHost(), + AclOperation.valueOf(dto.getOperation().name()), + AclPermissionType.valueOf(dto.getPermission().name()) + ) + ); + } + + static KafkaAclDTO toKafkaAclDto(AclBinding binding) { + var pattern = binding.pattern(); + var filter = binding.toFilter().entryFilter(); + return new KafkaAclDTO() + .resourceType(mapAclResourceType(pattern.resourceType())) + .resourceName(pattern.name()) + .namePatternType(KafkaAclNamePatternTypeDTO.fromValue(pattern.patternType().name())) + .principal(filter.principal()) + .host(filter.host()) + .operation(mapAclOperation(filter.operation())) + .permission(KafkaAclDTO.PermissionEnum.fromValue(filter.permissionType().name())); } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/ConsumerGroupMapper.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/ConsumerGroupMapper.java index d04484b3817..8a664882049 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/ConsumerGroupMapper.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/ConsumerGroupMapper.java @@ -6,12 +6,11 @@ import com.provectus.kafka.ui.model.ConsumerGroupStateDTO; import com.provectus.kafka.ui.model.ConsumerGroupTopicPartitionDTO; import com.provectus.kafka.ui.model.InternalConsumerGroup; +import com.provectus.kafka.ui.model.InternalTopicConsumerGroup; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.apache.kafka.common.Node; import org.apache.kafka.common.TopicPartition; @@ -24,6 +23,20 @@ public static ConsumerGroupDTO toDto(InternalConsumerGroup c) { return convertToConsumerGroup(c, new ConsumerGroupDTO()); } + public static ConsumerGroupDTO toDto(InternalTopicConsumerGroup c) { + ConsumerGroupDTO consumerGroup = new ConsumerGroupDetailsDTO(); + consumerGroup.setTopics(1); //for ui backward-compatibility, need to rm usage from ui + consumerGroup.setGroupId(c.getGroupId()); + consumerGroup.setMembers(c.getMembers()); + consumerGroup.setConsumerLag(c.getConsumerLag()); + consumerGroup.setSimple(c.isSimple()); + consumerGroup.setPartitionAssignor(c.getPartitionAssignor()); + consumerGroup.setState(mapConsumerGroupState(c.getState())); + Optional.ofNullable(c.getCoordinator()) + .ifPresent(cd -> consumerGroup.setCoordinator(mapCoordinator(cd))); + return consumerGroup; + } + public static ConsumerGroupDetailsDTO toDetailsDto(InternalConsumerGroup g) { ConsumerGroupDetailsDTO details = convertToConsumerGroup(g, new ConsumerGroupDetailsDTO()); Map partitionMap = new HashMap<>(); @@ -41,7 +54,7 @@ public static ConsumerGroupDetailsDTO toDetailsDto(InternalConsumerGroup g) { .orElse(0L); partition.setEndOffset(endOffset.orElse(0L)); - partition.setMessagesBehind(behind); + partition.setConsumerLag(behind); partitionMap.put(entry.getKey(), partition); } @@ -67,23 +80,8 @@ private static T convertToConsumerGroup( InternalConsumerGroup c, T consumerGroup) { consumerGroup.setGroupId(c.getGroupId()); consumerGroup.setMembers(c.getMembers().size()); - - int numTopics = Stream.concat( - c.getOffsets().keySet().stream().map(TopicPartition::topic), - c.getMembers().stream() - .flatMap(m -> m.getAssignment().stream().map(TopicPartition::topic)) - ).collect(Collectors.toSet()).size(); - - long messagesBehind = c.getOffsets().entrySet().stream() - .mapToLong(e -> - Optional.ofNullable(c.getEndOffsets()) - .map(o -> o.get(e.getKey())) - .map(o -> o - e.getValue()) - .orElse(0L) - ).sum(); - - consumerGroup.setMessagesBehind(messagesBehind); - consumerGroup.setTopics(numTopics); + consumerGroup.setConsumerLag(c.getConsumerLag()); + consumerGroup.setTopics(c.getTopicNum()); consumerGroup.setSimple(c.isSimple()); Optional.ofNullable(c.getState()) diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/DescribeLogDirsMapper.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/DescribeLogDirsMapper.java index c7e66d0f455..d07eff2e0a5 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/DescribeLogDirsMapper.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/DescribeLogDirsMapper.java @@ -8,6 +8,7 @@ import java.util.Map; import java.util.stream.Collectors; import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.common.protocol.Errors; import org.apache.kafka.common.requests.DescribeLogDirsResponse; import org.springframework.stereotype.Component; @@ -20,7 +21,7 @@ public List toBrokerLogDirsList( return logDirsInfo.entrySet().stream().map( mapEntry -> mapEntry.getValue().entrySet().stream() .map(e -> toBrokerLogDirs(mapEntry.getKey(), e.getKey(), e.getValue())) - .collect(Collectors.toList()) + .toList() ).flatMap(Collection::stream).collect(Collectors.toList()); } @@ -28,13 +29,13 @@ private BrokersLogdirsDTO toBrokerLogDirs(Integer broker, String dirName, DescribeLogDirsResponse.LogDirInfo logDirInfo) { BrokersLogdirsDTO result = new BrokersLogdirsDTO(); result.setName(dirName); - if (logDirInfo.error != null) { + if (logDirInfo.error != null && logDirInfo.error != Errors.NONE) { result.setError(logDirInfo.error.message()); } var topics = logDirInfo.replicaInfos.entrySet().stream() .collect(Collectors.groupingBy(e -> e.getKey().topic())).entrySet().stream() .map(e -> toTopicLogDirs(broker, e.getKey(), e.getValue())) - .collect(Collectors.toList()); + .toList(); result.setTopics(topics); return result; } @@ -47,7 +48,7 @@ private BrokerTopicLogdirsDTO toTopicLogDirs(Integer broker, String name, topic.setPartitions( partitions.stream().map( e -> topicPartitionLogDir( - broker, e.getKey().partition(), e.getValue())).collect(Collectors.toList()) + broker, e.getKey().partition(), e.getValue())).toList() ); return topic; } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/KafkaConnectMapper.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/KafkaConnectMapper.java index 468c86ecbed..a41054de6cb 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/KafkaConnectMapper.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/KafkaConnectMapper.java @@ -34,7 +34,7 @@ ConnectorPluginConfigValidationResponseDTO fromClient( com.provectus.kafka.ui.connect.model.ConnectorPluginConfigValidationResponse connectorPluginConfigValidationResponse); - default FullConnectorInfoDTO fullConnectorInfoFromTuple(InternalConnectInfo connectInfo) { + default FullConnectorInfoDTO fullConnectorInfo(InternalConnectInfo connectInfo) { ConnectorDTO connector = connectInfo.getConnector(); List tasks = connectInfo.getTasks(); int failedTasksCount = (int) tasks.stream() diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/KafkaSrMapper.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/KafkaSrMapper.java new file mode 100644 index 00000000000..9a5f68cbd12 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/KafkaSrMapper.java @@ -0,0 +1,43 @@ +package com.provectus.kafka.ui.mapper; + +import com.provectus.kafka.ui.model.CompatibilityCheckResponseDTO; +import com.provectus.kafka.ui.model.CompatibilityLevelDTO; +import com.provectus.kafka.ui.model.NewSchemaSubjectDTO; +import com.provectus.kafka.ui.model.SchemaReferenceDTO; +import com.provectus.kafka.ui.model.SchemaSubjectDTO; +import com.provectus.kafka.ui.model.SchemaTypeDTO; +import com.provectus.kafka.ui.service.SchemaRegistryService; +import com.provectus.kafka.ui.sr.model.Compatibility; +import com.provectus.kafka.ui.sr.model.CompatibilityCheckResponse; +import com.provectus.kafka.ui.sr.model.NewSubject; +import com.provectus.kafka.ui.sr.model.SchemaReference; +import com.provectus.kafka.ui.sr.model.SchemaType; +import java.util.List; +import java.util.Optional; +import org.mapstruct.Mapper; + + +@Mapper +public interface KafkaSrMapper { + + default SchemaSubjectDTO toDto(SchemaRegistryService.SubjectWithCompatibilityLevel s) { + return new SchemaSubjectDTO() + .id(s.getId()) + .version(s.getVersion()) + .subject(s.getSubject()) + .schema(s.getSchema()) + .schemaType(SchemaTypeDTO.fromValue(Optional.ofNullable(s.getSchemaType()).orElse(SchemaType.AVRO).getValue())) + .references(toDto(s.getReferences())) + .compatibilityLevel(s.getCompatibility().toString()); + } + + List toDto(List references); + + CompatibilityCheckResponseDTO toDto(CompatibilityCheckResponse ccr); + + CompatibilityLevelDTO.CompatibilityEnum toDto(Compatibility compatibility); + + NewSubject fromDto(NewSchemaSubjectDTO subjectDto); + + Compatibility fromDto(CompatibilityLevelDTO.CompatibilityEnum dtoEnum); +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/BrokerMetrics.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/BrokerMetrics.java new file mode 100644 index 00000000000..2ffc8de1850 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/BrokerMetrics.java @@ -0,0 +1,11 @@ +package com.provectus.kafka.ui.model; + +import java.util.List; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder(toBuilder = true) +public class BrokerMetrics { + private final List metrics; +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/ClusterFeature.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/ClusterFeature.java new file mode 100644 index 00000000000..2973e5500d9 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/ClusterFeature.java @@ -0,0 +1,10 @@ +package com.provectus.kafka.ui.model; + +public enum ClusterFeature { + KAFKA_CONNECT, + KSQL_DB, + SCHEMA_REGISTRY, + TOPIC_DELETION, + KAFKA_ACL_VIEW, + KAFKA_ACL_EDIT +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/ConsumerPosition.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/ConsumerPosition.java index 7c3f5a6229b..9d77923fbc6 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/ConsumerPosition.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/ConsumerPosition.java @@ -1,12 +1,14 @@ package com.provectus.kafka.ui.model; import java.util.Map; +import javax.annotation.Nullable; import lombok.Value; import org.apache.kafka.common.TopicPartition; @Value public class ConsumerPosition { SeekTypeDTO seekType; - Map seekTo; - SeekDirectionDTO seekDirection; + String topic; + @Nullable + Map seekTo; // null if positioning should apply to all tps } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/FailoverUrlList.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/FailoverUrlList.java deleted file mode 100644 index 1a760ba3396..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/FailoverUrlList.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.provectus.kafka.ui.model; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; -import lombok.experimental.Delegate; - -public class FailoverUrlList { - - public static final int DEFAULT_RETRY_GRACE_PERIOD_IN_MS = 5000; - - private final Map failures = new ConcurrentHashMap<>(); - private final AtomicInteger index = new AtomicInteger(0); - @Delegate - private final List urls; - private final int retryGracePeriodInMs; - - public FailoverUrlList(List urls) { - this(urls, DEFAULT_RETRY_GRACE_PERIOD_IN_MS); - } - - public FailoverUrlList(List urls, int retryGracePeriodInMs) { - if (urls != null && !urls.isEmpty()) { - this.urls = new ArrayList<>(urls); - } else { - throw new IllegalArgumentException("Expected at least one URL to be passed in constructor"); - } - this.retryGracePeriodInMs = retryGracePeriodInMs; - } - - public String current() { - return this.urls.get(this.index.get()); - } - - public void fail(String url) { - int currentIndex = this.index.get(); - if ((this.urls.get(currentIndex)).equals(url)) { - this.failures.put(currentIndex, Instant.now()); - this.index.compareAndSet(currentIndex, (currentIndex + 1) % this.urls.size()); - } - } - - public boolean isFailoverAvailable() { - var now = Instant.now(); - return this.urls.size() > this.failures.size() - || this.failures - .values() - .stream() - .anyMatch(e -> now.isAfter(e.plusMillis(retryGracePeriodInMs))); - } - - @Override - public String toString() { - return this.urls.toString(); - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/Feature.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/Feature.java deleted file mode 100644 index ff0e2fca4bc..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/Feature.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.provectus.kafka.ui.model; - -public enum Feature { - KAFKA_CONNECT, - KSQL_DB, - SCHEMA_REGISTRY, - TOPIC_DELETION -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalBroker.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalBroker.java new file mode 100644 index 00000000000..4a0d1ba0dd1 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalBroker.java @@ -0,0 +1,37 @@ +package com.provectus.kafka.ui.model; + +import java.math.BigDecimal; +import javax.annotation.Nullable; +import lombok.Data; +import org.apache.kafka.common.Node; + +@Data +public class InternalBroker { + + private final Integer id; + private final String host; + private final Integer port; + private final @Nullable BigDecimal bytesInPerSec; + private final @Nullable BigDecimal bytesOutPerSec; + private final @Nullable Integer partitionsLeader; + private final @Nullable Integer partitions; + private final @Nullable Integer inSyncPartitions; + private final @Nullable BigDecimal leadersSkew; + private final @Nullable BigDecimal partitionsSkew; + + public InternalBroker(Node node, + PartitionDistributionStats partitionDistribution, + Statistics statistics) { + this.id = node.id(); + this.host = node.host(); + this.port = node.port(); + this.bytesInPerSec = statistics.getMetrics().getBrokerBytesInPerSec().get(node.id()); + this.bytesOutPerSec = statistics.getMetrics().getBrokerBytesOutPerSec().get(node.id()); + this.partitionsLeader = partitionDistribution.getPartitionLeaders().get(node); + this.partitions = partitionDistribution.getPartitionsCount().get(node); + this.inSyncPartitions = partitionDistribution.getInSyncPartitions().get(node); + this.leadersSkew = partitionDistribution.leadersSkew(node); + this.partitionsSkew = partitionDistribution.partitionsSkew(node); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalClusterMetrics.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalClusterMetrics.java index b9706ef6c84..17aa8e51312 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalClusterMetrics.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalClusterMetrics.java @@ -46,10 +46,10 @@ public static InternalClusterMetrics empty() { @Nullable // will be null if log dir collection disabled private final Map internalBrokerDiskUsage; - // metrics from jmx + // metrics from metrics collector private final BigDecimal bytesInPerSec; private final BigDecimal bytesOutPerSec; - private final Map internalBrokerMetrics; + private final Map internalBrokerMetrics; private final List metrics; } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalClusterState.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalClusterState.java index 410cb6319bc..28e9a7413a3 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalClusterState.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalClusterState.java @@ -1,12 +1,12 @@ package com.provectus.kafka.ui.model; import com.google.common.base.Throwables; -import com.provectus.kafka.ui.service.MetricsCache; import java.math.BigDecimal; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import lombok.Data; +import org.apache.kafka.common.Node; @Data public class InternalClusterState { @@ -23,26 +23,28 @@ public class InternalClusterState { private Integer underReplicatedPartitionCount; private List diskUsage; private String version; - private List features; + private List features; private BigDecimal bytesInPerSec; private BigDecimal bytesOutPerSec; private Boolean readOnly; - public InternalClusterState(KafkaCluster cluster, MetricsCache.Metrics metrics) { + public InternalClusterState(KafkaCluster cluster, Statistics statistics) { name = cluster.getName(); - status = metrics.getStatus(); - lastError = Optional.ofNullable(metrics.getLastKafkaException()) + status = statistics.getStatus(); + lastError = Optional.ofNullable(statistics.getLastKafkaException()) .map(e -> new MetricsCollectionErrorDTO() .message(e.getMessage()) .stackTrace(Throwables.getStackTraceAsString(e))) .orElse(null); - topicCount = metrics.getTopicDescriptions().size(); - brokerCount = metrics.getClusterDescription().getNodes().size(); - activeControllers = metrics.getClusterDescription().getController() != null ? 1 : 0; - version = metrics.getVersion(); + topicCount = statistics.getTopicDescriptions().size(); + brokerCount = statistics.getClusterDescription().getNodes().size(); + activeControllers = Optional.ofNullable(statistics.getClusterDescription().getController()) + .map(Node::id) + .orElse(null); + version = statistics.getVersion(); - if (metrics.getLogDirInfo() != null) { - diskUsage = metrics.getLogDirInfo().getBrokerStats().entrySet().stream() + if (statistics.getLogDirInfo() != null) { + diskUsage = statistics.getLogDirInfo().getBrokerStats().entrySet().stream() .map(e -> new BrokerDiskUsageDTO() .brokerId(e.getKey()) .segmentSize(e.getValue().getSegmentSize()) @@ -50,15 +52,23 @@ public InternalClusterState(KafkaCluster cluster, MetricsCache.Metrics metrics) .collect(Collectors.toList()); } - features = metrics.getFeatures(); + features = statistics.getFeatures(); - bytesInPerSec = metrics.getJmxMetrics().getBytesInPerSec().values().stream() - .reduce(BigDecimal.ZERO, BigDecimal::add); + bytesInPerSec = statistics + .getMetrics() + .getBrokerBytesInPerSec() + .values().stream() + .reduce(BigDecimal::add) + .orElse(null); - bytesOutPerSec = metrics.getJmxMetrics().getBytesOutPerSec().values().stream() - .reduce(BigDecimal.ZERO, BigDecimal::add); + bytesOutPerSec = statistics + .getMetrics() + .getBrokerBytesOutPerSec() + .values().stream() + .reduce(BigDecimal::add) + .orElse(null); - var partitionsStats = new PartitionsStats(metrics.getTopicDescriptions().values()); + var partitionsStats = new PartitionsStats(statistics.getTopicDescriptions().values()); onlinePartitionCount = partitionsStats.getOnlinePartitionCount(); offlinePartitionCount = partitionsStats.getOfflinePartitionCount(); inSyncReplicasCount = partitionsStats.getInSyncReplicasCount(); diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalConsumerGroup.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalConsumerGroup.java index df5df2ef1f0..97cbfe4868c 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalConsumerGroup.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalConsumerGroup.java @@ -4,8 +4,8 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.Builder; import lombok.Data; import org.apache.kafka.clients.admin.ConsumerGroupDescription; @@ -21,6 +21,8 @@ public class InternalConsumerGroup { private final Collection members; private final Map offsets; private final Map endOffsets; + private final Long consumerLag; + private final Integer topicNum; private final String partitionAssignor; private final ConsumerGroupState state; private final Node coordinator; @@ -44,51 +46,55 @@ public static InternalConsumerGroup create( builder.simple(description.isSimpleConsumerGroup()); builder.state(description.state()); builder.partitionAssignor(description.partitionAssignor()); - builder.members( - description.members().stream() - .map(m -> - InternalConsumerGroup.InternalMember.builder() - .assignment(m.assignment().topicPartitions()) - .clientId(m.clientId()) - .groupInstanceId(m.groupInstanceId().orElse("")) - .consumerId(m.consumerId()) - .clientId(m.clientId()) - .host(m.host()) - .build() - ).collect(Collectors.toList()) - ); + Collection internalMembers = initInternalMembers(description); + builder.members(internalMembers); builder.offsets(groupOffsets); builder.endOffsets(topicEndOffsets); + builder.consumerLag(calculateConsumerLag(groupOffsets, topicEndOffsets)); + builder.topicNum(calculateTopicNum(groupOffsets, internalMembers)); Optional.ofNullable(description.coordinator()).ifPresent(builder::coordinator); return builder.build(); } - // removes data for all partitions that are not fit filter - public InternalConsumerGroup retainDataForPartitions(Predicate partitionsFilter) { - var offsetsMap = getOffsets().entrySet().stream() - .filter(e -> partitionsFilter.test(e.getKey())) - .collect(Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue - )); + private static Long calculateConsumerLag(Map offsets, Map endOffsets) { + Long consumerLag = null; + // consumerLag should be undefined if no committed offsets found for topic + if (!offsets.isEmpty()) { + consumerLag = offsets.entrySet().stream() + .mapToLong(e -> + Optional.ofNullable(endOffsets) + .map(o -> o.get(e.getKey())) + .map(o -> o - e.getValue()) + .orElse(0L) + ).sum(); + } - var nonEmptyMembers = getMembers().stream() - .map(m -> filterConsumerMemberTopic(m, partitionsFilter)) - .filter(m -> !m.getAssignment().isEmpty()) - .collect(Collectors.toList()); + return consumerLag; + } + + private static Integer calculateTopicNum(Map offsets, Collection members) { + + return (int) Stream.concat( + offsets.keySet().stream().map(TopicPartition::topic), + members.stream() + .flatMap(m -> m.getAssignment().stream().map(TopicPartition::topic)) + ).distinct().count(); - return toBuilder() - .offsets(offsetsMap) - .members(nonEmptyMembers) - .build(); } - private InternalConsumerGroup.InternalMember filterConsumerMemberTopic( - InternalConsumerGroup.InternalMember member, Predicate partitionsFilter) { - var topicPartitions = member.getAssignment() - .stream() - .filter(partitionsFilter) - .collect(Collectors.toSet()); - return member.toBuilder().assignment(topicPartitions).build(); + private static Collection initInternalMembers(ConsumerGroupDescription description) { + return description.members().stream() + .map(m -> + InternalConsumerGroup.InternalMember.builder() + .assignment(m.assignment().topicPartitions()) + .clientId(m.clientId()) + .groupInstanceId(m.groupInstanceId().orElse("")) + .consumerId(m.consumerId()) + .clientId(m.clientId()) + .host(m.host()) + .build() + ).collect(Collectors.toList()); } + + } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalLogDirStats.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalLogDirStats.java index 34ec3d59e3e..7fcecd9ab66 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalLogDirStats.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalLogDirStats.java @@ -44,7 +44,7 @@ public InternalLogDirStats(Map Tuples.of(b.getKey(), e.getKey(), e.getValue().size)) ) - ).collect(toList()); + ).toList(); partitionsStats = topicPartitions.stream().collect( groupingBy( diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalPartition.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalPartition.java index 76f916cb105..f5d16e0b65d 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalPartition.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalPartition.java @@ -13,12 +13,12 @@ public class InternalPartition { private final int inSyncReplicasCount; private final int replicasCount; - private final long offsetMin; - private final long offsetMax; + private final Long offsetMin; + private final Long offsetMax; // from log dir - private final long segmentSize; - private final long segmentCount; + private final Long segmentSize; + private final Integer segmentCount; } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalSchemaRegistry.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalSchemaRegistry.java deleted file mode 100644 index 1115bac1057..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalSchemaRegistry.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.provectus.kafka.ui.model; - -import lombok.Builder; -import lombok.Data; - -@Data -@Builder(toBuilder = true) -public class InternalSchemaRegistry { - private final String username; - private final String password; - private final FailoverUrlList url; - - public String getPrimaryNodeUri() { - return url.get(0); - } - - public String getUri() { - return url.current(); - } - - public void markAsUnavailable(String url) { - this.url.fail(url); - } - - public boolean isFailoverAvailable() { - return this.url.isFailoverAvailable(); - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalTopic.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalTopic.java index c6cc4b1b081..43a6012d215 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalTopic.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalTopic.java @@ -1,10 +1,11 @@ package com.provectus.kafka.ui.model; -import com.provectus.kafka.ui.util.JmxClusterUtil; +import com.provectus.kafka.ui.config.ClustersProperties; import java.math.BigDecimal; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import javax.annotation.Nullable; import lombok.Builder; import lombok.Data; import org.apache.kafka.clients.admin.ConfigEntry; @@ -15,6 +16,8 @@ @Builder(toBuilder = true) public class InternalTopic { + ClustersProperties clustersProperties; + // from TopicDescription private final String name; private final boolean internal; @@ -29,7 +32,7 @@ public class InternalTopic { private final List topicConfigs; private final CleanupPolicy cleanUpPolicy; - // rates from jmx + // rates from metrics private final BigDecimal bytesInPerSec; private final BigDecimal bytesOutPerSec; @@ -40,11 +43,17 @@ public class InternalTopic { public static InternalTopic from(TopicDescription topicDescription, List configs, InternalPartitionsOffsets partitionsOffsets, - JmxClusterUtil.JmxMetrics jmxMetrics, - InternalLogDirStats logDirInfo) { + Metrics metrics, + InternalLogDirStats logDirInfo, + @Nullable String internalTopicPrefix) { var topic = InternalTopic.builder(); + + internalTopicPrefix = internalTopicPrefix == null || internalTopicPrefix.isEmpty() + ? "_" + : internalTopicPrefix; + topic.internal( - topicDescription.isInternal() || topicDescription.name().startsWith("_") + topicDescription.isInternal() || topicDescription.name().startsWith(internalTopicPrefix) ); topic.name(topicDescription.name()); @@ -57,9 +66,12 @@ public static InternalTopic from(TopicDescription topicDescription, partitionDto.inSyncReplicasCount(partition.isr().size()); partitionDto.replicasCount(partition.replicas().size()); List replicas = partition.replicas().stream() - .map(r -> new InternalReplica(r.id(), - partition.leader() != null && partition.leader().id() != r.id(), - partition.isr().contains(r))) + .map(r -> + InternalReplica.builder() + .broker(r.id()) + .inSync(partition.isr().contains(r)) + .leader(partition.leader() != null && partition.leader().id() == r.id()) + .build()) .collect(Collectors.toList()); partitionDto.replicas(replicas); @@ -79,7 +91,7 @@ public static InternalTopic from(TopicDescription topicDescription, return partitionDto.build(); }) - .collect(Collectors.toList()); + .toList(); topic.partitions(partitions.stream().collect( Collectors.toMap(InternalPartition::getPartition, t -> t))); @@ -102,8 +114,8 @@ public static InternalTopic from(TopicDescription topicDescription, topic.segmentSize(segmentStats.getSegmentSize()); } - topic.bytesInPerSec(jmxMetrics.getBytesInPerSec().get(topicDescription.name())); - topic.bytesOutPerSec(jmxMetrics.getBytesOutPerSec().get(topicDescription.name())); + topic.bytesInPerSec(metrics.getTopicBytesInPerSec().get(topicDescription.name())); + topic.bytesOutPerSec(metrics.getTopicBytesOutPerSec().get(topicDescription.name())); topic.topicConfigs( configs.stream().map(InternalTopicConfig::from).collect(Collectors.toList())); diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalTopicConfig.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalTopicConfig.java index 294894ebc2f..d061dd49813 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalTopicConfig.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalTopicConfig.java @@ -1,8 +1,5 @@ package com.provectus.kafka.ui.model; -import static com.provectus.kafka.ui.util.KafkaConstants.TOPIC_DEFAULT_CONFIGS; -import static org.apache.kafka.common.config.TopicConfig.MESSAGE_FORMAT_VERSION_CONFIG; - import java.util.List; import lombok.Builder; import lombok.Data; @@ -19,6 +16,7 @@ public class InternalTopicConfig { private final boolean isSensitive; private final boolean isReadOnly; private final List synonyms; + private final String doc; public static InternalTopicConfig from(ConfigEntry configEntry) { InternalTopicConfig.InternalTopicConfigBuilder builder = InternalTopicConfig.builder() @@ -27,11 +25,22 @@ public static InternalTopicConfig from(ConfigEntry configEntry) { .source(configEntry.source()) .isReadOnly(configEntry.isReadOnly()) .isSensitive(configEntry.isSensitive()) - .synonyms(configEntry.synonyms()); - if (configEntry.name().equals(MESSAGE_FORMAT_VERSION_CONFIG)) { + .synonyms(configEntry.synonyms()) + .doc(configEntry.documentation()); + + if (configEntry.source() == ConfigEntry.ConfigSource.DEFAULT_CONFIG) { + // this is important case, because for some configs like "confluent.*" no synonyms returned, but + // they are set by default and "source" == DEFAULT_CONFIG builder.defaultValue(configEntry.value()); } else { - builder.defaultValue(TOPIC_DEFAULT_CONFIGS.get(configEntry.name())); + // normally by default first entity of synonyms values will be used. + configEntry.synonyms().stream() + // skipping DYNAMIC_TOPIC_CONFIG value - which is explicitly set value when + // topic was created (not default), see ConfigEntry.synonyms() doc + .filter(s -> s.source() != ConfigEntry.ConfigSource.DYNAMIC_TOPIC_CONFIG) + .map(ConfigEntry.ConfigSynonym::value) + .findFirst() + .ifPresent(builder::defaultValue); } return builder.build(); } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalTopicConsumerGroup.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalTopicConsumerGroup.java new file mode 100644 index 00000000000..529c1b954c9 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalTopicConsumerGroup.java @@ -0,0 +1,61 @@ +package com.provectus.kafka.ui.model; + +import java.util.Map; +import java.util.Optional; +import javax.annotation.Nullable; +import lombok.Builder; +import lombok.Value; +import org.apache.kafka.clients.admin.ConsumerGroupDescription; +import org.apache.kafka.common.ConsumerGroupState; +import org.apache.kafka.common.Node; +import org.apache.kafka.common.TopicPartition; + +@Value +@Builder +public class InternalTopicConsumerGroup { + + String groupId; + int members; + @Nullable + Long consumerLag; //null means no committed offsets found for this group + boolean isSimple; + String partitionAssignor; + ConsumerGroupState state; + @Nullable + Node coordinator; + + public static InternalTopicConsumerGroup create( + String topic, + ConsumerGroupDescription g, + Map committedOffsets, + Map endOffsets) { + return InternalTopicConsumerGroup.builder() + .groupId(g.groupId()) + .members( + (int) g.members().stream() + // counting only members with target topic assignment + .filter(m -> m.assignment().topicPartitions().stream().anyMatch(p -> p.topic().equals(topic))) + .count() + ) + .consumerLag(calculateConsumerLag(committedOffsets, endOffsets)) + .isSimple(g.isSimpleConsumerGroup()) + .partitionAssignor(g.partitionAssignor()) + .state(g.state()) + .coordinator(g.coordinator()) + .build(); + } + + @Nullable + private static Long calculateConsumerLag(Map committedOffsets, + Map endOffsets) { + if (committedOffsets.isEmpty()) { + return null; + } + return committedOffsets.entrySet().stream() + .mapToLong(e -> + Optional.ofNullable(endOffsets.get(e.getKey())) + .map(o -> o - e.getValue()) + .orElse(0L) + ).sum(); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/JmxBrokerMetrics.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/JmxBrokerMetrics.java deleted file mode 100644 index e57cbbad308..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/JmxBrokerMetrics.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.provectus.kafka.ui.model; - -import java.util.List; -import lombok.Builder; -import lombok.Data; - -@Data -@Builder(toBuilder = true) -public class JmxBrokerMetrics { - private final List metrics; -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/JmxConnectionInfo.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/JmxConnectionInfo.java deleted file mode 100644 index de80b25be3e..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/JmxConnectionInfo.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.provectus.kafka.ui.model; - -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.RequiredArgsConstructor; - -@Data -@RequiredArgsConstructor -@Builder -@EqualsAndHashCode(onlyExplicitlyIncluded = true) -public class JmxConnectionInfo { - - @EqualsAndHashCode.Include - private final String url; - private final boolean ssl; - private final String username; - private final String password; - - public JmxConnectionInfo(String url) { - this.url = url; - this.ssl = false; - this.username = null; - this.password = null; - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/KafkaCluster.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/KafkaCluster.java index b9f1ea96768..1e2903dbcc9 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/KafkaCluster.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/KafkaCluster.java @@ -1,7 +1,12 @@ package com.provectus.kafka.ui.model; -import java.nio.file.Path; -import java.util.List; +import com.provectus.kafka.ui.config.ClustersProperties; +import com.provectus.kafka.ui.connect.api.KafkaConnectClientApi; +import com.provectus.kafka.ui.emitter.PollingSettings; +import com.provectus.kafka.ui.service.ksql.KsqlApiClient; +import com.provectus.kafka.ui.service.masking.DataMasking; +import com.provectus.kafka.ui.sr.api.KafkaSrClientApi; +import com.provectus.kafka.ui.util.ReactiveFailover; import java.util.Map; import java.util.Properties; import lombok.AccessLevel; @@ -13,24 +18,17 @@ @Builder(toBuilder = true) @AllArgsConstructor(access = AccessLevel.PRIVATE) public class KafkaCluster { + private final ClustersProperties.Cluster originalProperties; + private final String name; private final String version; - private final Integer jmxPort; - private final boolean jmxSsl; - private final String jmxUsername; - private final String jmxPassword; private final String bootstrapServers; - private final InternalSchemaRegistry schemaRegistry; - private final String ksqldbServer; - private final List kafkaConnect; - private final String schemaNameTemplate; - private final String keySchemaNameTemplate; - private final Path protobufFile; - private final String protobufMessageName; - private final Map protobufMessageNameByTopic; - private final String protobufMessageNameForKey; - private final Map protobufMessageNameForKeyByTopic; private final Properties properties; private final boolean readOnly; - private final boolean disableLogDirsCollection; + private final MetricsConfig metricsConfig; + private final DataMasking masking; + private final PollingSettings pollingSettings; + private final ReactiveFailover schemaRegistryClient; + private final Map> connectsClients; + private final ReactiveFailover ksqlClient; } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/KafkaConnectCluster.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/KafkaConnectCluster.java deleted file mode 100644 index 6131f3fa9e9..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/KafkaConnectCluster.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.provectus.kafka.ui.model; - -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; - -@Data -@Builder(toBuilder = true) -@AllArgsConstructor(access = AccessLevel.PRIVATE) -public class KafkaConnectCluster { - private final String name; - private final String address; - private final String userName; - private final String password; -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/Metrics.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/Metrics.java new file mode 100644 index 00000000000..02bfe6dea13 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/Metrics.java @@ -0,0 +1,43 @@ +package com.provectus.kafka.ui.model; + +import static java.util.stream.Collectors.toMap; + +import com.provectus.kafka.ui.service.metrics.RawMetric; +import java.math.BigDecimal; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import lombok.Builder; +import lombok.Value; + + +@Builder +@Value +public class Metrics { + + Map brokerBytesInPerSec; + Map brokerBytesOutPerSec; + Map topicBytesInPerSec; + Map topicBytesOutPerSec; + Map> perBrokerMetrics; + + public static Metrics empty() { + return Metrics.builder() + .brokerBytesInPerSec(Map.of()) + .brokerBytesOutPerSec(Map.of()) + .topicBytesInPerSec(Map.of()) + .topicBytesOutPerSec(Map.of()) + .perBrokerMetrics(Map.of()) + .build(); + } + + public Stream getSummarizedMetrics() { + return perBrokerMetrics.values().stream() + .flatMap(Collection::stream) + .collect(toMap(RawMetric::identityKey, m -> m, (m1, m2) -> m1.copyWithValue(m1.value().add(m2.value())))) + .values() + .stream(); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/MetricsConfig.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/MetricsConfig.java new file mode 100644 index 00000000000..d3551443437 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/MetricsConfig.java @@ -0,0 +1,22 @@ +package com.provectus.kafka.ui.model; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder(toBuilder = true) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class MetricsConfig { + public static final String JMX_METRICS_TYPE = "JMX"; + public static final String PROMETHEUS_METRICS_TYPE = "PROMETHEUS"; + + private final String type; + private final Integer port; + private final boolean ssl; + private final String username; + private final String password; + private final String keystoreLocation; + private final String keystorePassword; +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/PartitionDistributionStats.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/PartitionDistributionStats.java new file mode 100644 index 00000000000..46efc670008 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/PartitionDistributionStats.java @@ -0,0 +1,92 @@ +package com.provectus.kafka.ui.model; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.admin.TopicDescription; +import org.apache.kafka.common.Node; +import org.apache.kafka.common.TopicPartitionInfo; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +@Slf4j +public class PartitionDistributionStats { + + // avg skew will show unuseful results on low number of partitions + private static final int MIN_PARTITIONS_FOR_SKEW_CALCULATION = 50; + + private final Map partitionLeaders; + private final Map partitionsCount; + private final Map inSyncPartitions; + private final double avgLeadersCntPerBroker; + private final double avgPartitionsPerBroker; + private final boolean skewCanBeCalculated; + + public static PartitionDistributionStats create(Statistics stats) { + return create(stats, MIN_PARTITIONS_FOR_SKEW_CALCULATION); + } + + static PartitionDistributionStats create(Statistics stats, int minPartitionsForSkewCalculation) { + var partitionLeaders = new HashMap(); + var partitionsReplicated = new HashMap(); + var isr = new HashMap(); + int partitionsCnt = 0; + for (TopicDescription td : stats.getTopicDescriptions().values()) { + for (TopicPartitionInfo tp : td.partitions()) { + partitionsCnt++; + tp.replicas().forEach(r -> incr(partitionsReplicated, r)); + tp.isr().forEach(r -> incr(isr, r)); + if (tp.leader() != null) { + incr(partitionLeaders, tp.leader()); + } + } + } + int nodesWithPartitions = partitionsReplicated.size(); + int partitionReplications = partitionsReplicated.values().stream().mapToInt(i -> i).sum(); + var avgPartitionsPerBroker = nodesWithPartitions == 0 ? 0 : ((double) partitionReplications) / nodesWithPartitions; + + int nodesWithLeaders = partitionLeaders.size(); + int leadersCnt = partitionLeaders.values().stream().mapToInt(i -> i).sum(); + var avgLeadersCntPerBroker = nodesWithLeaders == 0 ? 0 : ((double) leadersCnt) / nodesWithLeaders; + + return new PartitionDistributionStats( + partitionLeaders, + partitionsReplicated, + isr, + avgLeadersCntPerBroker, + avgPartitionsPerBroker, + partitionsCnt >= minPartitionsForSkewCalculation + ); + } + + private static void incr(Map map, Node n) { + map.compute(n, (k, c) -> c == null ? 1 : ++c); + } + + @Nullable + public BigDecimal partitionsSkew(Node node) { + return calculateAvgSkew(partitionsCount.get(node), avgPartitionsPerBroker); + } + + @Nullable + public BigDecimal leadersSkew(Node node) { + return calculateAvgSkew(partitionLeaders.get(node), avgLeadersCntPerBroker); + } + + // Returns difference (in percents) from average value, null if it can't be calculated + @Nullable + private BigDecimal calculateAvgSkew(@Nullable Integer value, double avgValue) { + if (avgValue == 0 || !skewCanBeCalculated) { + return null; + } + value = value == null ? 0 : value; + return new BigDecimal((value - avgValue) / avgValue * 100.0) + .setScale(1, RoundingMode.HALF_UP); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/Statistics.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/Statistics.java new file mode 100644 index 00000000000..e70547f1437 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/Statistics.java @@ -0,0 +1,38 @@ +package com.provectus.kafka.ui.model; + +import com.provectus.kafka.ui.service.ReactiveAdminClient; +import java.util.List; +import java.util.Map; +import java.util.Set; +import lombok.Builder; +import lombok.Value; +import org.apache.kafka.clients.admin.ConfigEntry; +import org.apache.kafka.clients.admin.TopicDescription; + +@Value +@Builder(toBuilder = true) +public class Statistics { + ServerStatusDTO status; + Throwable lastKafkaException; + String version; + List features; + ReactiveAdminClient.ClusterDescription clusterDescription; + Metrics metrics; + InternalLogDirStats logDirInfo; + Map topicDescriptions; + Map> topicConfigs; + + public static Statistics empty() { + return builder() + .status(ServerStatusDTO.OFFLINE) + .version("Unknown") + .features(List.of()) + .clusterDescription( + new ReactiveAdminClient.ClusterDescription(null, null, List.of(), Set.of())) + .metrics(Metrics.empty()) + .logDirInfo(InternalLogDirStats.empty()) + .topicDescriptions(Map.of()) + .topicConfigs(Map.of()) + .build(); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/AccessContext.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/AccessContext.java new file mode 100644 index 00000000000..dfa6ad7b371 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/AccessContext.java @@ -0,0 +1,191 @@ +package com.provectus.kafka.ui.model.rbac; + +import com.provectus.kafka.ui.model.rbac.permission.AclAction; +import com.provectus.kafka.ui.model.rbac.permission.ApplicationConfigAction; +import com.provectus.kafka.ui.model.rbac.permission.AuditAction; +import com.provectus.kafka.ui.model.rbac.permission.ClusterConfigAction; +import com.provectus.kafka.ui.model.rbac.permission.ConnectAction; +import com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction; +import com.provectus.kafka.ui.model.rbac.permission.KsqlAction; +import com.provectus.kafka.ui.model.rbac.permission.SchemaAction; +import com.provectus.kafka.ui.model.rbac.permission.TopicAction; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import lombok.Value; +import org.springframework.util.Assert; + +@Value +public class AccessContext { + + Collection applicationConfigActions; + + String cluster; + Collection clusterConfigActions; + + String topic; + Collection topicActions; + + String consumerGroup; + Collection consumerGroupActions; + + String connect; + Collection connectActions; + + String connector; + + String schema; + Collection schemaActions; + + Collection ksqlActions; + + Collection aclActions; + + Collection auditAction; + + String operationName; + Object operationParams; + + public static AccessContextBuilder builder() { + return new AccessContextBuilder(); + } + + public static final class AccessContextBuilder { + private static final String ACTIONS_NOT_PRESENT = "actions not present"; + + private Collection applicationConfigActions = Collections.emptySet(); + private String cluster; + private Collection clusterConfigActions = Collections.emptySet(); + private String topic; + private Collection topicActions = Collections.emptySet(); + private String consumerGroup; + private Collection consumerGroupActions = Collections.emptySet(); + private String connect; + private Collection connectActions = Collections.emptySet(); + private String connector; + private String schema; + private Collection schemaActions = Collections.emptySet(); + private Collection ksqlActions = Collections.emptySet(); + private Collection aclActions = Collections.emptySet(); + private Collection auditActions = Collections.emptySet(); + + private String operationName; + private Object operationParams; + + private AccessContextBuilder() { + } + + public AccessContextBuilder applicationConfigActions(ApplicationConfigAction... actions) { + Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT); + this.applicationConfigActions = List.of(actions); + return this; + } + + public AccessContextBuilder cluster(String cluster) { + this.cluster = cluster; + return this; + } + + public AccessContextBuilder clusterConfigActions(ClusterConfigAction... actions) { + Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT); + this.clusterConfigActions = List.of(actions); + return this; + } + + public AccessContextBuilder topic(String topic) { + this.topic = topic; + return this; + } + + public AccessContextBuilder topicActions(TopicAction... actions) { + Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT); + this.topicActions = List.of(actions); + return this; + } + + public AccessContextBuilder consumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + return this; + } + + public AccessContextBuilder consumerGroupActions(ConsumerGroupAction... actions) { + Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT); + this.consumerGroupActions = List.of(actions); + return this; + } + + public AccessContextBuilder connect(String connect) { + this.connect = connect; + return this; + } + + public AccessContextBuilder connectActions(ConnectAction... actions) { + Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT); + this.connectActions = List.of(actions); + return this; + } + + public AccessContextBuilder connector(String connector) { + this.connector = connector; + return this; + } + + public AccessContextBuilder schema(String schema) { + this.schema = schema; + return this; + } + + public AccessContextBuilder schemaActions(SchemaAction... actions) { + Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT); + this.schemaActions = List.of(actions); + return this; + } + + public AccessContextBuilder ksqlActions(KsqlAction... actions) { + Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT); + this.ksqlActions = List.of(actions); + return this; + } + + public AccessContextBuilder aclActions(AclAction... actions) { + Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT); + this.aclActions = List.of(actions); + return this; + } + + public AccessContextBuilder auditActions(AuditAction... actions) { + Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT); + this.auditActions = List.of(actions); + return this; + } + + public AccessContextBuilder operationName(String operationName) { + this.operationName = operationName; + return this; + } + + public AccessContextBuilder operationParams(Object operationParams) { + this.operationParams = operationParams; + return this; + } + + public AccessContextBuilder operationParams(Map paramsMap) { + this.operationParams = paramsMap; + return this; + } + + public AccessContext build() { + return new AccessContext( + applicationConfigActions, + cluster, clusterConfigActions, + topic, topicActions, + consumerGroup, consumerGroupActions, + connect, connectActions, + connector, + schema, schemaActions, + ksqlActions, aclActions, auditActions, + operationName, operationParams); + } + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Permission.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Permission.java new file mode 100644 index 00000000000..69c16af5720 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Permission.java @@ -0,0 +1,94 @@ +package com.provectus.kafka.ui.model.rbac; + +import static com.provectus.kafka.ui.model.rbac.Resource.ACL; +import static com.provectus.kafka.ui.model.rbac.Resource.APPLICATIONCONFIG; +import static com.provectus.kafka.ui.model.rbac.Resource.AUDIT; +import static com.provectus.kafka.ui.model.rbac.Resource.CLUSTERCONFIG; +import static com.provectus.kafka.ui.model.rbac.Resource.KSQL; + +import com.provectus.kafka.ui.model.rbac.permission.AclAction; +import com.provectus.kafka.ui.model.rbac.permission.ApplicationConfigAction; +import com.provectus.kafka.ui.model.rbac.permission.AuditAction; +import com.provectus.kafka.ui.model.rbac.permission.ClusterConfigAction; +import com.provectus.kafka.ui.model.rbac.permission.ConnectAction; +import com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction; +import com.provectus.kafka.ui.model.rbac.permission.KsqlAction; +import com.provectus.kafka.ui.model.rbac.permission.SchemaAction; +import com.provectus.kafka.ui.model.rbac.permission.TopicAction; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; +import javax.annotation.Nullable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.util.Assert; + +@Getter +@ToString +@EqualsAndHashCode +public class Permission { + + private static final List RBAC_ACTION_EXEMPT_LIST = + List.of(KSQL, CLUSTERCONFIG, APPLICATIONCONFIG, ACL, AUDIT); + + Resource resource; + List actions; + + @Nullable + String value; + @Nullable + transient Pattern compiledValuePattern; + + @SuppressWarnings("unused") + public void setResource(String resource) { + this.resource = Resource.fromString(resource.toUpperCase()); + } + + @SuppressWarnings("unused") + public void setValue(@Nullable String value) { + this.value = value; + } + + @SuppressWarnings("unused") + public void setActions(List actions) { + this.actions = actions; + } + + public void validate() { + Assert.notNull(resource, "resource cannot be null"); + if (!RBAC_ACTION_EXEMPT_LIST.contains(this.resource)) { + Assert.notNull(value, "permission value can't be empty for resource " + resource); + } + } + + public void transform() { + if (value != null) { + this.compiledValuePattern = Pattern.compile(value); + } + if (CollectionUtils.isNotEmpty(actions) && actions.stream().anyMatch("ALL"::equalsIgnoreCase)) { + this.actions = getAllActionValues(); + } + } + + private List getAllActionValues() { + if (resource == null) { + return Collections.emptyList(); + } + + return switch (this.resource) { + case APPLICATIONCONFIG -> Arrays.stream(ApplicationConfigAction.values()).map(Enum::toString).toList(); + case CLUSTERCONFIG -> Arrays.stream(ClusterConfigAction.values()).map(Enum::toString).toList(); + case TOPIC -> Arrays.stream(TopicAction.values()).map(Enum::toString).toList(); + case CONSUMER -> Arrays.stream(ConsumerGroupAction.values()).map(Enum::toString).toList(); + case SCHEMA -> Arrays.stream(SchemaAction.values()).map(Enum::toString).toList(); + case CONNECT -> Arrays.stream(ConnectAction.values()).map(Enum::toString).toList(); + case KSQL -> Arrays.stream(KsqlAction.values()).map(Enum::toString).toList(); + case ACL -> Arrays.stream(AclAction.values()).map(Enum::toString).toList(); + case AUDIT -> Arrays.stream(AuditAction.values()).map(Enum::toString).toList(); + }; + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Resource.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Resource.java new file mode 100644 index 00000000000..ca2efab3a9a --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Resource.java @@ -0,0 +1,24 @@ +package com.provectus.kafka.ui.model.rbac; + +import org.apache.commons.lang3.EnumUtils; +import org.jetbrains.annotations.Nullable; + +public enum Resource { + + APPLICATIONCONFIG, + CLUSTERCONFIG, + TOPIC, + CONSUMER, + SCHEMA, + CONNECT, + KSQL, + ACL, + AUDIT; + + @Nullable + public static Resource fromString(String name) { + return EnumUtils.getEnum(Resource.class, name); + } + + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Role.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Role.java new file mode 100644 index 00000000000..96a03e08a3c --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Role.java @@ -0,0 +1,19 @@ +package com.provectus.kafka.ui.model.rbac; + +import java.util.List; +import lombok.Data; + +@Data +public class Role { + + String name; + List clusters; + List subjects; + List permissions; + + public void validate() { + permissions.forEach(Permission::transform); + permissions.forEach(Permission::validate); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Subject.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Subject.java new file mode 100644 index 00000000000..851a3d1fc4a --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Subject.java @@ -0,0 +1,24 @@ +package com.provectus.kafka.ui.model.rbac; + +import com.provectus.kafka.ui.model.rbac.provider.Provider; +import lombok.Getter; + +@Getter +public class Subject { + + Provider provider; + String type; + String value; + + public void setProvider(String provider) { + this.provider = Provider.fromString(provider.toUpperCase()); + } + + public void setType(String type) { + this.type = type; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/AclAction.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/AclAction.java new file mode 100644 index 00000000000..8e7d2673d6d --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/AclAction.java @@ -0,0 +1,25 @@ +package com.provectus.kafka.ui.model.rbac.permission; + +import java.util.Set; +import org.apache.commons.lang3.EnumUtils; +import org.jetbrains.annotations.Nullable; + +public enum AclAction implements PermissibleAction { + + VIEW, + EDIT + + ; + + public static final Set ALTER_ACTIONS = Set.of(EDIT); + + @Nullable + public static AclAction fromString(String name) { + return EnumUtils.getEnum(AclAction.class, name); + } + + @Override + public boolean isAlter() { + return ALTER_ACTIONS.contains(this); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ApplicationConfigAction.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ApplicationConfigAction.java new file mode 100644 index 00000000000..e04ff3570c7 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ApplicationConfigAction.java @@ -0,0 +1,25 @@ +package com.provectus.kafka.ui.model.rbac.permission; + +import java.util.Set; +import org.apache.commons.lang3.EnumUtils; +import org.jetbrains.annotations.Nullable; + +public enum ApplicationConfigAction implements PermissibleAction { + + VIEW, + EDIT + + ; + + public static final Set ALTER_ACTIONS = Set.of(EDIT); + + @Nullable + public static ApplicationConfigAction fromString(String name) { + return EnumUtils.getEnum(ApplicationConfigAction.class, name); + } + + @Override + public boolean isAlter() { + return ALTER_ACTIONS.contains(this); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/AuditAction.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/AuditAction.java new file mode 100644 index 00000000000..a277f82fe9c --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/AuditAction.java @@ -0,0 +1,24 @@ +package com.provectus.kafka.ui.model.rbac.permission; + +import java.util.Set; +import org.apache.commons.lang3.EnumUtils; +import org.jetbrains.annotations.Nullable; + +public enum AuditAction implements PermissibleAction { + + VIEW + + ; + + private static final Set ALTER_ACTIONS = Set.of(); + + @Nullable + public static AuditAction fromString(String name) { + return EnumUtils.getEnum(AuditAction.class, name); + } + + @Override + public boolean isAlter() { + return ALTER_ACTIONS.contains(this); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ClusterConfigAction.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ClusterConfigAction.java new file mode 100644 index 00000000000..91c9e49f5e3 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ClusterConfigAction.java @@ -0,0 +1,25 @@ +package com.provectus.kafka.ui.model.rbac.permission; + +import java.util.Set; +import org.apache.commons.lang3.EnumUtils; +import org.jetbrains.annotations.Nullable; + +public enum ClusterConfigAction implements PermissibleAction { + + VIEW, + EDIT + + ; + + public static final Set ALTER_ACTIONS = Set.of(EDIT); + + @Nullable + public static ClusterConfigAction fromString(String name) { + return EnumUtils.getEnum(ClusterConfigAction.class, name); + } + + @Override + public boolean isAlter() { + return ALTER_ACTIONS.contains(this); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ConnectAction.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ConnectAction.java new file mode 100644 index 00000000000..88404c25f64 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ConnectAction.java @@ -0,0 +1,27 @@ +package com.provectus.kafka.ui.model.rbac.permission; + +import java.util.Set; +import org.apache.commons.lang3.EnumUtils; +import org.jetbrains.annotations.Nullable; + +public enum ConnectAction implements PermissibleAction { + + VIEW, + EDIT, + CREATE, + RESTART + + ; + + public static final Set ALTER_ACTIONS = Set.of(CREATE, EDIT, RESTART); + + @Nullable + public static ConnectAction fromString(String name) { + return EnumUtils.getEnum(ConnectAction.class, name); + } + + @Override + public boolean isAlter() { + return ALTER_ACTIONS.contains(this); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ConsumerGroupAction.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ConsumerGroupAction.java new file mode 100644 index 00000000000..4a050aa0668 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ConsumerGroupAction.java @@ -0,0 +1,26 @@ +package com.provectus.kafka.ui.model.rbac.permission; + +import java.util.Set; +import org.apache.commons.lang3.EnumUtils; +import org.jetbrains.annotations.Nullable; + +public enum ConsumerGroupAction implements PermissibleAction { + + VIEW, + DELETE, + RESET_OFFSETS + + ; + + public static final Set ALTER_ACTIONS = Set.of(DELETE, RESET_OFFSETS); + + @Nullable + public static ConsumerGroupAction fromString(String name) { + return EnumUtils.getEnum(ConsumerGroupAction.class, name); + } + + @Override + public boolean isAlter() { + return ALTER_ACTIONS.contains(this); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/KsqlAction.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/KsqlAction.java new file mode 100644 index 00000000000..0ff2c1857cb --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/KsqlAction.java @@ -0,0 +1,24 @@ +package com.provectus.kafka.ui.model.rbac.permission; + +import java.util.Set; +import org.apache.commons.lang3.EnumUtils; +import org.jetbrains.annotations.Nullable; + +public enum KsqlAction implements PermissibleAction { + + EXECUTE + + ; + + public static final Set ALTER_ACTIONS = Set.of(EXECUTE); + + @Nullable + public static KsqlAction fromString(String name) { + return EnumUtils.getEnum(KsqlAction.class, name); + } + + @Override + public boolean isAlter() { + return ALTER_ACTIONS.contains(this); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/PermissibleAction.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/PermissibleAction.java new file mode 100644 index 00000000000..24c4adba9f0 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/PermissibleAction.java @@ -0,0 +1,13 @@ +package com.provectus.kafka.ui.model.rbac.permission; + +public sealed interface PermissibleAction permits + AclAction, ApplicationConfigAction, + ConsumerGroupAction, SchemaAction, + ConnectAction, ClusterConfigAction, + KsqlAction, TopicAction, AuditAction { + + String name(); + + boolean isAlter(); + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/SchemaAction.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/SchemaAction.java new file mode 100644 index 00000000000..94102297b44 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/SchemaAction.java @@ -0,0 +1,28 @@ +package com.provectus.kafka.ui.model.rbac.permission; + +import java.util.Set; +import org.apache.commons.lang3.EnumUtils; +import org.jetbrains.annotations.Nullable; + +public enum SchemaAction implements PermissibleAction { + + VIEW, + CREATE, + DELETE, + EDIT, + MODIFY_GLOBAL_COMPATIBILITY + + ; + + public static final Set ALTER_ACTIONS = Set.of(CREATE, DELETE, EDIT, MODIFY_GLOBAL_COMPATIBILITY); + + @Nullable + public static SchemaAction fromString(String name) { + return EnumUtils.getEnum(SchemaAction.class, name); + } + + @Override + public boolean isAlter() { + return ALTER_ACTIONS.contains(this); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/TopicAction.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/TopicAction.java new file mode 100644 index 00000000000..06eee56af5f --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/TopicAction.java @@ -0,0 +1,30 @@ +package com.provectus.kafka.ui.model.rbac.permission; + +import java.util.Set; +import org.apache.commons.lang3.EnumUtils; +import org.jetbrains.annotations.Nullable; + +public enum TopicAction implements PermissibleAction { + + VIEW, + CREATE, + EDIT, + DELETE, + MESSAGES_READ, + MESSAGES_PRODUCE, + MESSAGES_DELETE, + + ; + + public static final Set ALTER_ACTIONS = Set.of(CREATE, EDIT, DELETE, MESSAGES_PRODUCE, MESSAGES_DELETE); + + @Nullable + public static TopicAction fromString(String name) { + return EnumUtils.getEnum(TopicAction.class, name); + } + + @Override + public boolean isAlter() { + return ALTER_ACTIONS.contains(this); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/provider/Provider.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/provider/Provider.java new file mode 100644 index 00000000000..a2cde9158c1 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/provider/Provider.java @@ -0,0 +1,31 @@ +package com.provectus.kafka.ui.model.rbac.provider; + +import org.apache.commons.lang3.EnumUtils; +import org.jetbrains.annotations.Nullable; + +public enum Provider { + + OAUTH_GOOGLE, + OAUTH_GITHUB, + + OAUTH_COGNITO, + + OAUTH, + + LDAP, + LDAP_AD; + + @Nullable + public static Provider fromString(String name) { + return EnumUtils.getEnum(Provider.class, name); + } + + public static class Name { + public static String GOOGLE = "google"; + public static String GITHUB = "github"; + public static String COGNITO = "cognito"; + + public static String OAUTH = "oauth"; + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/DeserializationService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/DeserializationService.java deleted file mode 100644 index b7df25c8e90..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/DeserializationService.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.provectus.kafka.ui.serde; - -import com.provectus.kafka.ui.model.KafkaCluster; -import com.provectus.kafka.ui.serde.schemaregistry.SchemaRegistryAwareRecordSerDe; -import com.provectus.kafka.ui.service.ClustersStorage; -import java.util.Map; -import java.util.stream.Collectors; -import javax.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -@Slf4j -@Component -@RequiredArgsConstructor -public class DeserializationService { - - private final ClustersStorage clustersStorage; - private Map clusterDeserializers; - - @PostConstruct - public void init() { - this.clusterDeserializers = clustersStorage.getKafkaClusters().stream() - .collect(Collectors.toMap( - KafkaCluster::getName, - this::createRecordDeserializerForCluster - )); - } - - private RecordSerDe createRecordDeserializerForCluster(KafkaCluster cluster) { - try { - if (cluster.getProtobufFile() != null) { - log.info("Using ProtobufFileRecordSerDe for cluster '{}'", cluster.getName()); - return new ProtobufFileRecordSerDe(cluster.getProtobufFile(), - cluster.getProtobufMessageNameByTopic(), cluster.getProtobufMessageNameForKeyByTopic(), - cluster.getProtobufMessageName(), cluster.getProtobufMessageNameForKey()); - } else if (cluster.getSchemaRegistry() != null) { - log.info("Using SchemaRegistryAwareRecordSerDe for cluster '{}'", cluster.getName()); - return new SchemaRegistryAwareRecordSerDe(cluster); - } else { - log.info("Using SimpleRecordSerDe for cluster '{}'", cluster.getName()); - return new SimpleRecordSerDe(); - } - } catch (Throwable e) { - throw new RuntimeException("Can't init deserializer", e); - } - } - - public RecordSerDe getRecordDeserializerForCluster(KafkaCluster cluster) { - return clusterDeserializers.get(cluster.getName()); - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/ProtobufFileRecordSerDe.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/ProtobufFileRecordSerDe.java deleted file mode 100644 index 597461e51bc..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/ProtobufFileRecordSerDe.java +++ /dev/null @@ -1,210 +0,0 @@ -package com.provectus.kafka.ui.serde; - -import com.google.protobuf.Descriptors.Descriptor; -import com.google.protobuf.DynamicMessage; -import com.google.protobuf.util.JsonFormat; -import com.provectus.kafka.ui.model.MessageSchemaDTO; -import com.provectus.kafka.ui.model.TopicMessageSchemaDTO; -import com.provectus.kafka.ui.serde.schemaregistry.MessageFormat; -import com.provectus.kafka.ui.serde.schemaregistry.StringMessageFormatter; -import com.provectus.kafka.ui.util.jsonschema.JsonSchema; -import com.provectus.kafka.ui.util.jsonschema.ProtobufSchemaConverter; -import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema; -import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchemaUtils; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.annotation.Nullable; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.producer.ProducerRecord; -import org.apache.kafka.common.utils.Bytes; - -@Slf4j -public class ProtobufFileRecordSerDe implements RecordSerDe { - private static final StringMessageFormatter FALLBACK_FORMATTER = new StringMessageFormatter(); - - private final ProtobufSchema protobufSchema; - private final Path protobufSchemaPath; - private final ProtobufSchemaConverter schemaConverter = new ProtobufSchemaConverter(); - private final Map messageDescriptorMap; - private final Map keyMessageDescriptorMap; - private final Descriptor defaultMessageDescriptor; - private final Descriptor defaultKeyMessageDescriptor; - - public ProtobufFileRecordSerDe(Path protobufSchemaPath, Map messageNameMap, - Map keyMessageNameMap, String defaultMessageName, - @Nullable String defaultKeyMessageName) - throws IOException { - this.protobufSchemaPath = protobufSchemaPath; - try (final Stream lines = Files.lines(protobufSchemaPath)) { - var schema = new ProtobufSchema( - lines.collect(Collectors.joining("\n")) - ); - if (defaultMessageName != null) { - this.protobufSchema = schema.copy(defaultMessageName); - } else { - this.protobufSchema = schema; - } - this.messageDescriptorMap = new HashMap<>(); - if (messageNameMap != null) { - populateDescriptors(messageNameMap, messageDescriptorMap); - } - this.keyMessageDescriptorMap = new HashMap<>(); - if (keyMessageNameMap != null) { - populateDescriptors(keyMessageNameMap, keyMessageDescriptorMap); - } - defaultMessageDescriptor = Objects.requireNonNull(protobufSchema.toDescriptor(), - "The given message type is not found in protobuf definition: " - + defaultMessageName); - if (defaultKeyMessageName != null) { - defaultKeyMessageDescriptor = schema.copy(defaultKeyMessageName).toDescriptor(); - } else { - defaultKeyMessageDescriptor = null; - } - } - } - - private void populateDescriptors(Map messageNameMap, Map messageDescriptorMap) { - for (Map.Entry entry : messageNameMap.entrySet()) { - var descriptor = Objects.requireNonNull(protobufSchema.toDescriptor(entry.getValue()), - "The given message type is not found in protobuf definition: " - + entry.getValue()); - messageDescriptorMap.put(entry.getKey(), descriptor); - } - } - - @Override - public DeserializedKeyValue deserialize(ConsumerRecord msg) { - var builder = DeserializedKeyValue.builder(); - - if (msg.key() != null) { - Descriptor descriptor = getKeyDescriptor(msg.topic()); - if (descriptor == null) { - builder.key(FALLBACK_FORMATTER.format(msg.topic(), msg.key().get())); - builder.keyFormat(FALLBACK_FORMATTER.getFormat()); - } else { - try { - builder.key(parse(msg.key().get(), descriptor)); - builder.keyFormat(MessageFormat.PROTOBUF); - } catch (Throwable e) { - log.debug("Failed to deserialize key as protobuf, falling back to string formatter", e); - builder.key(FALLBACK_FORMATTER.format(msg.topic(), msg.key().get())); - builder.keyFormat(FALLBACK_FORMATTER.getFormat()); - } - } - } - - if (msg.value() != null) { - try { - builder.value(parse(msg.value().get(), getDescriptor(msg.topic()))); - builder.valueFormat(MessageFormat.PROTOBUF); - } catch (Throwable e) { - log.debug("Failed to deserialize value as protobuf, falling back to string formatter", e); - builder.key(FALLBACK_FORMATTER.format(msg.topic(), msg.value().get())); - builder.keyFormat(FALLBACK_FORMATTER.getFormat()); - } - } - - return builder.build(); - } - - @Nullable - private Descriptor getKeyDescriptor(String topic) { - return keyMessageDescriptorMap.getOrDefault(topic, defaultKeyMessageDescriptor); - } - - private Descriptor getDescriptor(String topic) { - return messageDescriptorMap.getOrDefault(topic, defaultMessageDescriptor); - } - - @SneakyThrows - private String parse(byte[] value, Descriptor descriptor) { - DynamicMessage protoMsg = DynamicMessage.parseFrom( - descriptor, - new ByteArrayInputStream(value) - ); - byte[] jsonFromProto = ProtobufSchemaUtils.toJson(protoMsg); - return new String(jsonFromProto); - } - - @Override - public ProducerRecord serialize(String topic, - @Nullable String key, - @Nullable String data, - @Nullable Integer partition) { - byte[] keyPayload = null; - byte[] valuePayload = null; - - if (key != null) { - Descriptor keyDescriptor = getKeyDescriptor(topic); - if (keyDescriptor == null) { - keyPayload = key.getBytes(); - } else { - DynamicMessage.Builder builder = DynamicMessage.newBuilder(keyDescriptor); - try { - JsonFormat.parser().merge(key, builder); - keyPayload = builder.build().toByteArray(); - } catch (Throwable e) { - throw new RuntimeException("Failed to merge record key for topic " + topic, e); - } - } - } - - if (data != null) { - DynamicMessage.Builder builder = DynamicMessage.newBuilder(getDescriptor(topic)); - try { - JsonFormat.parser().merge(data, builder); - valuePayload = builder.build().toByteArray(); - } catch (Throwable e) { - throw new RuntimeException("Failed to merge record value for topic " + topic, e); - } - } - - return new ProducerRecord<>( - topic, - partition, - keyPayload, - valuePayload); - } - - @Override - public TopicMessageSchemaDTO getTopicSchema(String topic) { - JsonSchema keyJsonSchema; - - Descriptor keyDescriptor = getKeyDescriptor(topic); - if (keyDescriptor == null) { - keyJsonSchema = JsonSchema.stringSchema(); - } else { - keyJsonSchema = schemaConverter.convert( - protobufSchemaPath.toUri(), - keyDescriptor); - } - - final MessageSchemaDTO keySchema = new MessageSchemaDTO() - .name(protobufSchema.fullName()) - .source(MessageSchemaDTO.SourceEnum.PROTO_FILE) - .schema(keyJsonSchema.toJson()); - - final JsonSchema valueJsonSchema = schemaConverter.convert( - protobufSchemaPath.toUri(), - getDescriptor(topic)); - - final MessageSchemaDTO valueSchema = new MessageSchemaDTO() - .name(protobufSchema.fullName()) - .source(MessageSchemaDTO.SourceEnum.PROTO_FILE) - .schema(valueJsonSchema.toJson()); - - return new TopicMessageSchemaDTO() - .key(keySchema) - .value(valueSchema); - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/RecordSerDe.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/RecordSerDe.java deleted file mode 100644 index af5a188e3d3..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/RecordSerDe.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.provectus.kafka.ui.serde; - -import com.provectus.kafka.ui.model.TopicMessageSchemaDTO; -import com.provectus.kafka.ui.serde.schemaregistry.MessageFormat; -import javax.annotation.Nullable; -import lombok.Builder; -import lombok.Value; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.producer.ProducerRecord; -import org.apache.kafka.common.utils.Bytes; - -public interface RecordSerDe { - - @Value - @Builder - class DeserializedKeyValue { - @Nullable - String key; - @Nullable - String value; - @Nullable - MessageFormat keyFormat; - @Nullable - MessageFormat valueFormat; - @Nullable - String keySchemaId; - @Nullable - String valueSchemaId; - } - - DeserializedKeyValue deserialize(ConsumerRecord msg); - - ProducerRecord serialize(String topic, - @Nullable String key, - @Nullable String data, - @Nullable Integer partition); - - TopicMessageSchemaDTO getTopicSchema(String topic); -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/SimpleRecordSerDe.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/SimpleRecordSerDe.java deleted file mode 100644 index 0ee06e85688..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/SimpleRecordSerDe.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.provectus.kafka.ui.serde; - -import com.provectus.kafka.ui.model.MessageSchemaDTO; -import com.provectus.kafka.ui.model.TopicMessageSchemaDTO; -import com.provectus.kafka.ui.serde.schemaregistry.StringMessageFormatter; -import com.provectus.kafka.ui.util.jsonschema.JsonSchema; -import javax.annotation.Nullable; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.producer.ProducerRecord; -import org.apache.kafka.common.utils.Bytes; - -public class SimpleRecordSerDe implements RecordSerDe { - - private static final StringMessageFormatter FORMATTER = new StringMessageFormatter(); - - @Override - public DeserializedKeyValue deserialize(ConsumerRecord msg) { - var builder = DeserializedKeyValue.builder(); - if (msg.key() != null) { - builder.key(FORMATTER.format(msg.topic(), msg.key().get())); - builder.keyFormat(FORMATTER.getFormat()); - } - if (msg.value() != null) { - builder.value(FORMATTER.format(msg.topic(), msg.value().get())); - builder.valueFormat(FORMATTER.getFormat()); - } - return builder.build(); - } - - @Override - public ProducerRecord serialize(String topic, - @Nullable String key, - @Nullable String data, - @Nullable Integer partition) { - return new ProducerRecord<>( - topic, - partition, - key != null ? key.getBytes() : null, - data != null ? data.getBytes() : null - ); - } - - @Override - public TopicMessageSchemaDTO getTopicSchema(String topic) { - final MessageSchemaDTO schema = new MessageSchemaDTO() - .name("unknown") - .source(MessageSchemaDTO.SourceEnum.UNKNOWN) - .schema(JsonSchema.stringSchema().toJson()); - return new TopicMessageSchemaDTO() - .key(schema) - .value(schema); - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/AvroMessageFormatter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/AvroMessageFormatter.java deleted file mode 100644 index e69d522ea03..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/AvroMessageFormatter.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.provectus.kafka.ui.serde.schemaregistry; - -import io.confluent.kafka.schemaregistry.avro.AvroSchemaUtils; -import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient; -import io.confluent.kafka.serializers.KafkaAvroDeserializer; -import lombok.SneakyThrows; - -public class AvroMessageFormatter implements MessageFormatter { - private final KafkaAvroDeserializer avroDeserializer; - - public AvroMessageFormatter(SchemaRegistryClient client) { - this.avroDeserializer = new KafkaAvroDeserializer(client); - } - - @Override - @SneakyThrows - public String format(String topic, byte[] value) { - // deserialized will have type, that depends on schema type (record or primitive), - // AvroSchemaUtils.toJson(...) method will take it into account - Object deserialized = avroDeserializer.deserialize(topic, value); - byte[] jsonBytes = AvroSchemaUtils.toJson(deserialized); - return new String(jsonBytes); - } - - @Override - public MessageFormat getFormat() { - return MessageFormat.AVRO; - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/AvroMessageReader.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/AvroMessageReader.java deleted file mode 100644 index 7f2efbbd644..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/AvroMessageReader.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.provectus.kafka.ui.serde.schemaregistry; - -import io.confluent.kafka.schemaregistry.ParsedSchema; -import io.confluent.kafka.schemaregistry.avro.AvroSchema; -import io.confluent.kafka.schemaregistry.avro.AvroSchemaUtils; -import io.confluent.kafka.schemaregistry.client.SchemaMetadata; -import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient; -import io.confluent.kafka.schemaregistry.client.rest.exceptions.RestClientException; -import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig; -import io.confluent.kafka.serializers.KafkaAvroSerializer; -import java.io.IOException; -import java.util.Map; -import org.apache.kafka.common.serialization.Serializer; - -public class AvroMessageReader extends MessageReader { - - public AvroMessageReader(String topic, boolean isKey, - SchemaRegistryClient client, - SchemaMetadata schema) - throws IOException, RestClientException { - super(topic, isKey, client, schema); - } - - @Override - protected Serializer createSerializer(SchemaRegistryClient client) { - var serializer = new KafkaAvroSerializer(client); - serializer.configure( - Map.of( - "schema.registry.url", "wontbeused", - AbstractKafkaSchemaSerDeConfig.AUTO_REGISTER_SCHEMAS, false, - AbstractKafkaSchemaSerDeConfig.USE_LATEST_VERSION, true - ), - isKey - ); - return serializer; - } - - @Override - protected Object read(String value, ParsedSchema schema) { - try { - return AvroSchemaUtils.toObject(value, (AvroSchema) schema); - } catch (Throwable e) { - throw new RuntimeException("Failed to serialize record for topic " + topic, e); - } - - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/JsonSchemaMessageFormatter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/JsonSchemaMessageFormatter.java deleted file mode 100644 index 5127285e509..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/JsonSchemaMessageFormatter.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.provectus.kafka.ui.serde.schemaregistry; - -import com.fasterxml.jackson.databind.JsonNode; -import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient; -import io.confluent.kafka.serializers.json.KafkaJsonSchemaDeserializer; - -public class JsonSchemaMessageFormatter implements MessageFormatter { - - private final KafkaJsonSchemaDeserializer jsonSchemaDeserializer; - - public JsonSchemaMessageFormatter(SchemaRegistryClient client) { - this.jsonSchemaDeserializer = new KafkaJsonSchemaDeserializer<>(client); - } - - @Override - public String format(String topic, byte[] value) { - JsonNode json = jsonSchemaDeserializer.deserialize(topic, value); - return json.toString(); - } - - @Override - public MessageFormat getFormat() { - return MessageFormat.JSON; - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/JsonSchemaMessageReader.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/JsonSchemaMessageReader.java deleted file mode 100644 index de56ed462e6..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/JsonSchemaMessageReader.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.provectus.kafka.ui.serde.schemaregistry; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.provectus.kafka.ui.exception.ValidationException; -import com.provectus.kafka.ui.util.annotations.KafkaClientInternalsDependant; -import io.confluent.kafka.schemaregistry.ParsedSchema; -import io.confluent.kafka.schemaregistry.client.SchemaMetadata; -import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient; -import io.confluent.kafka.schemaregistry.client.rest.exceptions.RestClientException; -import io.confluent.kafka.schemaregistry.json.JsonSchema; -import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig; -import io.confluent.kafka.serializers.json.KafkaJsonSchemaSerializer; -import java.io.IOException; -import java.util.Map; -import org.apache.kafka.common.serialization.Serializer; - -public class JsonSchemaMessageReader extends MessageReader { - - private static final ObjectMapper MAPPER = new ObjectMapper(); - - public JsonSchemaMessageReader(String topic, - boolean isKey, - SchemaRegistryClient client, - SchemaMetadata schema) throws IOException, RestClientException { - super(topic, isKey, client, schema); - } - - @Override - protected Serializer createSerializer(SchemaRegistryClient client) { - var serializer = new KafkaJsonSchemaSerializerWithoutSchemaInfer(client); - serializer.configure( - Map.of( - "schema.registry.url", "wontbeused", - AbstractKafkaSchemaSerDeConfig.AUTO_REGISTER_SCHEMAS, false, - AbstractKafkaSchemaSerDeConfig.USE_LATEST_VERSION, true - ), - isKey - ); - return serializer; - } - - @Override - protected JsonNode read(String value, ParsedSchema schema) { - try { - JsonNode json = MAPPER.readTree(value); - ((JsonSchema) schema).validate(json); - return json; - } catch (JsonProcessingException e) { - throw new ValidationException(String.format("'%s' is not valid json", value)); - } catch (org.everit.json.schema.ValidationException e) { - throw new ValidationException( - String.format("'%s' does not fit schema: %s", value, e.getAllMessages())); - } - } - - @KafkaClientInternalsDependant - private class KafkaJsonSchemaSerializerWithoutSchemaInfer - extends KafkaJsonSchemaSerializer { - - KafkaJsonSchemaSerializerWithoutSchemaInfer(SchemaRegistryClient client) { - super(client); - } - - /** - * Need to override original method because it tries to infer schema from input - * by checking 'schema' json field or @Schema annotation on input class, which is not - * possible in our case. So, we just skip all infer logic and pass schema directly. - */ - @Override - public byte[] serialize(String topic, JsonNode rec) { - return super.serializeImpl( - super.getSubjectName(topic, isKey, rec, schema), - rec, - (JsonSchema) schema - ); - } - } - -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/MessageFormat.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/MessageFormat.java deleted file mode 100644 index 9f47350e073..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/MessageFormat.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.provectus.kafka.ui.serde.schemaregistry; - -import java.util.Optional; -import org.apache.commons.lang3.EnumUtils; - -public enum MessageFormat { - AVRO, - JSON, - PROTOBUF, - UNKNOWN; - - public static Optional fromString(String typeString) { - return Optional.ofNullable(EnumUtils.getEnum(MessageFormat.class, typeString)); - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/MessageFormatter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/MessageFormatter.java deleted file mode 100644 index e7fec895e73..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/MessageFormatter.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.provectus.kafka.ui.serde.schemaregistry; - -public interface MessageFormatter { - String format(String topic, byte[] value); - - default MessageFormat getFormat() { - return MessageFormat.UNKNOWN; - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/MessageReader.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/MessageReader.java deleted file mode 100644 index c6cb1e4606d..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/MessageReader.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.provectus.kafka.ui.serde.schemaregistry; - -import io.confluent.kafka.schemaregistry.ParsedSchema; -import io.confluent.kafka.schemaregistry.client.SchemaMetadata; -import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient; -import io.confluent.kafka.schemaregistry.client.rest.exceptions.RestClientException; -import java.io.IOException; -import org.apache.kafka.common.serialization.Serializer; - -public abstract class MessageReader { - protected final Serializer serializer; - protected final String topic; - protected final boolean isKey; - protected final ParsedSchema schema; - - protected MessageReader(String topic, boolean isKey, SchemaRegistryClient client, - SchemaMetadata schema) throws IOException, RestClientException { - this.topic = topic; - this.isKey = isKey; - this.serializer = createSerializer(client); - this.schema = client.getSchemaById(schema.getId()); - } - - protected abstract Serializer createSerializer(SchemaRegistryClient client); - - public byte[] read(String value) { - final T read = this.read(value, schema); - return this.serializer.serialize(topic, read); - } - - protected abstract T read(String value, ParsedSchema schema); -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/ProtobufMessageFormatter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/ProtobufMessageFormatter.java deleted file mode 100644 index adfdaf03113..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/ProtobufMessageFormatter.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.provectus.kafka.ui.serde.schemaregistry; - -import com.google.protobuf.Message; -import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient; -import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchemaUtils; -import io.confluent.kafka.serializers.protobuf.KafkaProtobufDeserializer; -import lombok.SneakyThrows; - -public class ProtobufMessageFormatter implements MessageFormatter { - private final KafkaProtobufDeserializer protobufDeserializer; - - public ProtobufMessageFormatter(SchemaRegistryClient client) { - this.protobufDeserializer = new KafkaProtobufDeserializer<>(client); - } - - @Override - @SneakyThrows - public String format(String topic, byte[] value) { - final Message message = protobufDeserializer.deserialize(topic, value); - byte[] jsonBytes = ProtobufSchemaUtils.toJson(message); - return new String(jsonBytes); - } - - @Override - public MessageFormat getFormat() { - return MessageFormat.PROTOBUF; - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/ProtobufMessageReader.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/ProtobufMessageReader.java deleted file mode 100644 index faa9cde0494..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/ProtobufMessageReader.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.provectus.kafka.ui.serde.schemaregistry; - -import com.google.protobuf.DynamicMessage; -import com.google.protobuf.Message; -import com.google.protobuf.util.JsonFormat; -import io.confluent.kafka.schemaregistry.ParsedSchema; -import io.confluent.kafka.schemaregistry.client.SchemaMetadata; -import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient; -import io.confluent.kafka.schemaregistry.client.rest.exceptions.RestClientException; -import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema; -import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig; -import io.confluent.kafka.serializers.protobuf.KafkaProtobufSerializer; -import java.io.IOException; -import java.util.Map; -import org.apache.kafka.common.serialization.Serializer; - -public class ProtobufMessageReader extends MessageReader { - - public ProtobufMessageReader(String topic, boolean isKey, - SchemaRegistryClient client, SchemaMetadata schema) - throws IOException, RestClientException { - super(topic, isKey, client, schema); - } - - @Override - protected Serializer createSerializer(SchemaRegistryClient client) { - var serializer = new KafkaProtobufSerializer<>(client); - serializer.configure( - Map.of( - "schema.registry.url", "wontbeused", - AbstractKafkaSchemaSerDeConfig.AUTO_REGISTER_SCHEMAS, false, - AbstractKafkaSchemaSerDeConfig.USE_LATEST_VERSION, true - ), - isKey - ); - return serializer; - } - - @Override - protected Message read(String value, ParsedSchema schema) { - ProtobufSchema protobufSchema = (ProtobufSchema) schema; - DynamicMessage.Builder builder = protobufSchema.newMessageBuilder(); - try { - JsonFormat.parser().merge(value, builder); - return builder.build(); - } catch (Throwable e) { - throw new RuntimeException("Failed to serialize record for topic " + topic, e); - } - } - -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/SchemaRegistryAwareRecordSerDe.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/SchemaRegistryAwareRecordSerDe.java deleted file mode 100644 index ad15821607f..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/SchemaRegistryAwareRecordSerDe.java +++ /dev/null @@ -1,298 +0,0 @@ -package com.provectus.kafka.ui.serde.schemaregistry; - - -import static io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig.BASIC_AUTH_CREDENTIALS_SOURCE; -import static io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig.USER_INFO_CONFIG; - -import com.google.common.annotations.VisibleForTesting; -import com.provectus.kafka.ui.exception.ValidationException; -import com.provectus.kafka.ui.model.KafkaCluster; -import com.provectus.kafka.ui.model.MessageSchemaDTO; -import com.provectus.kafka.ui.model.TopicMessageSchemaDTO; -import com.provectus.kafka.ui.serde.RecordSerDe; -import com.provectus.kafka.ui.serde.RecordSerDe.DeserializedKeyValue.DeserializedKeyValueBuilder; -import com.provectus.kafka.ui.util.jsonschema.AvroJsonSchemaConverter; -import com.provectus.kafka.ui.util.jsonschema.JsonSchema; -import com.provectus.kafka.ui.util.jsonschema.ProtobufSchemaConverter; -import io.confluent.kafka.schemaregistry.ParsedSchema; -import io.confluent.kafka.schemaregistry.SchemaProvider; -import io.confluent.kafka.schemaregistry.avro.AvroSchema; -import io.confluent.kafka.schemaregistry.avro.AvroSchemaProvider; -import io.confluent.kafka.schemaregistry.client.CachedSchemaRegistryClient; -import io.confluent.kafka.schemaregistry.client.SchemaMetadata; -import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient; -import io.confluent.kafka.schemaregistry.client.rest.exceptions.RestClientException; -import io.confluent.kafka.schemaregistry.json.JsonSchemaProvider; -import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema; -import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchemaProvider; -import java.net.URI; -import java.nio.ByteBuffer; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.Callable; -import java.util.stream.Collectors; -import javax.annotation.Nullable; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.producer.ProducerRecord; -import org.apache.kafka.common.utils.Bytes; - -@Slf4j -public class SchemaRegistryAwareRecordSerDe implements RecordSerDe { - - private static final byte SR_RECORD_MAGIC_BYTE = (byte) 0; - private static final int SR_RECORD_PREFIX_LENGTH = 5; - - private static final StringMessageFormatter FALLBACK_FORMATTER = new StringMessageFormatter(); - - private static final ProtobufSchemaConverter protoSchemaConverter = new ProtobufSchemaConverter(); - private static final AvroJsonSchemaConverter avroSchemaConverter = new AvroJsonSchemaConverter(); - - private final KafkaCluster cluster; - private final SchemaRegistryClient schemaRegistryClient; - - private final Map schemaRegistryFormatters; - - private static SchemaRegistryClient createSchemaRegistryClient(KafkaCluster cluster) { - List schemaProviders = - List.of(new AvroSchemaProvider(), new ProtobufSchemaProvider(), new JsonSchemaProvider()); - - Map configs = new HashMap<>(); - String username = cluster.getSchemaRegistry().getUsername(); - String password = cluster.getSchemaRegistry().getPassword(); - - if (username != null && password != null) { - configs.put(BASIC_AUTH_CREDENTIALS_SOURCE, "USER_INFO"); - configs.put(USER_INFO_CONFIG, username + ":" + password); - } else if (username != null) { - throw new ValidationException( - "You specified username but do not specified password"); - } else if (password != null) { - throw new ValidationException( - "You specified password but do not specified username"); - } - return new CachedSchemaRegistryClient( - cluster.getSchemaRegistry() - .getUrl() - .stream() - .collect(Collectors.toUnmodifiableList()), - 1_000, - schemaProviders, - configs - ); - } - - public SchemaRegistryAwareRecordSerDe(KafkaCluster cluster) { - this(cluster, createSchemaRegistryClient(cluster)); - } - - @VisibleForTesting - SchemaRegistryAwareRecordSerDe(KafkaCluster cluster, SchemaRegistryClient schemaRegistryClient) { - this.cluster = cluster; - this.schemaRegistryClient = schemaRegistryClient; - this.schemaRegistryFormatters = Map.of( - MessageFormat.AVRO, new AvroMessageFormatter(schemaRegistryClient), - MessageFormat.JSON, new JsonSchemaMessageFormatter(schemaRegistryClient), - MessageFormat.PROTOBUF, new ProtobufMessageFormatter(schemaRegistryClient) - ); - } - - public DeserializedKeyValue deserialize(ConsumerRecord msg) { - try { - DeserializedKeyValueBuilder builder = DeserializedKeyValue.builder(); - if (msg.key() != null) { - fillDeserializedKvBuilder(msg, true, builder); - } - if (msg.value() != null) { - fillDeserializedKvBuilder(msg, false, builder); - } - return builder.build(); - } catch (Throwable e) { - throw new RuntimeException("Failed to parse record from topic " + msg.topic(), e); - } - } - - private void fillDeserializedKvBuilder(ConsumerRecord rec, - boolean isKey, - DeserializedKeyValueBuilder builder) { - Optional schemaId = extractSchemaIdFromMsg(rec, isKey); - Optional format = schemaId.flatMap(this::getMessageFormatBySchemaId); - if (schemaId.isPresent() && format.isPresent() && schemaRegistryFormatters.containsKey(format.get())) { - var formatter = schemaRegistryFormatters.get(format.get()); - try { - var deserialized = formatter.format(rec.topic(), isKey ? rec.key().get() : rec.value().get()); - if (isKey) { - builder.key(deserialized); - builder.keyFormat(formatter.getFormat()); - builder.keySchemaId(String.valueOf(schemaId.get())); - } else { - builder.value(deserialized); - builder.valueFormat(formatter.getFormat()); - builder.valueSchemaId(String.valueOf(schemaId.get())); - } - return; - } catch (Exception e) { - log.trace("Can't deserialize record {} with formatter {}", - rec, formatter.getClass().getSimpleName(), e); - } - } - - // fallback - if (isKey) { - builder.key(FALLBACK_FORMATTER.format(rec.topic(), rec.key().get())); - builder.keyFormat(FALLBACK_FORMATTER.getFormat()); - } else { - builder.value(FALLBACK_FORMATTER.format(rec.topic(), rec.value().get())); - builder.valueFormat(FALLBACK_FORMATTER.getFormat()); - } - - } - - @Override - public ProducerRecord serialize(String topic, - @Nullable String key, - @Nullable String data, - @Nullable Integer partition) { - final Optional maybeKeySchema = getSchemaBySubject(topic, true); - final Optional maybeValueSchema = getSchemaBySubject(topic, false); - - final byte[] serializedKey = maybeKeySchema.isPresent() - ? serialize(maybeKeySchema.get(), topic, key, true) - : serialize(key); - - final byte[] serializedValue = maybeValueSchema.isPresent() - ? serialize(maybeValueSchema.get(), topic, data, false) - : serialize(data); - - return new ProducerRecord<>(topic, partition, serializedKey, serializedValue); - } - - @SneakyThrows - private byte[] serialize(SchemaMetadata schema, String topic, String value, boolean isKey) { - if (value == null) { - return null; - } - MessageReader reader; - if (schema.getSchemaType().equals(MessageFormat.PROTOBUF.name())) { - reader = new ProtobufMessageReader(topic, isKey, schemaRegistryClient, schema); - } else if (schema.getSchemaType().equals(MessageFormat.AVRO.name())) { - reader = new AvroMessageReader(topic, isKey, schemaRegistryClient, schema); - } else if (schema.getSchemaType().equals(MessageFormat.JSON.name())) { - reader = new JsonSchemaMessageReader(topic, isKey, schemaRegistryClient, schema); - } else { - throw new IllegalStateException("Unsupported schema type: " + schema.getSchemaType()); - } - - return reader.read(value); - } - - private byte[] serialize(String value) { - if (value == null) { - return null; - } - // if no schema provided serialize input as raw string - return value.getBytes(); - } - - @Override - public TopicMessageSchemaDTO getTopicSchema(String topic) { - final Optional maybeValueSchema = getSchemaBySubject(topic, false); - final Optional maybeKeySchema = getSchemaBySubject(topic, true); - - String sourceValueSchema = maybeValueSchema.map(this::convertSchema) - .orElseGet(() -> JsonSchema.stringSchema().toJson()); - - String sourceKeySchema = maybeKeySchema.map(this::convertSchema) - .orElseGet(() -> JsonSchema.stringSchema().toJson()); - - final MessageSchemaDTO keySchema = new MessageSchemaDTO() - .name(maybeKeySchema.map( - s -> schemaSubject(topic, true) - ).orElse("unknown")) - .source(MessageSchemaDTO.SourceEnum.SCHEMA_REGISTRY) - .schema(sourceKeySchema); - - final MessageSchemaDTO valueSchema = new MessageSchemaDTO() - .name(maybeValueSchema.map( - s -> schemaSubject(topic, false) - ).orElse("unknown")) - .source(MessageSchemaDTO.SourceEnum.SCHEMA_REGISTRY) - .schema(sourceValueSchema); - - return new TopicMessageSchemaDTO() - .key(keySchema) - .value(valueSchema); - } - - @SneakyThrows - private String convertSchema(SchemaMetadata schema) { - - String jsonSchema; - URI basePath = new URI(cluster.getSchemaRegistry().getPrimaryNodeUri()) - .resolve(Integer.toString(schema.getId())); - final ParsedSchema schemaById = schemaRegistryClient.getSchemaById(schema.getId()); - - if (schema.getSchemaType().equals(MessageFormat.PROTOBUF.name())) { - final ProtobufSchema protobufSchema = (ProtobufSchema) schemaById; - jsonSchema = protoSchemaConverter - .convert(basePath, protobufSchema.toDescriptor()) - .toJson(); - } else if (schema.getSchemaType().equals(MessageFormat.AVRO.name())) { - final AvroSchema avroSchema = (AvroSchema) schemaById; - jsonSchema = avroSchemaConverter - .convert(basePath, avroSchema.rawSchema()) - .toJson(); - } else if (schema.getSchemaType().equals(MessageFormat.JSON.name())) { - jsonSchema = schema.getSchema(); - } else { - jsonSchema = JsonSchema.stringSchema().toJson(); - } - - return jsonSchema; - } - - private Optional getMessageFormatBySchemaId(int schemaId) { - return wrapClientCall(() -> schemaRegistryClient.getSchemaById(schemaId)) - .map(ParsedSchema::schemaType) - .flatMap(MessageFormat::fromString); - } - - private Optional extractSchemaIdFromMsg(ConsumerRecord msg, boolean isKey) { - Bytes bytes = isKey ? msg.key() : msg.value(); - ByteBuffer buffer = ByteBuffer.wrap(bytes.get()); - if (buffer.remaining() > SR_RECORD_PREFIX_LENGTH && buffer.get() == SR_RECORD_MAGIC_BYTE) { - int id = buffer.getInt(); - return Optional.of(id); - } - return Optional.empty(); - } - - @SneakyThrows - private Optional getSchemaBySubject(String topic, boolean isKey) { - return wrapClientCall(() -> - schemaRegistryClient.getLatestSchemaMetadata(schemaSubject(topic, isKey))); - } - - @SneakyThrows - private Optional wrapClientCall(Callable call) { - try { - return Optional.ofNullable(call.call()); - } catch (RestClientException restClientException) { - if (restClientException.getStatus() == 404) { - return Optional.empty(); - } else { - throw new RuntimeException("Error calling SchemaRegistryClient", restClientException); - } - } - } - - private String schemaSubject(String topic, boolean isKey) { - return String.format( - isKey ? cluster.getKeySchemaNameTemplate() - : cluster.getSchemaNameTemplate(), topic - ); - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/StringMessageFormatter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/StringMessageFormatter.java deleted file mode 100644 index 00770586904..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serde/schemaregistry/StringMessageFormatter.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.provectus.kafka.ui.serde.schemaregistry; - -import java.nio.charset.StandardCharsets; - -public class StringMessageFormatter implements MessageFormatter { - - @Override - public String format(String topic, byte[] value) { - return new String(value, StandardCharsets.UTF_8); - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/BuiltInSerde.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/BuiltInSerde.java new file mode 100644 index 00000000000..c72a83ee29b --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/BuiltInSerde.java @@ -0,0 +1,27 @@ +package com.provectus.kafka.ui.serdes; + +import com.provectus.kafka.ui.serde.api.PropertyResolver; +import com.provectus.kafka.ui.serde.api.Serde; + +public interface BuiltInSerde extends Serde { + + // returns true is serde has enough properties set on cluster&global levels to + // be configured without explicit config provide + default boolean canBeAutoConfigured(PropertyResolver kafkaClusterProperties, + PropertyResolver globalProperties) { + return true; + } + + // will be called for build-in serdes that were not explicitly registered + // and that returned true on canBeAutoConfigured(..) call. + // NOTE: Serde.configure() method won't be called if serde is auto-configured! + default void autoConfigure(PropertyResolver kafkaClusterProperties, + PropertyResolver globalProperties) { + } + + @Override + default void configure(PropertyResolver serdeProperties, + PropertyResolver kafkaClusterProperties, + PropertyResolver globalProperties) { + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ClassloaderUtil.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ClassloaderUtil.java new file mode 100644 index 00000000000..f7c423189e7 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ClassloaderUtil.java @@ -0,0 +1,12 @@ +package com.provectus.kafka.ui.serdes; + +class ClassloaderUtil { + + static ClassLoader compareAndSwapLoaders(ClassLoader loader) { + ClassLoader current = Thread.currentThread().getContextClassLoader(); + if (!current.equals(loader)) { + Thread.currentThread().setContextClassLoader(loader); + } + return current; + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ClusterSerdes.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ClusterSerdes.java new file mode 100644 index 00000000000..1c64cf02f8c --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ClusterSerdes.java @@ -0,0 +1,79 @@ +package com.provectus.kafka.ui.serdes; + +import com.provectus.kafka.ui.serde.api.Serde; +import com.provectus.kafka.ui.serdes.builtin.StringSerde; +import java.io.Closeable; +import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +public class ClusterSerdes implements Closeable { + + final Map serdes; + + @Nullable + final SerdeInstance defaultKeySerde; + + @Nullable + final SerdeInstance defaultValueSerde; + + @Getter + final SerdeInstance fallbackSerde; + + private Optional findSerdeByPatternsOrDefault(String topic, + Serde.Target type, + Predicate additionalCheck) { + // iterating over serdes in the same order they were added in config + for (SerdeInstance serdeInstance : serdes.values()) { + var pattern = type == Serde.Target.KEY + ? serdeInstance.topicKeyPattern + : serdeInstance.topicValuePattern; + if (pattern != null + && pattern.matcher(topic).matches() + && additionalCheck.test(serdeInstance)) { + return Optional.of(serdeInstance); + } + } + if (type == Serde.Target.KEY + && defaultKeySerde != null + && additionalCheck.test(defaultKeySerde)) { + return Optional.of(defaultKeySerde); + } + if (type == Serde.Target.VALUE + && defaultValueSerde != null + && additionalCheck.test(defaultValueSerde)) { + return Optional.of(defaultValueSerde); + } + return Optional.empty(); + } + + public Optional serdeForName(String name) { + return Optional.ofNullable(serdes.get(name)); + } + + public Stream all() { + return serdes.values().stream(); + } + + public SerdeInstance suggestSerdeForSerialize(String topic, Serde.Target type) { + return findSerdeByPatternsOrDefault(topic, type, s -> s.canSerialize(topic, type)) + .orElse(serdes.get(StringSerde.name())); + } + + public SerdeInstance suggestSerdeForDeserialize(String topic, Serde.Target type) { + return findSerdeByPatternsOrDefault(topic, type, s -> s.canDeserialize(topic, type)) + .orElse(serdes.get(StringSerde.name())); + } + + @Override + public void close() { + serdes.values().forEach(SerdeInstance::close); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ConsumerRecordDeserializer.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ConsumerRecordDeserializer.java new file mode 100644 index 00000000000..4b507b86fcd --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ConsumerRecordDeserializer.java @@ -0,0 +1,138 @@ +package com.provectus.kafka.ui.serdes; + +import com.provectus.kafka.ui.model.TopicMessageDTO; +import com.provectus.kafka.ui.model.TopicMessageDTO.TimestampTypeEnum; +import com.provectus.kafka.ui.serde.api.Serde; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.function.UnaryOperator; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.common.header.Header; +import org.apache.kafka.common.header.Headers; +import org.apache.kafka.common.record.TimestampType; +import org.apache.kafka.common.utils.Bytes; + +@Slf4j +@RequiredArgsConstructor +public class ConsumerRecordDeserializer { + + private static final ZoneId UTC_ZONE_ID = ZoneId.of("UTC"); + + private final String keySerdeName; + private final Serde.Deserializer keyDeserializer; + + private final String valueSerdeName; + private final Serde.Deserializer valueDeserializer; + + private final String fallbackSerdeName; + private final Serde.Deserializer fallbackKeyDeserializer; + private final Serde.Deserializer fallbackValueDeserializer; + + private final UnaryOperator masker; + + public TopicMessageDTO deserialize(ConsumerRecord rec) { + var message = new TopicMessageDTO(); + fillKey(message, rec); + fillValue(message, rec); + fillHeaders(message, rec); + + message.setPartition(rec.partition()); + message.setOffset(rec.offset()); + message.setTimestampType(mapToTimestampType(rec.timestampType())); + message.setTimestamp(OffsetDateTime.ofInstant(Instant.ofEpochMilli(rec.timestamp()), UTC_ZONE_ID)); + + message.setKeySize(getKeySize(rec)); + message.setValueSize(getValueSize(rec)); + message.setHeadersSize(getHeadersSize(rec)); + + return masker.apply(message); + } + + private static TimestampTypeEnum mapToTimestampType(TimestampType timestampType) { + return switch (timestampType) { + case CREATE_TIME -> TimestampTypeEnum.CREATE_TIME; + case LOG_APPEND_TIME -> TimestampTypeEnum.LOG_APPEND_TIME; + case NO_TIMESTAMP_TYPE -> TimestampTypeEnum.NO_TIMESTAMP_TYPE; + }; + } + + private void fillHeaders(TopicMessageDTO message, ConsumerRecord rec) { + Map headers = new HashMap<>(); + rec.headers().iterator() + .forEachRemaining(header -> + headers.put( + header.key(), + header.value() != null ? new String(header.value()) : null + )); + message.setHeaders(headers); + } + + private void fillKey(TopicMessageDTO message, ConsumerRecord rec) { + if (rec.key() == null) { + return; + } + try { + var deserResult = keyDeserializer.deserialize(new RecordHeadersImpl(), rec.key().get()); + message.setKey(deserResult.getResult()); + message.setKeySerde(keySerdeName); + message.setKeyDeserializeProperties(deserResult.getAdditionalProperties()); + } catch (Exception e) { + log.trace("Error deserializing key for key topic: {}, partition {}, offset {}, with serde {}", + rec.topic(), rec.partition(), rec.offset(), keySerdeName, e); + var deserResult = fallbackKeyDeserializer.deserialize(new RecordHeadersImpl(), rec.key().get()); + message.setKey(deserResult.getResult()); + message.setKeySerde(fallbackSerdeName); + } + } + + private void fillValue(TopicMessageDTO message, ConsumerRecord rec) { + if (rec.value() == null) { + return; + } + try { + var deserResult = valueDeserializer.deserialize( + new RecordHeadersImpl(rec.headers()), rec.value().get()); + message.setContent(deserResult.getResult()); + message.setValueSerde(valueSerdeName); + message.setValueDeserializeProperties(deserResult.getAdditionalProperties()); + } catch (Exception e) { + log.trace("Error deserializing key for value topic: {}, partition {}, offset {}, with serde {}", + rec.topic(), rec.partition(), rec.offset(), valueSerdeName, e); + var deserResult = fallbackValueDeserializer.deserialize( + new RecordHeadersImpl(rec.headers()), rec.value().get()); + message.setContent(deserResult.getResult()); + message.setValueSerde(fallbackSerdeName); + } + } + + private static Long getHeadersSize(ConsumerRecord consumerRecord) { + Headers headers = consumerRecord.headers(); + if (headers != null) { + return Arrays.stream(headers.toArray()) + .mapToLong(ConsumerRecordDeserializer::headerSize) + .sum(); + } + return 0L; + } + + private static Long getKeySize(ConsumerRecord consumerRecord) { + return consumerRecord.key() != null ? (long) consumerRecord.serializedKeySize() : null; + } + + private static Long getValueSize(ConsumerRecord consumerRecord) { + return consumerRecord.value() != null ? (long) consumerRecord.serializedValueSize() : null; + } + + private static int headerSize(Header header) { + int key = header.key() != null ? header.key().getBytes().length : 0; + int val = header.value() != null ? header.value().length : 0; + return key + val; + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/CustomSerdeLoader.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/CustomSerdeLoader.java new file mode 100644 index 00000000000..e14e7b9756d --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/CustomSerdeLoader.java @@ -0,0 +1,173 @@ +package com.provectus.kafka.ui.serdes; + +import com.provectus.kafka.ui.serde.api.PropertyResolver; +import com.provectus.kafka.ui.serde.api.Serde; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import lombok.SneakyThrows; +import lombok.Value; + + +class CustomSerdeLoader { + + @Value + static class CustomSerde { + Serde serde; + ClassLoader classLoader; + } + + // serde location -> classloader + private final Map classloaders = new ConcurrentHashMap<>(); + + @SneakyThrows + CustomSerde loadAndConfigure(String className, + String filePath, + PropertyResolver serdeProps, + PropertyResolver clusterProps, + PropertyResolver globalProps) { + Path locationPath = Path.of(filePath); + var serdeClassloader = createClassloader(locationPath); + var origCL = ClassloaderUtil.compareAndSwapLoaders(serdeClassloader); + try { + var serdeClass = serdeClassloader.loadClass(className); + var serde = (Serde) serdeClass.getDeclaredConstructor().newInstance(); + serde.configure(serdeProps, clusterProps, globalProps); + return new CustomSerde(serde, serdeClassloader); + } finally { + ClassloaderUtil.compareAndSwapLoaders(origCL); + } + } + + private static boolean isArchive(Path path) { + String archivePath = path.toString().toLowerCase(); + return Files.isReadable(path) + && Files.isRegularFile(path) + && (archivePath.endsWith(".jar") || archivePath.endsWith(".zip")); + } + + @SneakyThrows + private static List findArchiveFiles(Path location) { + if (isArchive(location)) { + return List.of(location.toUri().toURL()); + } + if (Files.isDirectory(location)) { + List archiveFiles = new ArrayList<>(); + try (var files = Files.walk(location)) { + var paths = files.filter(CustomSerdeLoader::isArchive).collect(Collectors.toList()); + for (Path path : paths) { + archiveFiles.add(path.toUri().toURL()); + } + } + return archiveFiles; + } + return List.of(); + } + + private ClassLoader createClassloader(Path location) { + if (!Files.exists(location)) { + throw new IllegalStateException("Location does not exist"); + } + var archives = findArchiveFiles(location); + if (archives.isEmpty()) { + throw new IllegalStateException("No archive files were found"); + } + // we assume that location's content does not change during serdes creation + // so, we can reuse already created classloaders + return classloaders.computeIfAbsent(location, l -> + AccessController.doPrivileged( + (PrivilegedAction) () -> + new ChildFirstClassloader( + archives.toArray(URL[]::new), + CustomSerdeLoader.class.getClassLoader()))); + } + + //--------------------------------------------------------------------------------- + + // This Classloader first tries to load classes by itself. If class not fount + // search is propagated to parent (this is opposite to how usual classloaders work) + private static class ChildFirstClassloader extends URLClassLoader { + + private static final String JAVA_PACKAGE_PREFIX = "java."; + + ChildFirstClassloader(URL[] urls, ClassLoader parent) { + super(urls, parent); + } + + @Override + protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + // first check whether it's a system class, delegate to the system loader + if (name.startsWith(JAVA_PACKAGE_PREFIX)) { + return findSystemClass(name); + } + Class loadedClass = findLoadedClass(name); + if (loadedClass == null) { + try { + // start searching from current classloader + loadedClass = findClass(name); + } catch (ClassNotFoundException e) { + // if not found - going to parent + loadedClass = super.loadClass(name, resolve); + } + } + if (resolve) { + resolveClass(loadedClass); + } + return loadedClass; + } + + @Override + public Enumeration getResources(String name) throws IOException { + List allRes = new LinkedList<>(); + Enumeration thisRes = findResources(name); + if (thisRes != null) { + while (thisRes.hasMoreElements()) { + allRes.add(thisRes.nextElement()); + } + } + // then try finding resources from parent classloaders + Enumeration parentRes = super.findResources(name); + if (parentRes != null) { + while (parentRes.hasMoreElements()) { + allRes.add(parentRes.nextElement()); + } + } + return new Enumeration<>() { + final Iterator it = allRes.iterator(); + + @Override + public boolean hasMoreElements() { + return it.hasNext(); + } + + @Override + public URL nextElement() { + return it.next(); + } + }; + } + + @Override + public URL getResource(String name) { + URL res = findResource(name); + if (res == null) { + res = super.getResource(name); + } + return res; + } + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ProducerRecordCreator.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ProducerRecordCreator.java new file mode 100644 index 00000000000..f48f1c08099 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ProducerRecordCreator.java @@ -0,0 +1,38 @@ +package com.provectus.kafka.ui.serdes; + +import com.provectus.kafka.ui.serde.api.Serde; +import java.util.Map; +import javax.annotation.Nullable; +import lombok.RequiredArgsConstructor; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.header.Header; +import org.apache.kafka.common.header.internals.RecordHeader; +import org.apache.kafka.common.header.internals.RecordHeaders; + +@RequiredArgsConstructor +public class ProducerRecordCreator { + + private final Serde.Serializer keySerializer; + private final Serde.Serializer valuesSerializer; + + public ProducerRecord create(String topic, + @Nullable Integer partition, + @Nullable String key, + @Nullable String value, + @Nullable Map headers) { + return new ProducerRecord<>( + topic, + partition, + key == null ? null : keySerializer.serialize(key), + value == null ? null : valuesSerializer.serialize(value), + headers == null ? null : createHeaders(headers) + ); + } + + private Iterable
createHeaders(Map clientHeaders) { + RecordHeaders headers = new RecordHeaders(); + clientHeaders.forEach((k, v) -> headers.add(new RecordHeader(k, v.getBytes()))); + return headers; + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/PropertyResolverImpl.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/PropertyResolverImpl.java new file mode 100644 index 00000000000..4bf88cbac95 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/PropertyResolverImpl.java @@ -0,0 +1,64 @@ +package com.provectus.kafka.ui.serdes; + +import com.google.common.base.Preconditions; +import com.provectus.kafka.ui.serde.api.PropertyResolver; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.annotation.Nullable; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; + + +public class PropertyResolverImpl implements PropertyResolver { + + private final Binder binder; + + @Nullable + private final String prefix; + + public static PropertyResolverImpl empty() { + return new PropertyResolverImpl(new StandardEnvironment(), null); + } + + public PropertyResolverImpl(Environment env) { + this(env, null); + } + + public PropertyResolverImpl(Environment env, @Nullable String prefix) { + this.binder = Binder.get(env); + this.prefix = prefix; + } + + private ConfigurationPropertyName targetPropertyName(String key) { + Preconditions.checkNotNull(key); + Preconditions.checkState(!key.isBlank()); + String propertyName = prefix == null ? key : prefix + "." + key; + return ConfigurationPropertyName.adapt(propertyName, '.'); + } + + @Override + public Optional getProperty(String key, Class targetType) { + var targetKey = targetPropertyName(key); + var result = binder.bind(targetKey, Bindable.of(targetType)); + return result.isBound() ? Optional.of(result.get()) : Optional.empty(); + } + + @Override + public Optional> getListProperty(String key, Class itemType) { + var targetKey = targetPropertyName(key); + var listResult = binder.bind(targetKey, Bindable.listOf(itemType)); + return listResult.isBound() ? Optional.of(listResult.get()) : Optional.empty(); + } + + @Override + public Optional> getMapProperty(String key, Class keyType, Class valueType) { + var targetKey = targetPropertyName(key); + var mapResult = binder.bind(targetKey, Bindable.mapOf(keyType, valueType)); + return mapResult.isBound() ? Optional.of(mapResult.get()) : Optional.empty(); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/RecordHeaderImpl.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/RecordHeaderImpl.java new file mode 100644 index 00000000000..87cdcb0350b --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/RecordHeaderImpl.java @@ -0,0 +1,23 @@ +package com.provectus.kafka.ui.serdes; + +import com.provectus.kafka.ui.serde.api.RecordHeader; +import org.apache.kafka.common.header.Header; + +public class RecordHeaderImpl implements RecordHeader { + + private final Header header; + + public RecordHeaderImpl(Header header) { + this.header = header; + } + + @Override + public String key() { + return header.key(); + } + + @Override + public byte[] value() { + return header.value(); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/RecordHeadersImpl.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/RecordHeadersImpl.java new file mode 100644 index 00000000000..6ae81a7614c --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/RecordHeadersImpl.java @@ -0,0 +1,26 @@ +package com.provectus.kafka.ui.serdes; + +import com.google.common.collect.Iterators; +import com.provectus.kafka.ui.serde.api.RecordHeader; +import com.provectus.kafka.ui.serde.api.RecordHeaders; +import java.util.Iterator; +import org.apache.kafka.common.header.Headers; + + +public class RecordHeadersImpl implements RecordHeaders { + + private final Headers headers; + + public RecordHeadersImpl() { + this(new org.apache.kafka.common.header.internals.RecordHeaders()); + } + + public RecordHeadersImpl(Headers headers) { + this.headers = headers; + } + + @Override + public Iterator iterator() { + return Iterators.transform(headers.iterator(), RecordHeaderImpl::new); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/SerdeInstance.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/SerdeInstance.java new file mode 100644 index 00000000000..2826f89aa97 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/SerdeInstance.java @@ -0,0 +1,105 @@ +package com.provectus.kafka.ui.serdes; + +import com.provectus.kafka.ui.serde.api.SchemaDescription; +import com.provectus.kafka.ui.serde.api.Serde; +import java.io.Closeable; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.regex.Pattern; +import javax.annotation.Nullable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +public class SerdeInstance implements Closeable { + + @Getter + final String name; + + final Serde serde; + + @Nullable + final Pattern topicKeyPattern; + + @Nullable + final Pattern topicValuePattern; + + @Nullable // will be set for custom serdes + final ClassLoader classLoader; + + private T wrapWithClassloader(Supplier call) { + if (classLoader == null) { + return call.get(); + } + var origCl = ClassloaderUtil.compareAndSwapLoaders(classLoader); + try { + return call.get(); + } finally { + ClassloaderUtil.compareAndSwapLoaders(origCl); + } + } + + public Optional getSchema(String topic, Serde.Target type) { + try { + return wrapWithClassloader(() -> serde.getSchema(topic, type)); + } catch (Exception e) { + log.warn("Error getting schema for '{}'({}) with serde '{}'", topic, type, name, e); + return Optional.empty(); + } + } + + public Optional description() { + try { + return wrapWithClassloader(serde::getDescription); + } catch (Exception e) { + log.warn("Error getting description serde '{}'", name, e); + return Optional.empty(); + } + } + + public boolean canSerialize(String topic, Serde.Target type) { + try { + return wrapWithClassloader(() -> serde.canSerialize(topic, type)); + } catch (Exception e) { + log.warn("Error calling canSerialize for '{}'({}) with serde '{}'", topic, type, name, e); + return false; + } + } + + public boolean canDeserialize(String topic, Serde.Target type) { + try { + return wrapWithClassloader(() -> serde.canDeserialize(topic, type)); + } catch (Exception e) { + log.warn("Error calling canDeserialize for '{}'({}) with serde '{}'", topic, type, name, e); + return false; + } + } + + public Serde.Serializer serializer(String topic, Serde.Target type) { + return wrapWithClassloader(() -> { + var serializer = serde.serializer(topic, type); + return input -> wrapWithClassloader(() -> serializer.serialize(input)); + }); + } + + public Serde.Deserializer deserializer(String topic, Serde.Target type) { + return wrapWithClassloader(() -> { + var deserializer = serde.deserializer(topic, type); + return (headers, data) -> wrapWithClassloader(() -> deserializer.deserialize(headers, data)); + }); + } + + @Override + public void close() { + wrapWithClassloader(() -> { + try { + serde.close(); + } catch (Exception e) { + log.error("Error closing serde " + name, e); + } + return null; + }); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/SerdesInitializer.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/SerdesInitializer.java new file mode 100644 index 00000000000..6e28c2fdcfb --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/SerdesInitializer.java @@ -0,0 +1,281 @@ +package com.provectus.kafka.ui.serdes; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.provectus.kafka.ui.config.ClustersProperties; +import com.provectus.kafka.ui.config.ClustersProperties.SerdeConfig; +import com.provectus.kafka.ui.exception.ValidationException; +import com.provectus.kafka.ui.serde.api.PropertyResolver; +import com.provectus.kafka.ui.serde.api.Serde; +import com.provectus.kafka.ui.serdes.builtin.AvroEmbeddedSerde; +import com.provectus.kafka.ui.serdes.builtin.Base64Serde; +import com.provectus.kafka.ui.serdes.builtin.ConsumerOffsetsSerde; +import com.provectus.kafka.ui.serdes.builtin.HexSerde; +import com.provectus.kafka.ui.serdes.builtin.Int32Serde; +import com.provectus.kafka.ui.serdes.builtin.Int64Serde; +import com.provectus.kafka.ui.serdes.builtin.ProtobufFileSerde; +import com.provectus.kafka.ui.serdes.builtin.ProtobufRawSerde; +import com.provectus.kafka.ui.serdes.builtin.StringSerde; +import com.provectus.kafka.ui.serdes.builtin.UInt32Serde; +import com.provectus.kafka.ui.serdes.builtin.UInt64Serde; +import com.provectus.kafka.ui.serdes.builtin.UuidBinarySerde; +import com.provectus.kafka.ui.serdes.builtin.sr.SchemaRegistrySerde; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; +import javax.annotation.Nullable; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.env.Environment; + +@Slf4j +public class SerdesInitializer { + + private final Map> builtInSerdeClasses; + private final CustomSerdeLoader customSerdeLoader; + + public SerdesInitializer() { + this( + ImmutableMap.>builder() + .put(StringSerde.name(), StringSerde.class) + .put(SchemaRegistrySerde.name(), SchemaRegistrySerde.class) + .put(ProtobufFileSerde.name(), ProtobufFileSerde.class) + .put(Int32Serde.name(), Int32Serde.class) + .put(Int64Serde.name(), Int64Serde.class) + .put(UInt32Serde.name(), UInt32Serde.class) + .put(UInt64Serde.name(), UInt64Serde.class) + .put(AvroEmbeddedSerde.name(), AvroEmbeddedSerde.class) + .put(Base64Serde.name(), Base64Serde.class) + .put(HexSerde.name(), HexSerde.class) + .put(UuidBinarySerde.name(), UuidBinarySerde.class) + .put(ProtobufRawSerde.name(), ProtobufRawSerde.class) + .build(), + new CustomSerdeLoader() + ); + } + + @VisibleForTesting + SerdesInitializer(Map> builtInSerdeClasses, + CustomSerdeLoader customSerdeLoader) { + this.builtInSerdeClasses = builtInSerdeClasses; + this.customSerdeLoader = customSerdeLoader; + } + + /** + * Initialization algorithm: + * First, we iterate over explicitly configured serdes from cluster config: + * > if serde has name = one of built-in serde's names: + * - if serde's properties are empty, we treat it as serde should be + * auto-configured - we try to do that + * - if serde's properties not empty, we treat it as an intention to + * override default configuration, so we configuring it with specific config (calling configure(..)) + *

+ * > if serde has className = one of built-in serde's classes: + * - initializing it with specific config and with default classloader + *

+ * > if serde has custom className != one of built-in serde's classes: + * - initializing it with specific config and with custom classloader (see CustomSerdeLoader) + *

+ * Second, we iterate over remaining built-in serdes (that we NOT explicitly configured by config) + * trying to auto-configure them and registering with empty patterns - they will be present + * in Serde selection in UI, but not assigned to any topic k/v. + */ + public ClusterSerdes init(Environment env, + ClustersProperties clustersProperties, + int clusterIndex) { + ClustersProperties.Cluster clusterProperties = clustersProperties.getClusters().get(clusterIndex); + log.debug("Configuring serdes for cluster {}", clusterProperties.getName()); + + var globalPropertiesResolver = new PropertyResolverImpl(env); + var clusterPropertiesResolver = new PropertyResolverImpl(env, "kafka.clusters." + clusterIndex); + + Map registeredSerdes = new LinkedHashMap<>(); + // initializing serdes from config + if (clusterProperties.getSerde() != null) { + for (int i = 0; i < clusterProperties.getSerde().size(); i++) { + SerdeConfig serdeConfig = clusterProperties.getSerde().get(i); + if (Strings.isNullOrEmpty(serdeConfig.getName())) { + throw new ValidationException("'name' property not set for serde: " + serdeConfig); + } + if (registeredSerdes.containsKey(serdeConfig.getName())) { + throw new ValidationException("Multiple serdes with same name: " + serdeConfig.getName()); + } + var instance = createSerdeFromConfig( + serdeConfig, + new PropertyResolverImpl(env, "kafka.clusters." + clusterIndex + ".serde." + i + ".properties"), + clusterPropertiesResolver, + globalPropertiesResolver + ); + registeredSerdes.put(serdeConfig.getName(), instance); + } + } + + // initializing remaining built-in serdes with empty selection patters + builtInSerdeClasses.forEach((name, clazz) -> { + if (!registeredSerdes.containsKey(name)) { + BuiltInSerde serde = createSerdeInstance(clazz); + if (autoConfigureSerde(serde, clusterPropertiesResolver, globalPropertiesResolver)) { + registeredSerdes.put(name, new SerdeInstance(name, serde, null, null, null)); + } + } + }); + + registerTopicRelatedSerde(registeredSerdes); + + return new ClusterSerdes( + registeredSerdes, + Optional.ofNullable(clusterProperties.getDefaultKeySerde()) + .map(name -> Preconditions.checkNotNull(registeredSerdes.get(name), "Default key serde not found")) + .orElse(null), + Optional.ofNullable(clusterProperties.getDefaultValueSerde()) + .map(name -> Preconditions.checkNotNull(registeredSerdes.get(name), "Default value serde not found")) + .or(() -> Optional.ofNullable(registeredSerdes.get(SchemaRegistrySerde.name()))) + .or(() -> Optional.ofNullable(registeredSerdes.get(ProtobufFileSerde.name()))) + .orElse(null), + createFallbackSerde() + ); + } + + /** + * Registers serdse that should only be used for specific (hard-coded) topics, like ConsumerOffsetsSerde. + */ + private void registerTopicRelatedSerde(Map serdes) { + registerConsumerOffsetsSerde(serdes); + } + + private void registerConsumerOffsetsSerde(Map serdes) { + var pattern = Pattern.compile(ConsumerOffsetsSerde.TOPIC); + serdes.put( + ConsumerOffsetsSerde.name(), + new SerdeInstance( + ConsumerOffsetsSerde.name(), + new ConsumerOffsetsSerde(), + pattern, + pattern, + null + ) + ); + } + + private SerdeInstance createFallbackSerde() { + StringSerde serde = new StringSerde(); + serde.configure(PropertyResolverImpl.empty(), PropertyResolverImpl.empty(), PropertyResolverImpl.empty()); + return new SerdeInstance("Fallback", serde, null, null, null); + } + + @SneakyThrows + private SerdeInstance createSerdeFromConfig(SerdeConfig serdeConfig, + PropertyResolver serdeProps, + PropertyResolver clusterProps, + PropertyResolver globalProps) { + if (builtInSerdeClasses.containsKey(serdeConfig.getName())) { + return createSerdeWithBuiltInSerdeName(serdeConfig, serdeProps, clusterProps, globalProps); + } + if (serdeConfig.getClassName() != null) { + var builtInSerdeClass = builtInSerdeClasses.values().stream() + .filter(c -> c.getName().equals(serdeConfig.getClassName())) + .findAny(); + // built-in serde type with custom name + if (builtInSerdeClass.isPresent()) { + return createSerdeWithBuiltInClass(builtInSerdeClass.get(), serdeConfig, serdeProps, clusterProps, globalProps); + } + } + log.info("Loading custom serde {}", serdeConfig.getName()); + return loadAndInitCustomSerde(serdeConfig, serdeProps, clusterProps, globalProps); + } + + private SerdeInstance createSerdeWithBuiltInSerdeName(SerdeConfig serdeConfig, + PropertyResolver serdeProps, + PropertyResolver clusterProps, + PropertyResolver globalProps) { + String name = serdeConfig.getName(); + if (serdeConfig.getClassName() != null) { + throw new ValidationException("className can't be set for built-in serde"); + } + if (serdeConfig.getFilePath() != null) { + throw new ValidationException("filePath can't be set for built-in serde types"); + } + var clazz = builtInSerdeClasses.get(name); + BuiltInSerde serde = createSerdeInstance(clazz); + if (serdeConfig.getProperties() == null || serdeConfig.getProperties().isEmpty()) { + if (!autoConfigureSerde(serde, clusterProps, globalProps)) { + // no properties provided and serde does not support auto-configuration + throw new ValidationException(name + " serde is not configured"); + } + } else { + // configuring serde with explicitly set properties + serde.configure(serdeProps, clusterProps, globalProps); + } + return new SerdeInstance( + name, + serde, + nullablePattern(serdeConfig.getTopicKeysPattern()), + nullablePattern(serdeConfig.getTopicValuesPattern()), + null + ); + } + + private boolean autoConfigureSerde(BuiltInSerde serde, PropertyResolver clusterProps, PropertyResolver globalProps) { + if (serde.canBeAutoConfigured(clusterProps, globalProps)) { + serde.autoConfigure(clusterProps, globalProps); + return true; + } + return false; + } + + @SneakyThrows + private SerdeInstance createSerdeWithBuiltInClass(Class clazz, + SerdeConfig serdeConfig, + PropertyResolver serdeProps, + PropertyResolver clusterProps, + PropertyResolver globalProps) { + if (serdeConfig.getFilePath() != null) { + throw new ValidationException("filePath can't be set for built-in serde type"); + } + BuiltInSerde serde = createSerdeInstance(clazz); + serde.configure(serdeProps, clusterProps, globalProps); + return new SerdeInstance( + serdeConfig.getName(), + serde, + nullablePattern(serdeConfig.getTopicKeysPattern()), + nullablePattern(serdeConfig.getTopicValuesPattern()), + null + ); + } + + @SneakyThrows + private T createSerdeInstance(Class clazz) { + return clazz.getDeclaredConstructor().newInstance(); + } + + private SerdeInstance loadAndInitCustomSerde(SerdeConfig serdeConfig, + PropertyResolver serdeProps, + PropertyResolver clusterProps, + PropertyResolver globalProps) { + if (Strings.isNullOrEmpty(serdeConfig.getClassName())) { + throw new ValidationException( + "'className' property not set for custom serde " + serdeConfig.getName()); + } + if (Strings.isNullOrEmpty(serdeConfig.getFilePath())) { + throw new ValidationException( + "'filePath' property not set for custom serde " + serdeConfig.getName()); + } + var loaded = customSerdeLoader.loadAndConfigure( + serdeConfig.getClassName(), serdeConfig.getFilePath(), serdeProps, clusterProps, globalProps); + return new SerdeInstance( + serdeConfig.getName(), + loaded.getSerde(), + nullablePattern(serdeConfig.getTopicKeysPattern()), + nullablePattern(serdeConfig.getTopicValuesPattern()), + loaded.getClassLoader() + ); + } + + @Nullable + private Pattern nullablePattern(@Nullable String pattern) { + return pattern == null ? null : Pattern.compile(pattern); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/AvroEmbeddedSerde.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/AvroEmbeddedSerde.java new file mode 100644 index 00000000000..68ea03cfa65 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/AvroEmbeddedSerde.java @@ -0,0 +1,66 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.PropertyResolver; +import com.provectus.kafka.ui.serde.api.RecordHeaders; +import com.provectus.kafka.ui.serde.api.SchemaDescription; +import com.provectus.kafka.ui.serdes.BuiltInSerde; +import io.confluent.kafka.schemaregistry.avro.AvroSchemaUtils; +import java.util.Map; +import java.util.Optional; +import lombok.SneakyThrows; +import org.apache.avro.file.DataFileReader; +import org.apache.avro.file.SeekableByteArrayInput; +import org.apache.avro.generic.GenericDatumReader; + +public class AvroEmbeddedSerde implements BuiltInSerde { + + public static String name() { + return "Avro (Embedded)"; + } + + @Override + public Optional getDescription() { + return Optional.empty(); + } + + @Override + public Optional getSchema(String topic, Target type) { + return Optional.empty(); + } + + @Override + public boolean canDeserialize(String topic, Target type) { + return true; + } + + @Override + public boolean canSerialize(String topic, Target type) { + return false; + } + + @Override + public Serializer serializer(String topic, Target type) { + throw new IllegalStateException(); + } + + @Override + public Deserializer deserializer(String topic, Target type) { + return new Deserializer() { + @SneakyThrows + @Override + public DeserializeResult deserialize(RecordHeaders headers, byte[] data) { + try (var reader = new DataFileReader<>(new SeekableByteArrayInput(data), new GenericDatumReader<>())) { + if (!reader.hasNext()) { + // this is very strange situation, when only header present in payload + // returning null in this case + return new DeserializeResult(null, DeserializeResult.Type.JSON, Map.of()); + } + Object avroObj = reader.next(); + String jsonValue = new String(AvroSchemaUtils.toJson(avroObj)); + return new DeserializeResult(jsonValue, DeserializeResult.Type.JSON, Map.of()); + } + } + }; + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/Base64Serde.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/Base64Serde.java new file mode 100644 index 00000000000..9c2b00bfdf2 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/Base64Serde.java @@ -0,0 +1,59 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.SchemaDescription; +import com.provectus.kafka.ui.serdes.BuiltInSerde; +import java.util.Base64; +import java.util.Map; +import java.util.Optional; + +public class Base64Serde implements BuiltInSerde { + + public static String name() { + return "Base64"; + } + + @Override + public Optional getDescription() { + return Optional.empty(); + } + + @Override + public Optional getSchema(String topic, Target type) { + return Optional.empty(); + } + + @Override + public boolean canDeserialize(String topic, Target type) { + return true; + } + + @Override + public boolean canSerialize(String topic, Target type) { + return true; + } + + @Override + public Serializer serializer(String topic, Target type) { + var decoder = Base64.getDecoder(); + return inputString -> { + inputString = inputString.trim(); + // it is actually a hack to provide ability to sent empty array as a key/value + if (inputString.length() == 0) { + return new byte[] {}; + } + return decoder.decode(inputString); + }; + } + + @Override + public Deserializer deserializer(String topic, Target type) { + var encoder = Base64.getEncoder(); + return (headers, data) -> + new DeserializeResult( + encoder.encodeToString(data), + DeserializeResult.Type.STRING, + Map.of() + ); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/ConsumerOffsetsSerde.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/ConsumerOffsetsSerde.java new file mode 100644 index 00000000000..8a0c0e2bfa7 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/ConsumerOffsetsSerde.java @@ -0,0 +1,312 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.SchemaDescription; +import com.provectus.kafka.ui.serdes.BuiltInSerde; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.Optional; +import lombok.SneakyThrows; +import org.apache.kafka.common.protocol.types.ArrayOf; +import org.apache.kafka.common.protocol.types.BoundField; +import org.apache.kafka.common.protocol.types.CompactArrayOf; +import org.apache.kafka.common.protocol.types.Field; +import org.apache.kafka.common.protocol.types.Schema; +import org.apache.kafka.common.protocol.types.Struct; +import org.apache.kafka.common.protocol.types.Type; + +// Deserialization logic and message's schemas can be found in +// kafka.coordinator.group.GroupMetadataManager (readMessageKey, readOffsetMessageValue, readGroupMessageValue) +public class ConsumerOffsetsSerde implements BuiltInSerde { + + private static final JsonMapper JSON_MAPPER = createMapper(); + + private static final String ASSIGNMENT = "assignment"; + private static final String CLIENT_HOST = "client_host"; + private static final String CLIENT_ID = "client_id"; + private static final String COMMIT_TIMESTAMP = "commit_timestamp"; + private static final String CURRENT_STATE_TIMESTAMP = "current_state_timestamp"; + private static final String GENERATION = "generation"; + private static final String LEADER = "leader"; + private static final String MEMBERS = "members"; + private static final String MEMBER_ID = "member_id"; + private static final String METADATA = "metadata"; + private static final String OFFSET = "offset"; + private static final String PROTOCOL = "protocol"; + private static final String PROTOCOL_TYPE = "protocol_type"; + private static final String REBALANCE_TIMEOUT = "rebalance_timeout"; + private static final String SESSION_TIMEOUT = "session_timeout"; + private static final String SUBSCRIPTION = "subscription"; + + public static final String TOPIC = "__consumer_offsets"; + + public static String name() { + return "__consumer_offsets"; + } + + private static JsonMapper createMapper() { + var module = new SimpleModule(); + module.addSerializer(Struct.class, new JsonSerializer<>() { + @Override + public void serialize(Struct value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + for (BoundField field : value.schema().fields()) { + var fieldVal = value.get(field); + gen.writeObjectField(field.def.name, fieldVal); + } + gen.writeEndObject(); + } + }); + var mapper = new JsonMapper(); + mapper.registerModule(module); + return mapper; + } + + @Override + public Optional getDescription() { + return Optional.empty(); + } + + @Override + public Optional getSchema(String topic, Target type) { + return Optional.empty(); + } + + @Override + public boolean canDeserialize(String topic, Target type) { + return topic.equals(TOPIC); + } + + @Override + public boolean canSerialize(String topic, Target type) { + return false; + } + + @Override + public Serializer serializer(String topic, Target type) { + throw new UnsupportedOperationException(); + } + + @Override + public Deserializer deserializer(String topic, Target type) { + return switch (type) { + case KEY -> keyDeserializer(); + case VALUE -> valueDeserializer(); + }; + } + + private Deserializer keyDeserializer() { + final Schema commitKeySchema = new Schema( + new Field("group", Type.STRING, ""), + new Field("topic", Type.STRING, ""), + new Field("partition", Type.INT32, "") + ); + + final Schema groupMetadataSchema = new Schema( + new Field("group", Type.STRING, "") + ); + + return (headers, data) -> { + var bb = ByteBuffer.wrap(data); + short version = bb.getShort(); + return new DeserializeResult( + toJson( + switch (version) { + case 0, 1 -> commitKeySchema.read(bb); + case 2 -> groupMetadataSchema.read(bb); + default -> throw new IllegalStateException("Unknown group metadata message version: " + version); + } + ), + DeserializeResult.Type.JSON, + Map.of() + ); + }; + } + + private Deserializer valueDeserializer() { + final Schema commitOffsetSchemaV0 = + new Schema( + new Field(OFFSET, Type.INT64, ""), + new Field(METADATA, Type.STRING, ""), + new Field(COMMIT_TIMESTAMP, Type.INT64, "") + ); + + final Schema commitOffsetSchemaV1 = + new Schema( + new Field(OFFSET, Type.INT64, ""), + new Field(METADATA, Type.STRING, ""), + new Field(COMMIT_TIMESTAMP, Type.INT64, ""), + new Field("expire_timestamp", Type.INT64, "") + ); + + final Schema commitOffsetSchemaV2 = + new Schema( + new Field(OFFSET, Type.INT64, ""), + new Field(METADATA, Type.STRING, ""), + new Field(COMMIT_TIMESTAMP, Type.INT64, "") + ); + + final Schema commitOffsetSchemaV3 = + new Schema( + new Field(OFFSET, Type.INT64, ""), + new Field("leader_epoch", Type.INT32, ""), + new Field(METADATA, Type.STRING, ""), + new Field(COMMIT_TIMESTAMP, Type.INT64, "") + ); + + final Schema commitOffsetSchemaV4 = new Schema( + new Field(OFFSET, Type.INT64, ""), + new Field("leader_epoch", Type.INT32, ""), + new Field(METADATA, Type.COMPACT_STRING, ""), + new Field(COMMIT_TIMESTAMP, Type.INT64, ""), + Field.TaggedFieldsSection.of() + ); + + final Schema metadataSchema0 = + new Schema( + new Field(PROTOCOL_TYPE, Type.STRING, ""), + new Field(GENERATION, Type.INT32, ""), + new Field(PROTOCOL, Type.NULLABLE_STRING, ""), + new Field(LEADER, Type.NULLABLE_STRING, ""), + new Field(MEMBERS, new ArrayOf(new Schema( + new Field(MEMBER_ID, Type.STRING, ""), + new Field(CLIENT_ID, Type.STRING, ""), + new Field(CLIENT_HOST, Type.STRING, ""), + new Field(SESSION_TIMEOUT, Type.INT32, ""), + new Field(SUBSCRIPTION, Type.BYTES, ""), + new Field(ASSIGNMENT, Type.BYTES, "") + )), "") + ); + + final Schema metadataSchema1 = + new Schema( + new Field(PROTOCOL_TYPE, Type.STRING, ""), + new Field(GENERATION, Type.INT32, ""), + new Field(PROTOCOL, Type.NULLABLE_STRING, ""), + new Field(LEADER, Type.NULLABLE_STRING, ""), + new Field(MEMBERS, new ArrayOf(new Schema( + new Field(MEMBER_ID, Type.STRING, ""), + new Field(CLIENT_ID, Type.STRING, ""), + new Field(CLIENT_HOST, Type.STRING, ""), + new Field(REBALANCE_TIMEOUT, Type.INT32, ""), + new Field(SESSION_TIMEOUT, Type.INT32, ""), + new Field(SUBSCRIPTION, Type.BYTES, ""), + new Field(ASSIGNMENT, Type.BYTES, "") + )), "") + ); + + final Schema metadataSchema2 = + new Schema( + new Field(PROTOCOL_TYPE, Type.STRING, ""), + new Field(GENERATION, Type.INT32, ""), + new Field(PROTOCOL, Type.NULLABLE_STRING, ""), + new Field(LEADER, Type.NULLABLE_STRING, ""), + new Field(CURRENT_STATE_TIMESTAMP, Type.INT64, ""), + new Field(MEMBERS, new ArrayOf(new Schema( + new Field(MEMBER_ID, Type.STRING, ""), + new Field(CLIENT_ID, Type.STRING, ""), + new Field(CLIENT_HOST, Type.STRING, ""), + new Field(REBALANCE_TIMEOUT, Type.INT32, ""), + new Field(SESSION_TIMEOUT, Type.INT32, ""), + new Field(SUBSCRIPTION, Type.BYTES, ""), + new Field(ASSIGNMENT, Type.BYTES, "") + )), "") + ); + + final Schema metadataSchema3 = + new Schema( + new Field(PROTOCOL_TYPE, Type.STRING, ""), + new Field(GENERATION, Type.INT32, ""), + new Field(PROTOCOL, Type.NULLABLE_STRING, ""), + new Field(LEADER, Type.NULLABLE_STRING, ""), + new Field(CURRENT_STATE_TIMESTAMP, Type.INT64, ""), + new Field(MEMBERS, new ArrayOf(new Schema( + new Field(MEMBER_ID, Type.STRING, ""), + new Field("group_instance_id", Type.NULLABLE_STRING, ""), + new Field(CLIENT_ID, Type.STRING, ""), + new Field(CLIENT_HOST, Type.STRING, ""), + new Field(REBALANCE_TIMEOUT, Type.INT32, ""), + new Field(SESSION_TIMEOUT, Type.INT32, ""), + new Field(SUBSCRIPTION, Type.BYTES, ""), + new Field(ASSIGNMENT, Type.BYTES, "") + )), "") + ); + + final Schema metadataSchema4 = + new Schema( + new Field(PROTOCOL_TYPE, Type.COMPACT_STRING, ""), + new Field(GENERATION, Type.INT32, ""), + new Field(PROTOCOL, Type.COMPACT_NULLABLE_STRING, ""), + new Field(LEADER, Type.COMPACT_NULLABLE_STRING, ""), + new Field(CURRENT_STATE_TIMESTAMP, Type.INT64, ""), + new Field(MEMBERS, new CompactArrayOf(new Schema( + new Field(MEMBER_ID, Type.COMPACT_STRING, ""), + new Field("group_instance_id", Type.COMPACT_NULLABLE_STRING, ""), + new Field(CLIENT_ID, Type.COMPACT_STRING, ""), + new Field(CLIENT_HOST, Type.COMPACT_STRING, ""), + new Field(REBALANCE_TIMEOUT, Type.INT32, ""), + new Field(SESSION_TIMEOUT, Type.INT32, ""), + new Field(SUBSCRIPTION, Type.COMPACT_BYTES, ""), + new Field(ASSIGNMENT, Type.COMPACT_BYTES, ""), + Field.TaggedFieldsSection.of() + )), ""), + Field.TaggedFieldsSection.of() + ); + + return (headers, data) -> { + String result; + var bb = ByteBuffer.wrap(data); + short version = bb.getShort(); + // ideally, we should distinguish if value is commit or metadata + // by checking record's key, but our current serde structure doesn't allow that. + // so, we are trying to parse into metadata first and after into commit msg + try { + result = toJson( + switch (version) { + case 0 -> metadataSchema0.read(bb); + case 1 -> metadataSchema1.read(bb); + case 2 -> metadataSchema2.read(bb); + case 3 -> metadataSchema3.read(bb); + case 4 -> metadataSchema4.read(bb); + default -> throw new IllegalArgumentException("Unrecognized version: " + version); + } + ); + } catch (Throwable e) { + bb = bb.rewind(); + bb.getShort(); // skipping version + result = toJson( + switch (version) { + case 0 -> commitOffsetSchemaV0.read(bb); + case 1 -> commitOffsetSchemaV1.read(bb); + case 2 -> commitOffsetSchemaV2.read(bb); + case 3 -> commitOffsetSchemaV3.read(bb); + case 4 -> commitOffsetSchemaV4.read(bb); + default -> throw new IllegalArgumentException("Unrecognized version: " + version); + } + ); + } + + if (bb.remaining() != 0) { + throw new IllegalArgumentException( + "Message buffer is not read to the end, which is likely means message is unrecognized"); + } + return new DeserializeResult( + result, + DeserializeResult.Type.JSON, + Map.of() + ); + }; + } + + @SneakyThrows + private String toJson(Struct s) { + return JSON_MAPPER.writeValueAsString(s); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/HexSerde.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/HexSerde.java new file mode 100644 index 00000000000..343bb4e705d --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/HexSerde.java @@ -0,0 +1,89 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.PropertyResolver; +import com.provectus.kafka.ui.serde.api.SchemaDescription; +import com.provectus.kafka.ui.serdes.BuiltInSerde; +import java.util.HexFormat; +import java.util.Map; +import java.util.Optional; + +public class HexSerde implements BuiltInSerde { + + private HexFormat deserializeHexFormat; + + public static String name() { + return "Hex"; + } + + @Override + public void autoConfigure(PropertyResolver kafkaClusterProperties, PropertyResolver globalProperties) { + configure(" ", true); + } + + @Override + public void configure(PropertyResolver serdeProperties, + PropertyResolver kafkaClusterProperties, + PropertyResolver globalProperties) { + String delim = serdeProperties.getProperty("delimiter", String.class).orElse(" "); + boolean uppercase = serdeProperties.getProperty("uppercase", Boolean.class).orElse(true); + configure(delim, uppercase); + } + + private void configure(String delim, boolean uppercase) { + deserializeHexFormat = HexFormat.ofDelimiter(delim); + if (uppercase) { + deserializeHexFormat = deserializeHexFormat.withUpperCase(); + } + } + + @Override + public Optional getDescription() { + return Optional.empty(); + } + + @Override + public Optional getSchema(String topic, Target type) { + return Optional.empty(); + } + + @Override + public boolean canDeserialize(String topic, Target type) { + return true; + } + + @Override + public boolean canSerialize(String topic, Target type) { + return true; + } + + @Override + public Serializer serializer(String topic, Target type) { + return input -> { + input = input.trim(); + // it is a hack to provide ability to sent empty array as a key/value + if (input.length() == 0) { + return new byte[] {}; + } + return HexFormat.of().parseHex(prepareInputForParse(input)); + }; + } + + // removing most-common delimiters and prefixes + private static String prepareInputForParse(String input) { + return input + .replaceAll(" ", "") + .replaceAll("#", "") + .replaceAll(":", ""); + } + + @Override + public Deserializer deserializer(String topic, Target type) { + return (headers, data) -> + new DeserializeResult( + deserializeHexFormat.formatHex(data), + DeserializeResult.Type.STRING, + Map.of() + ); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/Int32Serde.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/Int32Serde.java new file mode 100644 index 00000000000..e89c7996060 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/Int32Serde.java @@ -0,0 +1,63 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import com.google.common.primitives.Ints; +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.SchemaDescription; +import com.provectus.kafka.ui.serdes.BuiltInSerde; +import java.util.Map; +import java.util.Optional; + +public class Int32Serde implements BuiltInSerde { + + public static String name() { + return "Int32"; + } + + @Override + public Optional getDescription() { + return Optional.empty(); + } + + @Override + public Optional getSchema(String topic, Target type) { + return Optional.of( + new SchemaDescription( + String.format( + "{ " + + " \"type\" : \"integer\", " + + " \"minimum\" : %s, " + + " \"maximum\" : %s " + + "}", + Integer.MIN_VALUE, + Integer.MAX_VALUE + ), + Map.of() + ) + ); + } + + @Override + public boolean canDeserialize(String topic, Target type) { + return true; + } + + @Override + public boolean canSerialize(String topic, Target type) { + return true; + } + + @Override + public Serializer serializer(String topic, Target type) { + return input -> Ints.toByteArray(Integer.parseInt(input)); + } + + @Override + public Deserializer deserializer(String topic, Target type) { + return (headers, data) -> + new DeserializeResult( + String.valueOf(Ints.fromByteArray(data)), + DeserializeResult.Type.JSON, + Map.of() + ); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/Int64Serde.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/Int64Serde.java new file mode 100644 index 00000000000..a897f941e0d --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/Int64Serde.java @@ -0,0 +1,65 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import com.google.common.primitives.Longs; +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.PropertyResolver; +import com.provectus.kafka.ui.serde.api.RecordHeaders; +import com.provectus.kafka.ui.serde.api.SchemaDescription; +import com.provectus.kafka.ui.serdes.BuiltInSerde; +import java.util.Map; +import java.util.Optional; + +public class Int64Serde implements BuiltInSerde { + + public static String name() { + return "Int64"; + } + + @Override + public Optional getDescription() { + return Optional.empty(); + } + + @Override + public Optional getSchema(String topic, Target type) { + return Optional.of( + new SchemaDescription( + String.format( + "{ " + + " \"type\" : \"integer\", " + + " \"minimum\" : %s, " + + " \"maximum\" : %s " + + "}", + Long.MIN_VALUE, + Long.MAX_VALUE + ), + Map.of() + ) + ); + } + + @Override + public boolean canDeserialize(String topic, Target type) { + return true; + } + + @Override + public boolean canSerialize(String topic, Target type) { + return true; + } + + @Override + public Serializer serializer(String topic, Target type) { + return input -> Longs.toByteArray(Long.parseLong(input)); + } + + @Override + public Deserializer deserializer(String topic, Target type) { + return (headers, data) -> + new DeserializeResult( + String.valueOf(Longs.fromByteArray(data)), + DeserializeResult.Type.JSON, + Map.of() + ); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/ProtobufFileSerde.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/ProtobufFileSerde.java new file mode 100644 index 00000000000..05809e26912 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/ProtobufFileSerde.java @@ -0,0 +1,417 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.protobuf.AnyProto; +import com.google.protobuf.ApiProto; +import com.google.protobuf.DescriptorProtos; +import com.google.protobuf.Descriptors; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.DurationProto; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.EmptyProto; +import com.google.protobuf.FieldMaskProto; +import com.google.protobuf.SourceContextProto; +import com.google.protobuf.StructProto; +import com.google.protobuf.TimestampProto; +import com.google.protobuf.TypeProto; +import com.google.protobuf.WrappersProto; +import com.google.protobuf.util.JsonFormat; +import com.google.type.ColorProto; +import com.google.type.DateProto; +import com.google.type.DateTimeProto; +import com.google.type.DayOfWeekProto; +import com.google.type.ExprProto; +import com.google.type.FractionProto; +import com.google.type.IntervalProto; +import com.google.type.LatLngProto; +import com.google.type.MoneyProto; +import com.google.type.MonthProto; +import com.google.type.PhoneNumberProto; +import com.google.type.PostalAddressProto; +import com.google.type.QuaternionProto; +import com.google.type.TimeOfDayProto; +import com.provectus.kafka.ui.exception.ValidationException; +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.PropertyResolver; +import com.provectus.kafka.ui.serde.api.RecordHeaders; +import com.provectus.kafka.ui.serde.api.SchemaDescription; +import com.provectus.kafka.ui.serdes.BuiltInSerde; +import com.provectus.kafka.ui.util.jsonschema.ProtobufSchemaConverter; +import com.squareup.wire.schema.ErrorCollector; +import com.squareup.wire.schema.Linker; +import com.squareup.wire.schema.Loader; +import com.squareup.wire.schema.Location; +import com.squareup.wire.schema.ProtoFile; +import com.squareup.wire.schema.internal.parser.ProtoFileElement; +import com.squareup.wire.schema.internal.parser.ProtoParser; +import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema; +import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchemaUtils; +import java.io.ByteArrayInputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; + +@Slf4j +public class ProtobufFileSerde implements BuiltInSerde { + + public static String name() { + return "ProtobufFile"; + } + + private static final ProtobufSchemaConverter SCHEMA_CONVERTER = new ProtobufSchemaConverter(); + + private Map messageDescriptorMap = new HashMap<>(); + private Map keyMessageDescriptorMap = new HashMap<>(); + + private Map descriptorPaths = new HashMap<>(); + + @Nullable + private Descriptor defaultMessageDescriptor; + + @Nullable + private Descriptor defaultKeyMessageDescriptor; + + @Override + public boolean canBeAutoConfigured(PropertyResolver kafkaClusterProperties, + PropertyResolver globalProperties) { + return Configuration.canBeAutoConfigured(kafkaClusterProperties); + } + + @Override + public void autoConfigure(PropertyResolver kafkaClusterProperties, + PropertyResolver globalProperties) { + configure(Configuration.create(kafkaClusterProperties)); + } + + @Override + public void configure(PropertyResolver serdeProperties, + PropertyResolver kafkaClusterProperties, + PropertyResolver globalProperties) { + configure(Configuration.create(serdeProperties)); + } + + @VisibleForTesting + void configure(Configuration configuration) { + if (configuration.defaultMessageDescriptor() == null + && configuration.defaultKeyMessageDescriptor() == null + && configuration.messageDescriptorMap().isEmpty() + && configuration.keyMessageDescriptorMap().isEmpty()) { + throw new ValidationException("Neither default, not per-topic descriptors defined for " + name() + " serde"); + } + this.defaultMessageDescriptor = configuration.defaultMessageDescriptor(); + this.defaultKeyMessageDescriptor = configuration.defaultKeyMessageDescriptor(); + this.descriptorPaths = configuration.descriptorPaths(); + this.messageDescriptorMap = configuration.messageDescriptorMap(); + this.keyMessageDescriptorMap = configuration.keyMessageDescriptorMap(); + } + + @Override + public Optional getDescription() { + return Optional.empty(); + } + + private Optional descriptorFor(String topic, Target type) { + return type == Target.KEY + ? + Optional.ofNullable(keyMessageDescriptorMap.get(topic)) + .or(() -> Optional.ofNullable(defaultKeyMessageDescriptor)) + : + Optional.ofNullable(messageDescriptorMap.get(topic)) + .or(() -> Optional.ofNullable(defaultMessageDescriptor)); + } + + @Override + public boolean canDeserialize(String topic, Target type) { + return descriptorFor(topic, type).isPresent(); + } + + @Override + public boolean canSerialize(String topic, Target type) { + return descriptorFor(topic, type).isPresent(); + } + + @Override + public Serializer serializer(String topic, Target type) { + var descriptor = descriptorFor(topic, type).orElseThrow(); + return new Serializer() { + @SneakyThrows + @Override + public byte[] serialize(String input) { + DynamicMessage.Builder builder = DynamicMessage.newBuilder(descriptor); + JsonFormat.parser().merge(input, builder); + return builder.build().toByteArray(); + } + }; + } + + @Override + public Deserializer deserializer(String topic, Target type) { + var descriptor = descriptorFor(topic, type).orElseThrow(); + return new Deserializer() { + @SneakyThrows + @Override + public DeserializeResult deserialize(RecordHeaders headers, byte[] data) { + var protoMsg = DynamicMessage.parseFrom(descriptor, new ByteArrayInputStream(data)); + byte[] jsonFromProto = ProtobufSchemaUtils.toJson(protoMsg); + var result = new String(jsonFromProto); + return new DeserializeResult( + result, + DeserializeResult.Type.JSON, + Map.of() + ); + } + }; + } + + @Override + public Optional getSchema(String topic, Target type) { + return descriptorFor(topic, type).map(this::toSchemaDescription); + } + + private SchemaDescription toSchemaDescription(Descriptor descriptor) { + Path path = descriptorPaths.get(descriptor); + return new SchemaDescription( + SCHEMA_CONVERTER.convert(path.toUri(), descriptor).toJson(), + Map.of("messageName", descriptor.getFullName()) + ); + } + + @SneakyThrows + private static String readFileAsString(Path path) { + return Files.readString(path); + } + + //---------------------------------------------------------------------------------------------------------------- + + @VisibleForTesting + record Configuration(@Nullable Descriptor defaultMessageDescriptor, + @Nullable Descriptor defaultKeyMessageDescriptor, + Map descriptorPaths, + Map messageDescriptorMap, + Map keyMessageDescriptorMap) { + + static boolean canBeAutoConfigured(PropertyResolver kafkaClusterProperties) { + Optional> protobufFiles = kafkaClusterProperties.getListProperty("protobufFiles", String.class); + Optional protobufFilesDir = kafkaClusterProperties.getProperty("protobufFilesDir", String.class); + return protobufFilesDir.isPresent() || protobufFiles.filter(files -> !files.isEmpty()).isPresent(); + } + + static Configuration create(PropertyResolver properties) { + var protobufSchemas = loadSchemas( + properties.getListProperty("protobufFiles", String.class), + properties.getProperty("protobufFilesDir", String.class) + ); + + // Load all referenced message schemas and store their source proto file with the descriptors + Map descriptorPaths = new HashMap<>(); + Optional protobufMessageName = properties.getProperty("protobufMessageName", String.class); + protobufMessageName.ifPresent(messageName -> addProtobufSchema(descriptorPaths, protobufSchemas, messageName)); + + Optional protobufMessageNameForKey = + properties.getProperty("protobufMessageNameForKey", String.class); + protobufMessageNameForKey + .ifPresent(messageName -> addProtobufSchema(descriptorPaths, protobufSchemas, messageName)); + + Optional> protobufMessageNameByTopic = + properties.getMapProperty("protobufMessageNameByTopic", String.class, String.class); + protobufMessageNameByTopic + .ifPresent(messageNamesByTopic -> addProtobufSchemas(descriptorPaths, protobufSchemas, messageNamesByTopic)); + + Optional> protobufMessageNameForKeyByTopic = + properties.getMapProperty("protobufMessageNameForKeyByTopic", String.class, String.class); + protobufMessageNameForKeyByTopic + .ifPresent(messageNamesByTopic -> addProtobufSchemas(descriptorPaths, protobufSchemas, messageNamesByTopic)); + + // Fill dictionary for descriptor lookup by full message name + Map descriptorMap = descriptorPaths.keySet().stream() + .collect(Collectors.toMap(Descriptor::getFullName, Function.identity())); + + return new Configuration( + protobufMessageName.map(descriptorMap::get).orElse(null), + protobufMessageNameForKey.map(descriptorMap::get).orElse(null), + descriptorPaths, + protobufMessageNameByTopic.map(map -> populateDescriptors(descriptorMap, map)).orElse(Map.of()), + protobufMessageNameForKeyByTopic.map(map -> populateDescriptors(descriptorMap, map)).orElse(Map.of()) + ); + } + + private static Map.Entry getDescriptorAndPath(Map protobufSchemas, + String msgName) { + return protobufSchemas.entrySet().stream() + .filter(schema -> schema.getValue().toDescriptor(msgName) != null) + .map(schema -> Map.entry(schema.getValue().toDescriptor(msgName), schema.getKey())) + .findFirst() + .orElseThrow(() -> new NullPointerException( + "The given message type not found in protobuf definition: " + msgName)); + } + + private static Map populateDescriptors(Map descriptorMap, + Map messageNameMap) { + Map descriptors = new HashMap<>(); + for (Map.Entry entry : messageNameMap.entrySet()) { + descriptors.put(entry.getKey(), descriptorMap.get(entry.getValue())); + } + return descriptors; + } + + @VisibleForTesting + static Map loadSchemas(Optional> protobufFiles, + Optional protobufFilesDir) { + if (protobufFilesDir.isPresent()) { + if (protobufFiles.isPresent()) { + log.warn("protobufFiles properties will be ignored, since protobufFilesDir provided"); + } + List loadedFiles = new ProtoSchemaLoader(protobufFilesDir.get()).load(); + Map allPaths = loadedFiles.stream() + .collect(Collectors.toMap(f -> f.getLocation().getPath(), ProtoFile::toElement)); + return loadedFiles.stream() + .collect(Collectors.toMap( + f -> Path.of(f.getLocation().getBase(), f.getLocation().getPath()), + f -> new ProtobufSchema(f.toElement(), List.of(), allPaths))); + } + //Supporting for backward-compatibility. Normally, protobufFilesDir setting should be used + return protobufFiles.stream() + .flatMap(Collection::stream) + .distinct() + .map(Path::of) + .collect(Collectors.toMap(path -> path, path -> new ProtobufSchema(readFileAsString(path)))); + } + + private static void addProtobufSchema(Map descriptorPaths, + Map protobufSchemas, + String messageName) { + var descriptorAndPath = getDescriptorAndPath(protobufSchemas, messageName); + descriptorPaths.put(descriptorAndPath.getKey(), descriptorAndPath.getValue()); + } + + private static void addProtobufSchemas(Map descriptorPaths, + Map protobufSchemas, + Map messageNamesByTopic) { + messageNamesByTopic.values().stream() + .map(msgName -> getDescriptorAndPath(protobufSchemas, msgName)) + .forEach(entry -> descriptorPaths.put(entry.getKey(), entry.getValue())); + } + } + + static class ProtoSchemaLoader { + + private final Path baseLocation; + + ProtoSchemaLoader(String baseLocationStr) { + this.baseLocation = Path.of(baseLocationStr); + if (!Files.isReadable(baseLocation)) { + throw new ValidationException("proto files directory not readable"); + } + } + + List load() { + Map knownTypes = knownProtoFiles(); + + Map filesByLocations = new HashMap<>(); + filesByLocations.putAll(knownTypes); + filesByLocations.putAll(loadFilesWithLocations()); + + Linker linker = new Linker( + createFilesLoader(filesByLocations), + new ErrorCollector(), + true, + true + ); + var schema = linker.link(filesByLocations.values()); + linker.getErrors().throwIfNonEmpty(); + return schema.getProtoFiles() + .stream() + .filter(p -> !knownTypes.containsKey(p.getLocation().getPath())) //filtering known types + .toList(); + } + + private Map knownProtoFiles() { + return Stream.of( + loadKnownProtoFile("google/type/color.proto", ColorProto.getDescriptor()), + loadKnownProtoFile("google/type/date.proto", DateProto.getDescriptor()), + loadKnownProtoFile("google/type/datetime.proto", DateTimeProto.getDescriptor()), + loadKnownProtoFile("google/type/dayofweek.proto", DayOfWeekProto.getDescriptor()), + loadKnownProtoFile("google/type/decimal.proto", com.google.type.DecimalProto.getDescriptor()), + loadKnownProtoFile("google/type/expr.proto", ExprProto.getDescriptor()), + loadKnownProtoFile("google/type/fraction.proto", FractionProto.getDescriptor()), + loadKnownProtoFile("google/type/interval.proto", IntervalProto.getDescriptor()), + loadKnownProtoFile("google/type/latlng.proto", LatLngProto.getDescriptor()), + loadKnownProtoFile("google/type/money.proto", MoneyProto.getDescriptor()), + loadKnownProtoFile("google/type/month.proto", MonthProto.getDescriptor()), + loadKnownProtoFile("google/type/phone_number.proto", PhoneNumberProto.getDescriptor()), + loadKnownProtoFile("google/type/postal_address.proto", PostalAddressProto.getDescriptor()), + loadKnownProtoFile("google/type/quaternion.prot", QuaternionProto.getDescriptor()), + loadKnownProtoFile("google/type/timeofday.proto", TimeOfDayProto.getDescriptor()), + loadKnownProtoFile("google/protobuf/any.proto", AnyProto.getDescriptor()), + loadKnownProtoFile("google/protobuf/api.proto", ApiProto.getDescriptor()), + loadKnownProtoFile("google/protobuf/descriptor.proto", DescriptorProtos.getDescriptor()), + loadKnownProtoFile("google/protobuf/duration.proto", DurationProto.getDescriptor()), + loadKnownProtoFile("google/protobuf/empty.proto", EmptyProto.getDescriptor()), + loadKnownProtoFile("google/protobuf/field_mask.proto", FieldMaskProto.getDescriptor()), + loadKnownProtoFile("google/protobuf/source_context.proto", SourceContextProto.getDescriptor()), + loadKnownProtoFile("google/protobuf/struct.proto", StructProto.getDescriptor()), + loadKnownProtoFile("google/protobuf/timestamp.proto", TimestampProto.getDescriptor()), + loadKnownProtoFile("google/protobuf/type.proto", TypeProto.getDescriptor()), + loadKnownProtoFile("google/protobuf/wrappers.proto", WrappersProto.getDescriptor()) + ).collect(Collectors.toMap(p -> p.getLocation().getPath(), p -> p)); + } + + private ProtoFile loadKnownProtoFile(String path, Descriptors.FileDescriptor fileDescriptor) { + String protoFileString = null; + // know type file contains either message or enum + if (!fileDescriptor.getMessageTypes().isEmpty()) { + protoFileString = new ProtobufSchema(fileDescriptor.getMessageTypes().get(0)).canonicalString(); + } else if (!fileDescriptor.getEnumTypes().isEmpty()) { + protoFileString = new ProtobufSchema(fileDescriptor.getEnumTypes().get(0)).canonicalString(); + } else { + throw new IllegalStateException(); + } + return ProtoFile.Companion.get(ProtoParser.Companion.parse(Location.get(path), protoFileString)); + } + + private Loader createFilesLoader(Map files) { + return new Loader() { + @Override + public @NotNull ProtoFile load(@NotNull String path) { + return Preconditions.checkNotNull(files.get(path), "ProtoFile not found for import '%s'", path); + } + + @Override + public @NotNull Loader withErrors(@NotNull ErrorCollector errorCollector) { + return this; + } + }; + } + + @SneakyThrows + private Map loadFilesWithLocations() { + Map filesByLocations = new HashMap<>(); + try (var files = Files.walk(baseLocation)) { + files.filter(p -> !Files.isDirectory(p) && p.toString().endsWith(".proto")) + .forEach(path -> { + // relative path will be used as "import" statement + String relativePath = baseLocation.relativize(path).toString(); + var protoFileElement = ProtoParser.Companion.parse( + Location.get(baseLocation.toString(), relativePath), + readFileAsString(path) + ); + filesByLocations.put(relativePath, ProtoFile.Companion.get(protoFileElement)); + }); + } + return filesByLocations; + } + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/ProtobufRawSerde.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/ProtobufRawSerde.java new file mode 100644 index 00000000000..221b8b5ea53 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/ProtobufRawSerde.java @@ -0,0 +1,59 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import com.google.protobuf.UnknownFieldSet; +import com.provectus.kafka.ui.exception.ValidationException; +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.RecordHeaders; +import com.provectus.kafka.ui.serde.api.SchemaDescription; +import com.provectus.kafka.ui.serdes.BuiltInSerde; +import java.util.Map; +import java.util.Optional; +import lombok.SneakyThrows; + +public class ProtobufRawSerde implements BuiltInSerde { + + public static String name() { + return "ProtobufDecodeRaw"; + } + + @Override + public Optional getDescription() { + return Optional.empty(); + } + + @Override + public Optional getSchema(String topic, Target type) { + return Optional.empty(); + } + + @Override + public boolean canSerialize(String topic, Target type) { + return false; + } + + @Override + public boolean canDeserialize(String topic, Target type) { + return true; + } + + @Override + public Serializer serializer(String topic, Target type) { + throw new UnsupportedOperationException(); + } + + @Override + public Deserializer deserializer(String topic, Target type) { + return new Deserializer() { + @SneakyThrows + @Override + public DeserializeResult deserialize(RecordHeaders headers, byte[] data) { + try { + UnknownFieldSet unknownFields = UnknownFieldSet.parseFrom(data); + return new DeserializeResult(unknownFields.toString(), DeserializeResult.Type.STRING, Map.of()); + } catch (Exception e) { + throw new ValidationException(e.getMessage()); + } + } + }; + } +} \ No newline at end of file diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/StringSerde.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/StringSerde.java new file mode 100644 index 00000000000..3e3671c29c5 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/StringSerde.java @@ -0,0 +1,64 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.PropertyResolver; +import com.provectus.kafka.ui.serde.api.SchemaDescription; +import com.provectus.kafka.ui.serdes.BuiltInSerde; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Optional; + +public class StringSerde implements BuiltInSerde { + + public static String name() { + return "String"; + } + + private Charset encoding = StandardCharsets.UTF_8; + + @Override + public void configure(PropertyResolver serdeProperties, + PropertyResolver kafkaClusterProperties, + PropertyResolver globalProperties) { + serdeProperties.getProperty("encoding", String.class) + .map(Charset::forName) + .ifPresent(e -> StringSerde.this.encoding = e); + } + + @Override + public Optional getDescription() { + return Optional.empty(); + } + + @Override + public Optional getSchema(String topic, Target type) { + return Optional.empty(); + } + + @Override + public boolean canDeserialize(String topic, Target type) { + return true; + } + + @Override + public boolean canSerialize(String topic, Target type) { + return true; + } + + @Override + public Serializer serializer(String topic, Target type) { + return input -> input.getBytes(encoding); + } + + @Override + public Deserializer deserializer(String topic, Target type) { + return (headers, data) -> + new DeserializeResult( + new String(data, encoding), + DeserializeResult.Type.STRING, + Map.of() + ); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/UInt32Serde.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/UInt32Serde.java new file mode 100644 index 00000000000..64645fa79e4 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/UInt32Serde.java @@ -0,0 +1,63 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import com.google.common.primitives.Ints; +import com.google.common.primitives.UnsignedInteger; +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.SchemaDescription; +import com.provectus.kafka.ui.serdes.BuiltInSerde; +import java.util.Map; +import java.util.Optional; + +public class UInt32Serde implements BuiltInSerde { + + public static String name() { + return "UInt32"; + } + + @Override + public Optional getDescription() { + return Optional.empty(); + } + + @Override + public Optional getSchema(String topic, Target type) { + return Optional.of( + new SchemaDescription( + String.format( + "{ " + + " \"type\" : \"integer\", " + + " \"minimum\" : 0, " + + " \"maximum\" : %s" + + "}", + UnsignedInteger.MAX_VALUE + ), + Map.of() + ) + ); + } + + @Override + public boolean canDeserialize(String topic, Target type) { + return true; + } + + @Override + public boolean canSerialize(String topic, Target type) { + return true; + } + + @Override + public Serializer serializer(String topic, Target type) { + return input -> Ints.toByteArray(Integer.parseUnsignedInt(input)); + } + + @Override + public Deserializer deserializer(String topic, Target type) { + return (headers, data) -> + new DeserializeResult( + UnsignedInteger.fromIntBits(Ints.fromByteArray(data)).toString(), + DeserializeResult.Type.JSON, + Map.of() + ); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/UInt64Serde.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/UInt64Serde.java new file mode 100644 index 00000000000..de7c8a6a638 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/UInt64Serde.java @@ -0,0 +1,64 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import com.google.common.primitives.Longs; +import com.google.common.primitives.UnsignedLong; +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.SchemaDescription; +import com.provectus.kafka.ui.serdes.BuiltInSerde; +import java.util.Map; +import java.util.Optional; + + +public class UInt64Serde implements BuiltInSerde { + + public static String name() { + return "UInt64"; + } + + @Override + public Optional getDescription() { + return Optional.empty(); + } + + @Override + public Optional getSchema(String topic, Target type) { + return Optional.of( + new SchemaDescription( + String.format( + "{ " + + " \"type\" : \"integer\", " + + " \"minimum\" : 0, " + + " \"maximum\" : %s " + + "}", + UnsignedLong.MAX_VALUE + ), + Map.of() + ) + ); + } + + @Override + public boolean canDeserialize(String topic, Target type) { + return true; + } + + @Override + public boolean canSerialize(String topic, Target type) { + return true; + } + + @Override + public Serializer serializer(String topic, Target type) { + return input -> Longs.toByteArray(Long.parseUnsignedLong(input)); + } + + @Override + public Deserializer deserializer(String topic, Target type) { + return (headers, data) -> + new DeserializeResult( + UnsignedLong.fromLongBits(Longs.fromByteArray(data)).toString(), + DeserializeResult.Type.JSON, + Map.of() + ); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/UuidBinarySerde.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/UuidBinarySerde.java new file mode 100644 index 00000000000..0be17e757f2 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/UuidBinarySerde.java @@ -0,0 +1,84 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import com.provectus.kafka.ui.exception.ValidationException; +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.PropertyResolver; +import com.provectus.kafka.ui.serde.api.RecordHeaders; +import com.provectus.kafka.ui.serde.api.SchemaDescription; +import com.provectus.kafka.ui.serdes.BuiltInSerde; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + + +public class UuidBinarySerde implements BuiltInSerde { + + public static String name() { + return "UUIDBinary"; + } + + private boolean mostSignificantBitsFirst = true; + + @Override + public void configure(PropertyResolver serdeProperties, + PropertyResolver kafkaClusterProperties, + PropertyResolver globalProperties) { + serdeProperties.getProperty("mostSignificantBitsFirst", Boolean.class) + .ifPresent(msb -> UuidBinarySerde.this.mostSignificantBitsFirst = msb); + } + + @Override + public Optional getDescription() { + return Optional.empty(); + } + + @Override + public Optional getSchema(String topic, Target type) { + return Optional.empty(); + } + + @Override + public boolean canDeserialize(String topic, Target type) { + return true; + } + + @Override + public boolean canSerialize(String topic, Target type) { + return true; + } + + @Override + public Serializer serializer(String topic, Target type) { + return input -> { + UUID uuid = UUID.fromString(input); + ByteBuffer bb = ByteBuffer.wrap(new byte[16]); + if (mostSignificantBitsFirst) { + bb.putLong(uuid.getMostSignificantBits()); + bb.putLong(uuid.getLeastSignificantBits()); + } else { + bb.putLong(uuid.getLeastSignificantBits()); + bb.putLong(uuid.getMostSignificantBits()); + } + return bb.array(); + }; + } + + @Override + public Deserializer deserializer(String topic, Target type) { + return (headers, data) -> { + if (data.length != 16) { + throw new ValidationException("UUID data should be 16 bytes, but it is " + data.length); + } + ByteBuffer bb = ByteBuffer.wrap(data); + long msb = bb.getLong(); + long lsb = bb.getLong(); + UUID uuid = mostSignificantBitsFirst ? new UUID(msb, lsb) : new UUID(lsb, msb); + return new DeserializeResult( + uuid.toString(), + DeserializeResult.Type.STRING, + Map.of() + ); + }; + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/MessageFormatter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/MessageFormatter.java new file mode 100644 index 00000000000..40073d85347 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/MessageFormatter.java @@ -0,0 +1,85 @@ +package com.provectus.kafka.ui.serdes.builtin.sr; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.protobuf.Message; +import com.google.protobuf.util.JsonFormat; +import com.provectus.kafka.ui.util.jsonschema.JsonAvroConversion; +import io.confluent.kafka.schemaregistry.avro.AvroSchemaUtils; +import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient; +import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig; +import io.confluent.kafka.serializers.KafkaAvroDeserializer; +import io.confluent.kafka.serializers.KafkaAvroDeserializerConfig; +import io.confluent.kafka.serializers.json.KafkaJsonSchemaDeserializer; +import io.confluent.kafka.serializers.protobuf.KafkaProtobufDeserializer; +import java.util.Map; +import lombok.SneakyThrows; + +interface MessageFormatter { + + String format(String topic, byte[] value); + + static Map createMap(SchemaRegistryClient schemaRegistryClient) { + return Map.of( + SchemaType.AVRO, new AvroMessageFormatter(schemaRegistryClient), + SchemaType.JSON, new JsonSchemaMessageFormatter(schemaRegistryClient), + SchemaType.PROTOBUF, new ProtobufMessageFormatter(schemaRegistryClient) + ); + } + + class AvroMessageFormatter implements MessageFormatter { + private final KafkaAvroDeserializer avroDeserializer; + + AvroMessageFormatter(SchemaRegistryClient client) { + this.avroDeserializer = new KafkaAvroDeserializer(client); + this.avroDeserializer.configure( + Map.of( + AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG, "wontbeused", + KafkaAvroDeserializerConfig.SPECIFIC_AVRO_READER_CONFIG, false, + KafkaAvroDeserializerConfig.SCHEMA_REFLECTION_CONFIG, false, + KafkaAvroDeserializerConfig.AVRO_USE_LOGICAL_TYPE_CONVERTERS_CONFIG, true + ), + false + ); + } + + @Override + public String format(String topic, byte[] value) { + Object deserialized = avroDeserializer.deserialize(topic, value); + var schema = AvroSchemaUtils.getSchema(deserialized); + return JsonAvroConversion.convertAvroToJson(deserialized, schema).toString(); + } + } + + class ProtobufMessageFormatter implements MessageFormatter { + private final KafkaProtobufDeserializer protobufDeserializer; + + ProtobufMessageFormatter(SchemaRegistryClient client) { + this.protobufDeserializer = new KafkaProtobufDeserializer<>(client); + } + + @Override + @SneakyThrows + public String format(String topic, byte[] value) { + final Message message = protobufDeserializer.deserialize(topic, value); + return JsonFormat.printer() + .includingDefaultValueFields() + .omittingInsignificantWhitespace() + .preservingProtoFieldNames() + .print(message); + } + } + + class JsonSchemaMessageFormatter implements MessageFormatter { + private final KafkaJsonSchemaDeserializer jsonSchemaDeserializer; + + JsonSchemaMessageFormatter(SchemaRegistryClient client) { + this.jsonSchemaDeserializer = new KafkaJsonSchemaDeserializer<>(client); + } + + @Override + public String format(String topic, byte[] value) { + JsonNode json = jsonSchemaDeserializer.deserialize(topic, value); + return json.toString(); + } + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/SchemaRegistrySerde.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/SchemaRegistrySerde.java new file mode 100644 index 00000000000..4ef0bbe5dd4 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/SchemaRegistrySerde.java @@ -0,0 +1,314 @@ +package com.provectus.kafka.ui.serdes.builtin.sr; + +import static com.provectus.kafka.ui.serdes.builtin.sr.Serialize.serializeAvro; +import static com.provectus.kafka.ui.serdes.builtin.sr.Serialize.serializeJson; +import static com.provectus.kafka.ui.serdes.builtin.sr.Serialize.serializeProto; +import static io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig.BASIC_AUTH_CREDENTIALS_SOURCE; +import static io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig.USER_INFO_CONFIG; + +import com.google.common.annotations.VisibleForTesting; +import com.provectus.kafka.ui.exception.ValidationException; +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.PropertyResolver; +import com.provectus.kafka.ui.serde.api.SchemaDescription; +import com.provectus.kafka.ui.serdes.BuiltInSerde; +import com.provectus.kafka.ui.util.jsonschema.AvroJsonSchemaConverter; +import com.provectus.kafka.ui.util.jsonschema.ProtobufSchemaConverter; +import io.confluent.kafka.schemaregistry.ParsedSchema; +import io.confluent.kafka.schemaregistry.avro.AvroSchema; +import io.confluent.kafka.schemaregistry.avro.AvroSchemaProvider; +import io.confluent.kafka.schemaregistry.client.CachedSchemaRegistryClient; +import io.confluent.kafka.schemaregistry.client.SchemaMetadata; +import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient; +import io.confluent.kafka.schemaregistry.client.SchemaRegistryClientConfig; +import io.confluent.kafka.schemaregistry.client.rest.exceptions.RestClientException; +import io.confluent.kafka.schemaregistry.json.JsonSchema; +import io.confluent.kafka.schemaregistry.json.JsonSchemaProvider; +import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema; +import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchemaProvider; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Callable; +import javax.annotation.Nullable; +import lombok.SneakyThrows; +import org.apache.kafka.common.config.SslConfigs; + + +public class SchemaRegistrySerde implements BuiltInSerde { + + private static final byte SR_PAYLOAD_MAGIC_BYTE = 0x0; + private static final int SR_PAYLOAD_PREFIX_LENGTH = 5; + + public static String name() { + return "SchemaRegistry"; + } + + private static final String SCHEMA_REGISTRY = "schemaRegistry"; + + private SchemaRegistryClient schemaRegistryClient; + private List schemaRegistryUrls; + private String valueSchemaNameTemplate; + private String keySchemaNameTemplate; + private boolean checkSchemaExistenceForDeserialize; + + private Map schemaRegistryFormatters; + + @Override + public boolean canBeAutoConfigured(PropertyResolver kafkaClusterProperties, + PropertyResolver globalProperties) { + return kafkaClusterProperties.getListProperty(SCHEMA_REGISTRY, String.class) + .filter(lst -> !lst.isEmpty()) + .isPresent(); + } + + @Override + public void autoConfigure(PropertyResolver kafkaClusterProperties, + PropertyResolver globalProperties) { + var urls = kafkaClusterProperties.getListProperty(SCHEMA_REGISTRY, String.class) + .filter(lst -> !lst.isEmpty()) + .orElseThrow(() -> new ValidationException("No urls provided for schema registry")); + configure( + urls, + createSchemaRegistryClient( + urls, + kafkaClusterProperties.getProperty("schemaRegistryAuth.username", String.class).orElse(null), + kafkaClusterProperties.getProperty("schemaRegistryAuth.password", String.class).orElse(null), + kafkaClusterProperties.getProperty("schemaRegistrySsl.keystoreLocation", String.class).orElse(null), + kafkaClusterProperties.getProperty("schemaRegistrySsl.keystorePassword", String.class).orElse(null), + kafkaClusterProperties.getProperty("ssl.truststoreLocation", String.class).orElse(null), + kafkaClusterProperties.getProperty("ssl.truststorePassword", String.class).orElse(null) + ), + kafkaClusterProperties.getProperty("schemaRegistryKeySchemaNameTemplate", String.class).orElse("%s-key"), + kafkaClusterProperties.getProperty("schemaRegistrySchemaNameTemplate", String.class).orElse("%s-value"), + kafkaClusterProperties.getProperty("schemaRegistryCheckSchemaExistenceForDeserialize", Boolean.class) + .orElse(false) + ); + } + + @Override + public void configure(PropertyResolver serdeProperties, + PropertyResolver kafkaClusterProperties, + PropertyResolver globalProperties) { + var urls = serdeProperties.getListProperty("url", String.class) + .or(() -> kafkaClusterProperties.getListProperty(SCHEMA_REGISTRY, String.class)) + .filter(lst -> !lst.isEmpty()) + .orElseThrow(() -> new ValidationException("No urls provided for schema registry")); + configure( + urls, + createSchemaRegistryClient( + urls, + serdeProperties.getProperty("username", String.class).orElse(null), + serdeProperties.getProperty("password", String.class).orElse(null), + serdeProperties.getProperty("keystoreLocation", String.class).orElse(null), + serdeProperties.getProperty("keystorePassword", String.class).orElse(null), + kafkaClusterProperties.getProperty("ssl.truststoreLocation", String.class).orElse(null), + kafkaClusterProperties.getProperty("ssl.truststorePassword", String.class).orElse(null) + ), + serdeProperties.getProperty("keySchemaNameTemplate", String.class).orElse("%s-key"), + serdeProperties.getProperty("schemaNameTemplate", String.class).orElse("%s-value"), + serdeProperties.getProperty("checkSchemaExistenceForDeserialize", Boolean.class) + .orElse(false) + ); + } + + @VisibleForTesting + void configure( + List schemaRegistryUrls, + SchemaRegistryClient schemaRegistryClient, + String keySchemaNameTemplate, + String valueSchemaNameTemplate, + boolean checkTopicSchemaExistenceForDeserialize) { + this.schemaRegistryUrls = schemaRegistryUrls; + this.schemaRegistryClient = schemaRegistryClient; + this.keySchemaNameTemplate = keySchemaNameTemplate; + this.valueSchemaNameTemplate = valueSchemaNameTemplate; + this.schemaRegistryFormatters = MessageFormatter.createMap(schemaRegistryClient); + this.checkSchemaExistenceForDeserialize = checkTopicSchemaExistenceForDeserialize; + } + + private static SchemaRegistryClient createSchemaRegistryClient(List urls, + @Nullable String username, + @Nullable String password, + @Nullable String keyStoreLocation, + @Nullable String keyStorePassword, + @Nullable String trustStoreLocation, + @Nullable String trustStorePassword) { + Map configs = new HashMap<>(); + if (username != null && password != null) { + configs.put(BASIC_AUTH_CREDENTIALS_SOURCE, "USER_INFO"); + configs.put(USER_INFO_CONFIG, username + ":" + password); + } else if (username != null) { + throw new ValidationException( + "You specified username but do not specified password"); + } else if (password != null) { + throw new ValidationException( + "You specified password but do not specified username"); + } + + // We require at least a truststore. The logic is done similar to SchemaRegistryService.securedWebClientOnTLS + if (trustStoreLocation != null && trustStorePassword != null) { + configs.put(SchemaRegistryClientConfig.CLIENT_NAMESPACE + SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, + trustStoreLocation); + configs.put(SchemaRegistryClientConfig.CLIENT_NAMESPACE + SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, + trustStorePassword); + } + + if (keyStoreLocation != null && keyStorePassword != null) { + configs.put(SchemaRegistryClientConfig.CLIENT_NAMESPACE + SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, + keyStoreLocation); + configs.put(SchemaRegistryClientConfig.CLIENT_NAMESPACE + SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, + keyStorePassword); + configs.put(SchemaRegistryClientConfig.CLIENT_NAMESPACE + SslConfigs.SSL_KEY_PASSWORD_CONFIG, + keyStorePassword); + } + + return new CachedSchemaRegistryClient( + urls, + 1_000, + List.of(new AvroSchemaProvider(), new ProtobufSchemaProvider(), new JsonSchemaProvider()), + configs + ); + } + + @Override + public Optional getDescription() { + return Optional.empty(); + } + + @Override + public boolean canDeserialize(String topic, Target type) { + String subject = schemaSubject(topic, type); + return !checkSchemaExistenceForDeserialize + || getSchemaBySubject(subject).isPresent(); + } + + @Override + public boolean canSerialize(String topic, Target type) { + String subject = schemaSubject(topic, type); + return getSchemaBySubject(subject).isPresent(); + } + + @Override + public Optional getSchema(String topic, Target type) { + String subject = schemaSubject(topic, type); + return getSchemaBySubject(subject) + .flatMap(schemaMetadata -> + //schema can be not-found, when schema contexts configured improperly + getSchemaById(schemaMetadata.getId()) + .map(parsedSchema -> + new SchemaDescription( + convertSchema(schemaMetadata, parsedSchema), + Map.of( + "subject", subject, + "schemaId", schemaMetadata.getId(), + "latestVersion", schemaMetadata.getVersion(), + "type", schemaMetadata.getSchemaType() // AVRO / PROTOBUF / JSON + ) + ))); + } + + @SneakyThrows + private String convertSchema(SchemaMetadata schema, ParsedSchema parsedSchema) { + URI basePath = new URI(schemaRegistryUrls.get(0)) + .resolve(Integer.toString(schema.getId())); + SchemaType schemaType = SchemaType.fromString(schema.getSchemaType()) + .orElseThrow(() -> new IllegalStateException("Unknown schema type: " + schema.getSchemaType())); + return switch (schemaType) { + case PROTOBUF -> new ProtobufSchemaConverter() + .convert(basePath, ((ProtobufSchema) parsedSchema).toDescriptor()) + .toJson(); + case AVRO -> new AvroJsonSchemaConverter() + .convert(basePath, ((AvroSchema) parsedSchema).rawSchema()) + .toJson(); + case JSON -> + //need to use confluent JsonSchema since it includes resolved references + ((JsonSchema) parsedSchema).rawSchema().toString(); + }; + } + + private Optional getSchemaById(int id) { + return wrapWith404Handler(() -> schemaRegistryClient.getSchemaById(id)); + } + + private Optional getSchemaBySubject(String subject) { + return wrapWith404Handler(() -> schemaRegistryClient.getLatestSchemaMetadata(subject)); + } + + @SneakyThrows + private Optional wrapWith404Handler(Callable call) { + try { + return Optional.ofNullable(call.call()); + } catch (RestClientException restClientException) { + if (restClientException.getStatus() == 404) { + return Optional.empty(); + } else { + throw new RuntimeException("Error calling SchemaRegistryClient", restClientException); + } + } + } + + private String schemaSubject(String topic, Target type) { + return String.format(type == Target.KEY ? keySchemaNameTemplate : valueSchemaNameTemplate, topic); + } + + @Override + public Serializer serializer(String topic, Target type) { + String subject = schemaSubject(topic, type); + SchemaMetadata meta = getSchemaBySubject(subject) + .orElseThrow(() -> new ValidationException( + String.format("No schema for subject '%s' found", subject))); + ParsedSchema schema = getSchemaById(meta.getId()) + .orElseThrow(() -> new IllegalStateException( + String.format("Schema found for id %s, subject '%s'", meta.getId(), subject))); + SchemaType schemaType = SchemaType.fromString(meta.getSchemaType()) + .orElseThrow(() -> new IllegalStateException("Unknown schema type: " + meta.getSchemaType())); + return switch (schemaType) { + case PROTOBUF -> input -> + serializeProto(schemaRegistryClient, topic, type, (ProtobufSchema) schema, meta.getId(), input); + case AVRO -> input -> + serializeAvro((AvroSchema) schema, meta.getId(), input); + case JSON -> input -> + serializeJson((JsonSchema) schema, meta.getId(), input); + }; + } + + @Override + public Deserializer deserializer(String topic, Target type) { + return (headers, data) -> { + var schemaId = extractSchemaIdFromMsg(data); + SchemaType format = getMessageFormatBySchemaId(schemaId); + MessageFormatter formatter = schemaRegistryFormatters.get(format); + return new DeserializeResult( + formatter.format(topic, data), + DeserializeResult.Type.JSON, + Map.of( + "schemaId", schemaId, + "type", format.name() + ) + ); + }; + } + + private SchemaType getMessageFormatBySchemaId(int schemaId) { + return getSchemaById(schemaId) + .map(ParsedSchema::schemaType) + .flatMap(SchemaType::fromString) + .orElseThrow(() -> new ValidationException(String.format("Schema for id '%d' not found ", schemaId))); + } + + private int extractSchemaIdFromMsg(byte[] data) { + ByteBuffer buffer = ByteBuffer.wrap(data); + if (buffer.remaining() >= SR_PAYLOAD_PREFIX_LENGTH && buffer.get() == SR_PAYLOAD_MAGIC_BYTE) { + return buffer.getInt(); + } + throw new ValidationException( + String.format( + "Data doesn't contain magic byte and schema id prefix, so it can't be deserialized with %s serde", + name()) + ); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/SchemaType.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/SchemaType.java new file mode 100644 index 00000000000..dc38d3ae483 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/SchemaType.java @@ -0,0 +1,14 @@ +package com.provectus.kafka.ui.serdes.builtin.sr; + +import java.util.Optional; +import org.apache.commons.lang3.EnumUtils; + +enum SchemaType { + AVRO, + JSON, + PROTOBUF; + + public static Optional fromString(String typeString) { + return Optional.ofNullable(EnumUtils.getEnum(SchemaType.class, typeString)); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/Serialize.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/Serialize.java new file mode 100644 index 00000000000..8381382576b --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/Serialize.java @@ -0,0 +1,126 @@ +package com.provectus.kafka.ui.serdes.builtin.sr; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Preconditions; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.Message; +import com.google.protobuf.util.JsonFormat; +import com.provectus.kafka.ui.exception.ValidationException; +import com.provectus.kafka.ui.serde.api.Serde; +import com.provectus.kafka.ui.util.annotation.KafkaClientInternalsDependant; +import com.provectus.kafka.ui.util.jsonschema.JsonAvroConversion; +import io.confluent.kafka.schemaregistry.avro.AvroSchema; +import io.confluent.kafka.schemaregistry.avro.AvroSchemaUtils; +import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient; +import io.confluent.kafka.schemaregistry.json.JsonSchema; +import io.confluent.kafka.schemaregistry.json.jackson.Jackson; +import io.confluent.kafka.schemaregistry.protobuf.MessageIndexes; +import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema; +import io.confluent.kafka.serializers.protobuf.AbstractKafkaProtobufSerializer; +import io.confluent.kafka.serializers.subject.DefaultReferenceSubjectNameStrategy; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.HashMap; +import lombok.SneakyThrows; +import org.apache.avro.Schema; +import org.apache.avro.io.BinaryEncoder; +import org.apache.avro.io.DatumWriter; +import org.apache.avro.io.EncoderFactory; + +final class Serialize { + + private static final byte MAGIC = 0x0; + private static final ObjectMapper JSON_SERIALIZE_MAPPER = Jackson.newObjectMapper(); //from confluent package + + private Serialize() { + } + + @KafkaClientInternalsDependant("AbstractKafkaJsonSchemaSerializer::serializeImpl") + @SneakyThrows + static byte[] serializeJson(JsonSchema schema, int schemaId, String value) { + JsonNode json; + try { + json = JSON_SERIALIZE_MAPPER.readTree(value); + } catch (JsonProcessingException e) { + throw new ValidationException(String.format("'%s' is not valid json", value)); + } + try { + schema.validate(json); + } catch (org.everit.json.schema.ValidationException e) { + throw new ValidationException( + String.format("'%s' does not fit schema: %s", value, e.getAllMessages())); + } + try (var out = new ByteArrayOutputStream()) { + out.write(MAGIC); + out.write(schemaId(schemaId)); + out.write(JSON_SERIALIZE_MAPPER.writeValueAsBytes(json)); + return out.toByteArray(); + } + } + + @KafkaClientInternalsDependant("AbstractKafkaProtobufSerializer::serializeImpl") + @SneakyThrows + static byte[] serializeProto(SchemaRegistryClient srClient, + String topic, + Serde.Target target, + ProtobufSchema schema, + int schemaId, + String input) { + // flags are tuned like in ProtobufSerializer by default + boolean normalizeSchema = false; + boolean autoRegisterSchema = false; + boolean useLatestVersion = true; + boolean latestCompatStrict = true; + boolean skipKnownTypes = true; + + schema = AbstractKafkaProtobufSerializer.resolveDependencies( + srClient, normalizeSchema, autoRegisterSchema, useLatestVersion, latestCompatStrict, + new HashMap<>(), skipKnownTypes, new DefaultReferenceSubjectNameStrategy(), + topic, target == Serde.Target.KEY, schema + ); + + DynamicMessage.Builder builder = schema.newMessageBuilder(); + JsonFormat.parser().merge(input, builder); + Message message = builder.build(); + MessageIndexes indexes = schema.toMessageIndexes(message.getDescriptorForType().getFullName(), normalizeSchema); + try (var out = new ByteArrayOutputStream()) { + out.write(MAGIC); + out.write(schemaId(schemaId)); + out.write(indexes.toByteArray()); + message.writeTo(out); + return out.toByteArray(); + } + } + + @KafkaClientInternalsDependant("AbstractKafkaAvroSerializer::serializeImpl") + @SneakyThrows + static byte[] serializeAvro(AvroSchema schema, int schemaId, String input) { + var avroObject = JsonAvroConversion.convertJsonToAvro(input, schema.rawSchema()); + try (var out = new ByteArrayOutputStream()) { + out.write(MAGIC); + out.write(schemaId(schemaId)); + Schema rawSchema = schema.rawSchema(); + if (rawSchema.getType().equals(Schema.Type.BYTES)) { + Preconditions.checkState( + avroObject instanceof ByteBuffer, + "Unrecognized bytes object of type: " + avroObject.getClass().getName() + ); + out.write(((ByteBuffer) avroObject).array()); + } else { + boolean useLogicalTypeConverters = true; + BinaryEncoder encoder = EncoderFactory.get().directBinaryEncoder(out, null); + DatumWriter writer = + (DatumWriter) AvroSchemaUtils.getDatumWriter(avroObject, rawSchema, useLogicalTypeConverters); + writer.write(avroObject, encoder); + encoder.flush(); + } + return out.toByteArray(); + } + } + + private static byte[] schemaId(int id) { + return ByteBuffer.allocate(Integer.BYTES).putInt(id).array(); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/AdminClientServiceImpl.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/AdminClientServiceImpl.java index d8c96f8cbcc..1bd4d7e33e8 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/AdminClientServiceImpl.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/AdminClientServiceImpl.java @@ -1,28 +1,36 @@ package com.provectus.kafka.ui.service; +import com.provectus.kafka.ui.config.ClustersProperties; import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.util.SslPropertiesUtil; import java.io.Closeable; +import java.time.Instant; import java.util.Map; +import java.util.Optional; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; -import lombok.RequiredArgsConstructor; -import lombok.Setter; +import java.util.concurrent.atomic.AtomicLong; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.admin.AdminClient; import org.apache.kafka.clients.admin.AdminClientConfig; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; import reactor.core.publisher.Mono; @Service -@RequiredArgsConstructor @Slf4j public class AdminClientServiceImpl implements AdminClientService, Closeable { + + private static final int DEFAULT_CLIENT_TIMEOUT_MS = 30_000; + + private static final AtomicLong CLIENT_ID_SEQ = new AtomicLong(); + private final Map adminClientCache = new ConcurrentHashMap<>(); - @Setter // used in tests - @Value("${kafka.admin-client-timeout:30000}") - private int clientTimeout; + private final int clientTimeout; + + public AdminClientServiceImpl(ClustersProperties clustersProperties) { + this.clientTimeout = Optional.ofNullable(clustersProperties.getAdminClientTimeout()) + .orElse(DEFAULT_CLIENT_TIMEOUT_MS); + } @Override public Mono get(KafkaCluster cluster) { @@ -34,13 +42,16 @@ public Mono get(KafkaCluster cluster) { private Mono createAdminClient(KafkaCluster cluster) { return Mono.fromSupplier(() -> { Properties properties = new Properties(); + SslPropertiesUtil.addKafkaSslProperties(cluster.getOriginalProperties().getSsl(), properties); properties.putAll(cluster.getProperties()); - properties - .put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers()); - properties.put(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, clientTimeout); + properties.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers()); + properties.putIfAbsent(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, clientTimeout); + properties.putIfAbsent( + AdminClientConfig.CLIENT_ID_CONFIG, + "kafka-ui-admin-" + Instant.now().getEpochSecond() + "-" + CLIENT_ID_SEQ.incrementAndGet() + ); return AdminClient.create(properties); - }) - .flatMap(ReactiveAdminClient::create) + }).flatMap(ac -> ReactiveAdminClient.create(ac).doOnError(th -> ac.close())) .onErrorMap(th -> new IllegalStateException( "Error while creating AdminClient for Cluster " + cluster.getName(), th)); } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ApplicationInfoService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ApplicationInfoService.java new file mode 100644 index 00000000000..750a7179fb8 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ApplicationInfoService.java @@ -0,0 +1,76 @@ +package com.provectus.kafka.ui.service; + +import static com.provectus.kafka.ui.model.ApplicationInfoDTO.EnabledFeaturesEnum; + +import com.provectus.kafka.ui.model.ApplicationInfoBuildDTO; +import com.provectus.kafka.ui.model.ApplicationInfoDTO; +import com.provectus.kafka.ui.model.ApplicationInfoLatestReleaseDTO; +import com.provectus.kafka.ui.util.DynamicConfigOperations; +import com.provectus.kafka.ui.util.GithubReleaseInfo; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Properties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.info.BuildProperties; +import org.springframework.boot.info.GitProperties; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +@Service +public class ApplicationInfoService { + + private final GithubReleaseInfo githubReleaseInfo = new GithubReleaseInfo(); + + private final DynamicConfigOperations dynamicConfigOperations; + private final BuildProperties buildProperties; + private final GitProperties gitProperties; + + public ApplicationInfoService(DynamicConfigOperations dynamicConfigOperations, + @Autowired(required = false) BuildProperties buildProperties, + @Autowired(required = false) GitProperties gitProperties) { + this.dynamicConfigOperations = dynamicConfigOperations; + this.buildProperties = Optional.ofNullable(buildProperties).orElse(new BuildProperties(new Properties())); + this.gitProperties = Optional.ofNullable(gitProperties).orElse(new GitProperties(new Properties())); + } + + public ApplicationInfoDTO getApplicationInfo() { + var releaseInfo = githubReleaseInfo.get(); + return new ApplicationInfoDTO() + .build(getBuildInfo(releaseInfo)) + .enabledFeatures(getEnabledFeatures()) + .latestRelease(convert(releaseInfo)); + } + + private ApplicationInfoLatestReleaseDTO convert(GithubReleaseInfo.GithubReleaseDto releaseInfo) { + return new ApplicationInfoLatestReleaseDTO() + .htmlUrl(releaseInfo.html_url()) + .publishedAt(releaseInfo.published_at()) + .versionTag(releaseInfo.tag_name()); + } + + private ApplicationInfoBuildDTO getBuildInfo(GithubReleaseInfo.GithubReleaseDto release) { + return new ApplicationInfoBuildDTO() + .isLatestRelease(release.tag_name() != null && release.tag_name().equals(buildProperties.getVersion())) + .commitId(gitProperties.getShortCommitId()) + .version(buildProperties.getVersion()) + .buildTime(buildProperties.getTime() != null + ? DateTimeFormatter.ISO_INSTANT.format(buildProperties.getTime()) : null); + } + + private List getEnabledFeatures() { + var enabledFeatures = new ArrayList(); + if (dynamicConfigOperations.dynamicConfigEnabled()) { + enabledFeatures.add(EnabledFeaturesEnum.DYNAMIC_CONFIG); + } + return enabledFeatures; + } + + // updating on startup and every hour + @Scheduled(fixedRateString = "${github-release-info-update-rate:3600000}") + public void updateGithubReleaseInfo() { + githubReleaseInfo.refresh().block(); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/BrokerService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/BrokerService.java index 28125874bb6..ff86d2be0f7 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/BrokerService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/BrokerService.java @@ -5,17 +5,19 @@ import com.provectus.kafka.ui.exception.NotFoundException; import com.provectus.kafka.ui.exception.TopicOrPartitionNotFoundException; import com.provectus.kafka.ui.mapper.DescribeLogDirsMapper; -import com.provectus.kafka.ui.model.BrokerDTO; import com.provectus.kafka.ui.model.BrokerLogdirUpdateDTO; import com.provectus.kafka.ui.model.BrokersLogdirsDTO; +import com.provectus.kafka.ui.model.InternalBroker; import com.provectus.kafka.ui.model.InternalBrokerConfig; -import com.provectus.kafka.ui.model.JmxBrokerMetrics; import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.PartitionDistributionStats; +import com.provectus.kafka.ui.service.metrics.RawMetric; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import javax.annotation.Nullable; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.admin.ConfigEntry; @@ -35,7 +37,7 @@ @Slf4j public class BrokerService { - private final MetricsCache metricsCache; + private final StatisticsCache statisticsCache; private final AdminClientService adminClientService; private final DescribeLogDirsMapper describeLogDirsMapper; @@ -47,14 +49,11 @@ private Mono>> loadBrokersConfig( private Mono> loadBrokersConfig( KafkaCluster cluster, Integer brokerId) { return loadBrokersConfig(cluster, Collections.singletonList(brokerId)) - .map(map -> map.values().stream() - .findFirst() - .orElseThrow(() -> new NotFoundException( - String.format("Config for broker %s not found", brokerId)))); + .map(map -> map.values().stream().findFirst().orElse(List.of())); } private Flux getBrokersConfig(KafkaCluster cluster, Integer brokerId) { - if (metricsCache.get(cluster).getClusterDescription().getNodes() + if (statisticsCache.get(cluster).getClusterDescription().getNodes() .stream().noneMatch(node -> node.id() == brokerId)) { return Flux.error( new NotFoundException(String.format("Broker with id %s not found", brokerId))); @@ -66,28 +65,18 @@ private Flux getBrokersConfig(KafkaCluster cluster, Intege .flatMapMany(Flux::fromIterable); } - public Flux getBrokers(KafkaCluster cluster) { + public Flux getBrokers(KafkaCluster cluster) { + var stats = statisticsCache.get(cluster); + var partitionsDistribution = PartitionDistributionStats.create(stats); return adminClientService .get(cluster) .flatMap(ReactiveAdminClient::describeCluster) .map(description -> description.getNodes().stream() - .map(node -> { - BrokerDTO broker = new BrokerDTO(); - broker.setId(node.id()); - broker.setHost(node.host()); - broker.setPort(node.port()); - return broker; - }).collect(Collectors.toList())) + .map(node -> new InternalBroker(node, partitionsDistribution, stats)) + .collect(Collectors.toList())) .flatMapMany(Flux::fromIterable); } - public Mono getController(KafkaCluster cluster) { - return adminClientService - .get(cluster) - .flatMap(ReactiveAdminClient::describeCluster) - .map(ReactiveAdminClient.ClusterDescription::getController); - } - public Mono updateBrokerLogDir(KafkaCluster cluster, Integer broker, BrokerLogdirUpdateDTO brokerLogDir) { @@ -125,11 +114,11 @@ private Mono>> getC KafkaCluster cluster, List reqBrokers) { return adminClientService.get(cluster) .flatMap(admin -> { - List brokers = metricsCache.get(cluster).getClusterDescription().getNodes() + List brokers = statisticsCache.get(cluster).getClusterDescription().getNodes() .stream() .map(Node::id) .collect(Collectors.toList()); - if (reqBrokers != null && !reqBrokers.isEmpty()) { + if (!reqBrokers.isEmpty()) { brokers.retainAll(reqBrokers); } return admin.describeLogDirs(brokers); @@ -150,9 +139,8 @@ public Flux getBrokerConfig(KafkaCluster cluster, Integer return getBrokersConfig(cluster, brokerId); } - public Mono getBrokerMetrics(KafkaCluster cluster, Integer brokerId) { - return Mono.justOrEmpty( - metricsCache.get(cluster).getJmxMetrics().getInternalBrokerMetrics().get(brokerId)); + public Mono> getBrokerMetrics(KafkaCluster cluster, Integer brokerId) { + return Mono.justOrEmpty(statisticsCache.get(cluster).getMetrics().getPerBrokerMetrics().get(brokerId)); } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ClusterService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ClusterService.java index d38af2a18ce..18071400b5a 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ClusterService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ClusterService.java @@ -18,33 +18,34 @@ @Slf4j public class ClusterService { - private final MetricsCache metricsCache; + private final StatisticsCache statisticsCache; private final ClustersStorage clustersStorage; private final ClusterMapper clusterMapper; - private final MetricsService metricsService; + private final StatisticsService statisticsService; public List getClusters() { return clustersStorage.getKafkaClusters() .stream() - .map(c -> clusterMapper.toCluster(new InternalClusterState(c, metricsCache.get(c)))) + .map(c -> clusterMapper.toCluster(new InternalClusterState(c, statisticsCache.get(c)))) .collect(Collectors.toList()); } public Mono getClusterStats(KafkaCluster cluster) { return Mono.justOrEmpty( clusterMapper.toClusterStats( - new InternalClusterState(cluster, metricsCache.get(cluster))) + new InternalClusterState(cluster, statisticsCache.get(cluster))) ); } public Mono getClusterMetrics(KafkaCluster cluster) { + return Mono.just( clusterMapper.toClusterMetrics( - metricsCache.get(cluster).getJmxMetrics())); + statisticsCache.get(cluster).getMetrics())); } public Mono updateCluster(KafkaCluster cluster) { - return metricsService.updateCache(cluster) + return statisticsService.updateCache(cluster) .map(metrics -> clusterMapper.toCluster(new InternalClusterState(cluster, metrics))); } } \ No newline at end of file diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ClustersMetricsScheduler.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ClustersMetricsScheduler.java deleted file mode 100644 index 53fff7060b1..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ClustersMetricsScheduler.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.provectus.kafka.ui.service; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Flux; -import reactor.core.scheduler.Schedulers; - -@Component -@RequiredArgsConstructor -@Slf4j -public class ClustersMetricsScheduler { - - private final ClustersStorage clustersStorage; - - private final MetricsService metricsService; - - @Scheduled(fixedRateString = "${kafka.update-metrics-rate-millis:30000}") - public void updateMetrics() { - Flux.fromIterable(clustersStorage.getKafkaClusters()) - .parallel() - .runOn(Schedulers.parallel()) - .flatMap(cluster -> { - log.debug("Start getting metrics for kafkaCluster: {}", cluster.getName()); - return metricsService.updateCache(cluster) - .doOnSuccess(m -> log.debug("Metrics updated for cluster: {}", cluster.getName())); - }) - .then() - .block(); - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ClustersStatisticsScheduler.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ClustersStatisticsScheduler.java new file mode 100644 index 00000000000..f7ac1239a04 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ClustersStatisticsScheduler.java @@ -0,0 +1,32 @@ +package com.provectus.kafka.ui.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.scheduler.Schedulers; + +@Component +@RequiredArgsConstructor +@Slf4j +public class ClustersStatisticsScheduler { + + private final ClustersStorage clustersStorage; + + private final StatisticsService statisticsService; + + @Scheduled(fixedRateString = "${kafka.update-metrics-rate-millis:30000}") + public void updateStatistics() { + Flux.fromIterable(clustersStorage.getKafkaClusters()) + .parallel() + .runOn(Schedulers.parallel()) + .flatMap(cluster -> { + log.debug("Start getting metrics for kafkaCluster: {}", cluster.getName()); + return statisticsService.updateCache(cluster) + .doOnSuccess(m -> log.debug("Metrics updated for cluster: {}", cluster.getName())); + }) + .then() + .block(); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ClustersStorage.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ClustersStorage.java index 08b71b4d318..ee08d6392d8 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ClustersStorage.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ClustersStorage.java @@ -2,7 +2,6 @@ import com.google.common.collect.ImmutableMap; import com.provectus.kafka.ui.config.ClustersProperties; -import com.provectus.kafka.ui.mapper.ClusterMapper; import com.provectus.kafka.ui.model.KafkaCluster; import java.util.Collection; import java.util.Optional; @@ -13,9 +12,9 @@ public class ClustersStorage { private final ImmutableMap kafkaClusters; - public ClustersStorage(ClustersProperties properties, ClusterMapper mapper) { + public ClustersStorage(ClustersProperties properties, KafkaClusterFactory factory) { var builder = ImmutableMap.builder(); - properties.getClusters().forEach(c -> builder.put(c.getName(), mapper.toKafkaCluster(c))); + properties.getClusters().forEach(c -> builder.put(c.getName(), factory.create(properties, c))); this.kafkaClusters = builder.build(); } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ConsumerGroupService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ConsumerGroupService.java index beb9f849798..9764664d6a2 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ConsumerGroupService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ConsumerGroupService.java @@ -1,60 +1,58 @@ package com.provectus.kafka.ui.service; +import com.google.common.collect.Streams; +import com.google.common.collect.Table; +import com.provectus.kafka.ui.emitter.EnhancedConsumer; import com.provectus.kafka.ui.model.ConsumerGroupOrderingDTO; import com.provectus.kafka.ui.model.InternalConsumerGroup; +import com.provectus.kafka.ui.model.InternalTopicConsumerGroup; import com.provectus.kafka.ui.model.KafkaCluster; import com.provectus.kafka.ui.model.SortOrderDTO; +import com.provectus.kafka.ui.service.rbac.AccessControlService; +import com.provectus.kafka.ui.util.ApplicationMetrics; +import com.provectus.kafka.ui.util.SslPropertiesUtil; import java.util.ArrayList; +import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.UUID; import java.util.function.ToIntFunction; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nullable; import lombok.RequiredArgsConstructor; -import lombok.Value; import org.apache.commons.lang3.StringUtils; import org.apache.kafka.clients.admin.ConsumerGroupDescription; +import org.apache.kafka.clients.admin.ConsumerGroupListing; import org.apache.kafka.clients.admin.OffsetSpec; import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.common.ConsumerGroupState; import org.apache.kafka.common.TopicPartition; -import org.apache.kafka.common.serialization.BytesDeserializer; -import org.apache.kafka.common.utils.Bytes; import org.springframework.stereotype.Service; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - @Service @RequiredArgsConstructor public class ConsumerGroupService { private final AdminClientService adminClientService; + private final AccessControlService accessControlService; private Mono> getConsumerGroups( ReactiveAdminClient ac, List descriptions) { - return Flux.fromIterable(descriptions) - // 1. getting committed offsets for all groups - .flatMap(desc -> ac.listConsumerGroupOffsets(desc.groupId()) - .map(offsets -> Tuples.of(desc, offsets))) - .collectMap(Tuple2::getT1, Tuple2::getT2) - .flatMap((Map> groupOffsetsMap) -> { - var tpsFromGroupOffsets = groupOffsetsMap.values().stream() - .flatMap(v -> v.keySet().stream()) - .collect(Collectors.toSet()); - // 2. getting end offsets for partitions with in committed offsets - return ac.listOffsets(tpsFromGroupOffsets, OffsetSpec.latest()) + var groupNames = descriptions.stream().map(ConsumerGroupDescription::groupId).toList(); + // 1. getting committed offsets for all groups + return ac.listConsumerGroupOffsets(groupNames, null) + .flatMap((Table committedOffsets) -> { + // 2. getting end offsets for partitions with committed offsets + return ac.listOffsets(committedOffsets.columnKeySet(), OffsetSpec.latest(), false) .map(endOffsets -> descriptions.stream() .map(desc -> { - var groupOffsets = groupOffsetsMap.get(desc); + var groupOffsets = committedOffsets.row(desc.groupId()); var endOffsetsForGroup = new HashMap<>(endOffsets); endOffsetsForGroup.keySet().retainAll(groupOffsets.keySet()); // 3. gathering description & offsets @@ -64,120 +62,173 @@ private Mono> getConsumerGroups( }); } - @Deprecated // need to migrate to pagination - public Mono> getAllConsumerGroups(KafkaCluster cluster) { - return adminClientService.get(cluster) - .flatMap(ac -> describeConsumerGroups(ac, null) - .flatMap(descriptions -> getConsumerGroups(ac, descriptions))); - } - - public Mono> getConsumerGroupsForTopic(KafkaCluster cluster, - String topic) { + public Mono> getConsumerGroupsForTopic(KafkaCluster cluster, + String topic) { return adminClientService.get(cluster) // 1. getting topic's end offsets - .flatMap(ac -> ac.listOffsets(topic, OffsetSpec.latest()) + .flatMap(ac -> ac.listTopicOffsets(topic, OffsetSpec.latest(), false) .flatMap(endOffsets -> { var tps = new ArrayList<>(endOffsets.keySet()); // 2. getting all consumer groups - return ac.listConsumerGroups() - .flatMap((List groups) -> - Flux.fromIterable(groups) - // 3. for each group trying to find committed offsets for topic - .flatMap(g -> - ac.listConsumerGroupOffsets(g, tps) - .map(offsets -> Tuples.of(g, offsets))) - .filter(t -> !t.getT2().isEmpty()) - .collectMap(Tuple2::getT1, Tuple2::getT2) - ) - .flatMap((Map> groupOffsets) -> - // 4. getting description for groups with non-emtpy offsets - ac.describeConsumerGroups(new ArrayList<>(groupOffsets.keySet())) - .map((Map descriptions) -> - descriptions.values().stream().map(desc -> - // 5. gathering and filter non-target-topic data - InternalConsumerGroup.create( - desc, groupOffsets.get(desc.groupId()), endOffsets) - .retainDataForPartitions(p -> p.topic().equals(topic)) - ) - .collect(Collectors.toList()))); + return describeConsumerGroups(ac) + .flatMap((List groups) -> { + // 3. trying to find committed offsets for topic + var groupNames = groups.stream().map(ConsumerGroupDescription::groupId).toList(); + return ac.listConsumerGroupOffsets(groupNames, tps).map(offsets -> + groups.stream() + // 4. keeping only groups that relates to topic + .filter(g -> isConsumerGroupRelatesToTopic(topic, g, offsets.containsRow(g.groupId()))) + .map(g -> + // 5. constructing results + InternalTopicConsumerGroup.create(topic, g, offsets.row(g.groupId()), endOffsets)) + .toList() + ); + } + ); })); } - @Value - public static class ConsumerGroupsPage { - List consumerGroups; - int totalPages; + private boolean isConsumerGroupRelatesToTopic(String topic, + ConsumerGroupDescription description, + boolean hasCommittedOffsets) { + boolean hasActiveMembersForTopic = description.members() + .stream() + .anyMatch(m -> m.assignment().topicPartitions().stream().anyMatch(tp -> tp.topic().equals(topic))); + return hasActiveMembersForTopic || hasCommittedOffsets; + } + + public record ConsumerGroupsPage(List consumerGroups, int totalPages) { + } + + private record GroupWithDescr(InternalConsumerGroup icg, ConsumerGroupDescription cgd) { } public Mono getConsumerGroupsPage( KafkaCluster cluster, - int page, + int pageNum, int perPage, @Nullable String search, ConsumerGroupOrderingDTO orderBy, - SortOrderDTO sortOrderDto - ) { - var comparator = sortOrderDto.equals(SortOrderDTO.ASC) - ? getPaginationComparator(orderBy) - : getPaginationComparator(orderBy).reversed(); + SortOrderDTO sortOrderDto) { return adminClientService.get(cluster).flatMap(ac -> - describeConsumerGroups(ac, search).flatMap(descriptions -> - getConsumerGroups( - ac, - descriptions.stream() - .sorted(comparator) - .skip((long) (page - 1) * perPage) - .limit(perPage) - .collect(Collectors.toList()) - ).map(cgs -> new ConsumerGroupsPage( - cgs, - (descriptions.size() / perPage) + (descriptions.size() % perPage == 0 ? 0 : 1)))) - ); + ac.listConsumerGroups() + .map(listing -> search == null + ? listing + : listing.stream() + .filter(g -> StringUtils.containsIgnoreCase(g.groupId(), search)) + .toList() + ) + .flatMapIterable(lst -> lst) + .filterWhen(cg -> accessControlService.isConsumerGroupAccessible(cg.groupId(), cluster.getName())) + .collectList() + .flatMap(allGroups -> + loadSortedDescriptions(ac, allGroups, pageNum, perPage, orderBy, sortOrderDto) + .flatMap(descriptions -> getConsumerGroups(ac, descriptions) + .map(page -> new ConsumerGroupsPage( + page, + (allGroups.size() / perPage) + (allGroups.size() % perPage == 0 ? 0 : 1)))))); } - private Comparator getPaginationComparator(ConsumerGroupOrderingDTO - orderBy) { - switch (orderBy) { - case NAME: - return Comparator.comparing(ConsumerGroupDescription::groupId); - case STATE: - ToIntFunction statesPriorities = cg -> { - switch (cg.state()) { - case STABLE: - return 0; - case COMPLETING_REBALANCE: - return 1; - case PREPARING_REBALANCE: - return 2; - case EMPTY: - return 3; - case DEAD: - return 4; - case UNKNOWN: - return 5; - default: - return 100; - } - }; - return Comparator.comparingInt(statesPriorities); - case MEMBERS: - return Comparator.comparingInt(cg -> -cg.members().size()); - default: - throw new IllegalStateException("Unsupported order by: " + orderBy); - } + private Mono> loadSortedDescriptions(ReactiveAdminClient ac, + List groups, + int pageNum, + int perPage, + ConsumerGroupOrderingDTO orderBy, + SortOrderDTO sortOrderDto) { + return switch (orderBy) { + case NAME -> { + Comparator comparator = Comparator.comparing(ConsumerGroupListing::groupId); + yield loadDescriptionsByListings(ac, groups, comparator, pageNum, perPage, sortOrderDto); + } + case STATE -> { + ToIntFunction statesPriorities = + cg -> switch (cg.state().orElse(ConsumerGroupState.UNKNOWN)) { + case STABLE -> 0; + case COMPLETING_REBALANCE -> 1; + case PREPARING_REBALANCE -> 2; + case EMPTY -> 3; + case DEAD -> 4; + case UNKNOWN -> 5; + }; + var comparator = Comparator.comparingInt(statesPriorities); + yield loadDescriptionsByListings(ac, groups, comparator, pageNum, perPage, sortOrderDto); + } + case MEMBERS -> { + var comparator = Comparator.comparingInt(cg -> cg.members().size()); + var groupNames = groups.stream().map(ConsumerGroupListing::groupId).toList(); + yield ac.describeConsumerGroups(groupNames) + .map(descriptions -> + sortAndPaginate(descriptions.values(), comparator, pageNum, perPage, sortOrderDto).toList()); + } + case MESSAGES_BEHIND -> { + + Comparator comparator = Comparator.comparingLong(gwd -> + gwd.icg.getConsumerLag() == null ? 0L : gwd.icg.getConsumerLag()); + + yield loadDescriptionsByInternalConsumerGroups(ac, groups, comparator, pageNum, perPage, sortOrderDto); + } + + case TOPIC_NUM -> { + + Comparator comparator = Comparator.comparingInt(gwd -> gwd.icg.getTopicNum()); + + yield loadDescriptionsByInternalConsumerGroups(ac, groups, comparator, pageNum, perPage, sortOrderDto); + + } + }; + } + + private Mono> loadDescriptionsByListings(ReactiveAdminClient ac, + List listings, + Comparator comparator, + int pageNum, + int perPage, + SortOrderDTO sortOrderDto) { + List sortedGroups = sortAndPaginate(listings, comparator, pageNum, perPage, sortOrderDto) + .map(ConsumerGroupListing::groupId) + .toList(); + return ac.describeConsumerGroups(sortedGroups) + .map(descrMap -> sortedGroups.stream().map(descrMap::get).toList()); + } + + private Stream sortAndPaginate(Collection collection, + Comparator comparator, + int pageNum, + int perPage, + SortOrderDTO sortOrderDto) { + return collection.stream() + .sorted(sortOrderDto == SortOrderDTO.ASC ? comparator : comparator.reversed()) + .skip((long) (pageNum - 1) * perPage) + .limit(perPage); } - private Mono> describeConsumerGroups(ReactiveAdminClient ac, - @Nullable String search) { - return ac.listConsumerGroups() - .map(groupIds -> groupIds - .stream() - .filter(groupId -> search == null || StringUtils.containsIgnoreCase(groupId, search)) - .collect(Collectors.toList())) + private Mono> describeConsumerGroups(ReactiveAdminClient ac) { + return ac.listConsumerGroupNames() .flatMap(ac::describeConsumerGroups) .map(cgs -> new ArrayList<>(cgs.values())); } + + private Mono> loadDescriptionsByInternalConsumerGroups(ReactiveAdminClient ac, + List groups, + Comparator comparator, + int pageNum, + int perPage, + SortOrderDTO sortOrderDto) { + var groupNames = groups.stream().map(ConsumerGroupListing::groupId).toList(); + + return ac.describeConsumerGroups(groupNames) + .flatMap(descriptionsMap -> { + List descriptions = descriptionsMap.values().stream().toList(); + return getConsumerGroups(ac, descriptions) + .map(icg -> Streams.zip(icg.stream(), descriptions.stream(), GroupWithDescr::new).toList()) + .map(gwd -> sortAndPaginate(gwd, comparator, pageNum, perPage, sortOrderDto) + .map(GroupWithDescr::cgd).toList()); + } + ); + + } + public Mono getConsumerGroupDetail(KafkaCluster cluster, String consumerGroupId) { return adminClientService.get(cluster) @@ -196,24 +247,27 @@ public Mono deleteConsumerGroupById(KafkaCluster cluster, .flatMap(adminClient -> adminClient.deleteConsumerGroups(List.of(groupId))); } - public KafkaConsumer createConsumer(KafkaCluster cluster) { + public EnhancedConsumer createConsumer(KafkaCluster cluster) { return createConsumer(cluster, Map.of()); } - public KafkaConsumer createConsumer(KafkaCluster cluster, - Map properties) { + public EnhancedConsumer createConsumer(KafkaCluster cluster, + Map properties) { Properties props = new Properties(); + SslPropertiesUtil.addKafkaSslProperties(cluster.getOriginalProperties().getSsl(), props); props.putAll(cluster.getProperties()); - props.put(ConsumerConfig.CLIENT_ID_CONFIG, "kafka-ui-" + UUID.randomUUID()); + props.put(ConsumerConfig.CLIENT_ID_CONFIG, "kafka-ui-consumer-" + System.currentTimeMillis()); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers()); - props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, BytesDeserializer.class); - props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, BytesDeserializer.class); props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); props.put(ConsumerConfig.ALLOW_AUTO_CREATE_TOPICS_CONFIG, "false"); props.putAll(properties); - return new KafkaConsumer<>(props); + return new EnhancedConsumer( + props, + cluster.getPollingSettings().getPollingThrottler(), + ApplicationMetrics.forCluster(cluster) + ); } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/DeserializationService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/DeserializationService.java new file mode 100644 index 00000000000..8807c950597 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/DeserializationService.java @@ -0,0 +1,155 @@ +package com.provectus.kafka.ui.service; + +import com.provectus.kafka.ui.config.ClustersProperties; +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.SerdeDescriptionDTO; +import com.provectus.kafka.ui.serde.api.SchemaDescription; +import com.provectus.kafka.ui.serde.api.Serde; +import com.provectus.kafka.ui.serdes.ClusterSerdes; +import com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer; +import com.provectus.kafka.ui.serdes.ProducerRecordCreator; +import com.provectus.kafka.ui.serdes.SerdeInstance; +import com.provectus.kafka.ui.serdes.SerdesInitializer; +import java.io.Closeable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nullable; +import javax.validation.ValidationException; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +/** + * Class is responsible for managing serdes for kafka clusters. + * NOTE: Since Serde interface is designed to be blocking it is required that DeserializationService + * (and all Serde-related code) calls executed within special thread pool (boundedElastic). + */ +@Component +public class DeserializationService implements Closeable { + + private final Map clusterSerdes = new ConcurrentHashMap<>(); + + public DeserializationService(Environment env, + ClustersStorage clustersStorage, + ClustersProperties clustersProperties) { + var serdesInitializer = new SerdesInitializer(); + for (int i = 0; i < clustersProperties.getClusters().size(); i++) { + var clusterProperties = clustersProperties.getClusters().get(i); + var cluster = clustersStorage.getClusterByName(clusterProperties.getName()).get(); + clusterSerdes.put(cluster.getName(), serdesInitializer.init(env, clustersProperties, i)); + } + } + + private ClusterSerdes getSerdesFor(KafkaCluster cluster) { + return clusterSerdes.get(cluster.getName()); + } + + private Serde.Serializer getSerializer(KafkaCluster cluster, + String topic, + Serde.Target type, + String serdeName) { + var serdes = getSerdesFor(cluster); + var serde = serdes.serdeForName(serdeName) + .orElseThrow(() -> new ValidationException( + String.format("Serde %s not found", serdeName))); + if (!serde.canSerialize(topic, type)) { + throw new ValidationException( + String.format("Serde %s can't be applied for '%s' topic's %s serialization", serde, topic, type)); + } + return serde.serializer(topic, type); + } + + private SerdeInstance getSerdeForDeserialize(KafkaCluster cluster, + String topic, + Serde.Target type, + @Nullable String serdeName) { + var serdes = getSerdesFor(cluster); + if (serdeName != null) { + var serde = serdes.serdeForName(serdeName) + .orElseThrow(() -> new ValidationException(String.format("Serde '%s' not found", serdeName))); + if (!serde.canDeserialize(topic, type)) { + throw new ValidationException( + String.format("Serde '%s' can't be applied to '%s' topic %s", serdeName, topic, type)); + } + return serde; + } else { + return serdes.suggestSerdeForDeserialize(topic, type); + } + } + + public ProducerRecordCreator producerRecordCreator(KafkaCluster cluster, + String topic, + String keySerdeName, + String valueSerdeName) { + return new ProducerRecordCreator( + getSerializer(cluster, topic, Serde.Target.KEY, keySerdeName), + getSerializer(cluster, topic, Serde.Target.VALUE, valueSerdeName) + ); + } + + public ConsumerRecordDeserializer deserializerFor(KafkaCluster cluster, + String topic, + @Nullable String keySerdeName, + @Nullable String valueSerdeName) { + var keySerde = getSerdeForDeserialize(cluster, topic, Serde.Target.KEY, keySerdeName); + var valueSerde = getSerdeForDeserialize(cluster, topic, Serde.Target.VALUE, valueSerdeName); + var fallbackSerde = getSerdesFor(cluster).getFallbackSerde(); + return new ConsumerRecordDeserializer( + keySerde.getName(), + keySerde.deserializer(topic, Serde.Target.KEY), + valueSerde.getName(), + valueSerde.deserializer(topic, Serde.Target.VALUE), + fallbackSerde.getName(), + fallbackSerde.deserializer(topic, Serde.Target.KEY), + fallbackSerde.deserializer(topic, Serde.Target.VALUE), + cluster.getMasking().getMaskerForTopic(topic) + ); + } + + public List getSerdesForSerialize(KafkaCluster cluster, + String topic, + Serde.Target serdeType) { + var serdes = getSerdesFor(cluster); + var preferred = serdes.suggestSerdeForSerialize(topic, serdeType); + var result = new ArrayList(); + result.add(toDto(preferred, topic, serdeType, true)); + serdes.all() + .filter(s -> !s.getName().equals(preferred.getName())) + .filter(s -> s.canSerialize(topic, serdeType)) + .forEach(s -> result.add(toDto(s, topic, serdeType, false))); + return result; + } + + public List getSerdesForDeserialize(KafkaCluster cluster, + String topic, + Serde.Target serdeType) { + var serdes = getSerdesFor(cluster); + var preferred = serdes.suggestSerdeForDeserialize(topic, serdeType); + var result = new ArrayList(); + result.add(toDto(preferred, topic, serdeType, true)); + serdes.all() + .filter(s -> !s.getName().equals(preferred.getName())) + .filter(s -> s.canDeserialize(topic, serdeType)) + .forEach(s -> result.add(toDto(s, topic, serdeType, false))); + return result; + } + + private SerdeDescriptionDTO toDto(SerdeInstance serdeInstance, + String topic, + Serde.Target serdeType, + boolean preferred) { + var schemaOpt = serdeInstance.getSchema(topic, serdeType); + return new SerdeDescriptionDTO() + .name(serdeInstance.getName()) + .description(serdeInstance.description().orElse(null)) + .schema(schemaOpt.map(SchemaDescription::getSchema).orElse(null)) + .additionalProperties(schemaOpt.map(SchemaDescription::getAdditionalProperties).orElse(null)) + .preferred(preferred); + } + + @Override + public void close() { + clusterSerdes.values().forEach(ClusterSerdes::close); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/FeatureService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/FeatureService.java index 24a824fb168..b08691aef5a 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/FeatureService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/FeatureService.java @@ -1,65 +1,73 @@ package com.provectus.kafka.ui.service; -import com.provectus.kafka.ui.model.Feature; +import com.provectus.kafka.ui.model.ClusterFeature; import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.service.ReactiveAdminClient.ClusterDescription; import java.util.ArrayList; -import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Predicate; -import javax.annotation.Nullable; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.kafka.common.Node; +import org.apache.kafka.common.acl.AclOperation; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @Service -@RequiredArgsConstructor @Slf4j public class FeatureService { - private static final String DELETE_TOPIC_ENABLED_SERVER_PROPERTY = "delete.topic.enable"; + public Mono> getAvailableFeatures(ReactiveAdminClient adminClient, + KafkaCluster cluster, + ClusterDescription clusterDescription) { + List> features = new ArrayList<>(); - private final AdminClientService adminClientService; - - public Mono> getAvailableFeatures(KafkaCluster cluster, @Nullable Node controller) { - List> features = new ArrayList<>(); - - if (Optional.ofNullable(cluster.getKafkaConnect()) - .filter(Predicate.not(List::isEmpty)) + if (Optional.ofNullable(cluster.getConnectsClients()) + .filter(Predicate.not(Map::isEmpty)) .isPresent()) { - features.add(Mono.just(Feature.KAFKA_CONNECT)); + features.add(Mono.just(ClusterFeature.KAFKA_CONNECT)); } - if (cluster.getKsqldbServer() != null) { - features.add(Mono.just(Feature.KSQL_DB)); + if (cluster.getKsqlClient() != null) { + features.add(Mono.just(ClusterFeature.KSQL_DB)); } - if (cluster.getSchemaRegistry() != null) { - features.add(Mono.just(Feature.SCHEMA_REGISTRY)); + if (cluster.getSchemaRegistryClient() != null) { + features.add(Mono.just(ClusterFeature.SCHEMA_REGISTRY)); } - if (controller != null) { - features.add( - isTopicDeletionEnabled(cluster, controller) - .flatMap(r -> Boolean.TRUE.equals(r) ? Mono.just(Feature.TOPIC_DELETION) : Mono.empty()) - ); - } + features.add(topicDeletionEnabled(adminClient)); + features.add(aclView(adminClient)); + features.add(aclEdit(adminClient, clusterDescription)); return Flux.fromIterable(features).flatMap(m -> m).collectList(); } - private Mono isTopicDeletionEnabled(KafkaCluster cluster, Node controller) { - return adminClientService.get(cluster) - .flatMap(ac -> ac.loadBrokersConfig(List.of(controller.id()))) - .map(config -> - config.values().stream() - .flatMap(Collection::stream) - .filter(e -> e.name().equals(DELETE_TOPIC_ENABLED_SERVER_PROPERTY)) - .map(e -> Boolean.parseBoolean(e.value())) - .findFirst() - .orElse(false)); + private Mono topicDeletionEnabled(ReactiveAdminClient adminClient) { + return adminClient.isTopicDeletionEnabled() + ? Mono.just(ClusterFeature.TOPIC_DELETION) + : Mono.empty(); + } + + private Mono aclEdit(ReactiveAdminClient adminClient, ClusterDescription clusterDescription) { + var authorizedOps = Optional.ofNullable(clusterDescription.getAuthorizedOperations()).orElse(Set.of()); + boolean canEdit = aclViewEnabled(adminClient) + && (authorizedOps.contains(AclOperation.ALL) || authorizedOps.contains(AclOperation.ALTER)); + return canEdit + ? Mono.just(ClusterFeature.KAFKA_ACL_EDIT) + : Mono.empty(); } + + private Mono aclView(ReactiveAdminClient adminClient) { + return aclViewEnabled(adminClient) + ? Mono.just(ClusterFeature.KAFKA_ACL_VIEW) + : Mono.empty(); + } + + private boolean aclViewEnabled(ReactiveAdminClient adminClient) { + return adminClient.getClusterFeatures().contains(ReactiveAdminClient.SupportedFeature.AUTHORIZED_SECURITY_ENABLED); + } + } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaClusterFactory.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaClusterFactory.java new file mode 100644 index 00000000000..964b25473d3 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaClusterFactory.java @@ -0,0 +1,221 @@ +package com.provectus.kafka.ui.service; + +import com.provectus.kafka.ui.client.RetryingKafkaConnectClient; +import com.provectus.kafka.ui.config.ClustersProperties; +import com.provectus.kafka.ui.config.WebclientProperties; +import com.provectus.kafka.ui.connect.api.KafkaConnectClientApi; +import com.provectus.kafka.ui.emitter.PollingSettings; +import com.provectus.kafka.ui.model.ApplicationPropertyValidationDTO; +import com.provectus.kafka.ui.model.ClusterConfigValidationDTO; +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.MetricsConfig; +import com.provectus.kafka.ui.service.ksql.KsqlApiClient; +import com.provectus.kafka.ui.service.masking.DataMasking; +import com.provectus.kafka.ui.sr.ApiClient; +import com.provectus.kafka.ui.sr.api.KafkaSrClientApi; +import com.provectus.kafka.ui.util.KafkaServicesValidation; +import com.provectus.kafka.ui.util.ReactiveFailover; +import com.provectus.kafka.ui.util.WebClientConfigurator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.unit.DataSize; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +@Service +@Slf4j +public class KafkaClusterFactory { + + private static final DataSize DEFAULT_WEBCLIENT_BUFFER = DataSize.parse("20MB"); + + private final DataSize webClientMaxBuffSize; + + public KafkaClusterFactory(WebclientProperties webclientProperties) { + this.webClientMaxBuffSize = Optional.ofNullable(webclientProperties.getMaxInMemoryBufferSize()) + .map(DataSize::parse) + .orElse(DEFAULT_WEBCLIENT_BUFFER); + } + + public KafkaCluster create(ClustersProperties properties, + ClustersProperties.Cluster clusterProperties) { + KafkaCluster.KafkaClusterBuilder builder = KafkaCluster.builder(); + + builder.name(clusterProperties.getName()); + builder.bootstrapServers(clusterProperties.getBootstrapServers()); + builder.properties(convertProperties(clusterProperties.getProperties())); + builder.readOnly(clusterProperties.isReadOnly()); + builder.masking(DataMasking.create(clusterProperties.getMasking())); + builder.pollingSettings(PollingSettings.create(clusterProperties, properties)); + + if (schemaRegistryConfigured(clusterProperties)) { + builder.schemaRegistryClient(schemaRegistryClient(clusterProperties)); + } + if (connectClientsConfigured(clusterProperties)) { + builder.connectsClients(connectClients(clusterProperties)); + } + if (ksqlConfigured(clusterProperties)) { + builder.ksqlClient(ksqlClient(clusterProperties)); + } + if (metricsConfigured(clusterProperties)) { + builder.metricsConfig(metricsConfigDataToMetricsConfig(clusterProperties.getMetrics())); + } + builder.originalProperties(clusterProperties); + return builder.build(); + } + + public Mono validate(ClustersProperties.Cluster clusterProperties) { + if (clusterProperties.getSsl() != null) { + Optional errMsg = KafkaServicesValidation.validateTruststore(clusterProperties.getSsl()); + if (errMsg.isPresent()) { + return Mono.just(new ClusterConfigValidationDTO() + .kafka(new ApplicationPropertyValidationDTO() + .error(true) + .errorMessage("Truststore not valid: " + errMsg.get()))); + } + } + + return Mono.zip( + KafkaServicesValidation.validateClusterConnection( + clusterProperties.getBootstrapServers(), + convertProperties(clusterProperties.getProperties()), + clusterProperties.getSsl() + ), + schemaRegistryConfigured(clusterProperties) + ? KafkaServicesValidation.validateSchemaRegistry( + () -> schemaRegistryClient(clusterProperties)).map(Optional::of) + : Mono.>just(Optional.empty()), + + ksqlConfigured(clusterProperties) + ? KafkaServicesValidation.validateKsql(() -> ksqlClient(clusterProperties)).map(Optional::of) + : Mono.>just(Optional.empty()), + + connectClientsConfigured(clusterProperties) + ? + Flux.fromIterable(clusterProperties.getKafkaConnect()) + .flatMap(c -> + KafkaServicesValidation.validateConnect(() -> connectClient(clusterProperties, c)) + .map(r -> Tuples.of(c.getName(), r))) + .collectMap(Tuple2::getT1, Tuple2::getT2) + .map(Optional::of) + : + Mono.>>just(Optional.empty()) + ).map(tuple -> { + var validation = new ClusterConfigValidationDTO(); + validation.kafka(tuple.getT1()); + tuple.getT2().ifPresent(validation::schemaRegistry); + tuple.getT3().ifPresent(validation::ksqldb); + tuple.getT4().ifPresent(validation::kafkaConnects); + return validation; + }); + } + + private Properties convertProperties(Map propertiesMap) { + Properties properties = new Properties(); + if (propertiesMap != null) { + properties.putAll(propertiesMap); + } + return properties; + } + + private boolean connectClientsConfigured(ClustersProperties.Cluster clusterProperties) { + return clusterProperties.getKafkaConnect() != null; + } + + private Map> connectClients( + ClustersProperties.Cluster clusterProperties) { + Map> connects = new HashMap<>(); + clusterProperties.getKafkaConnect().forEach(c -> connects.put(c.getName(), connectClient(clusterProperties, c))); + return connects; + } + + private ReactiveFailover connectClient(ClustersProperties.Cluster cluster, + ClustersProperties.ConnectCluster connectCluster) { + return ReactiveFailover.create( + parseUrlList(connectCluster.getAddress()), + url -> new RetryingKafkaConnectClient( + connectCluster.toBuilder().address(url).build(), + cluster.getSsl(), + webClientMaxBuffSize + ), + ReactiveFailover.CONNECTION_REFUSED_EXCEPTION_FILTER, + "No alive connect instances available", + ReactiveFailover.DEFAULT_RETRY_GRACE_PERIOD_MS + ); + } + + private boolean schemaRegistryConfigured(ClustersProperties.Cluster clusterProperties) { + return clusterProperties.getSchemaRegistry() != null; + } + + private ReactiveFailover schemaRegistryClient(ClustersProperties.Cluster clusterProperties) { + var auth = Optional.ofNullable(clusterProperties.getSchemaRegistryAuth()) + .orElse(new ClustersProperties.SchemaRegistryAuth()); + WebClient webClient = new WebClientConfigurator() + .configureSsl(clusterProperties.getSsl(), clusterProperties.getSchemaRegistrySsl()) + .configureBasicAuth(auth.getUsername(), auth.getPassword()) + .configureBufferSize(webClientMaxBuffSize) + .build(); + return ReactiveFailover.create( + parseUrlList(clusterProperties.getSchemaRegistry()), + url -> new KafkaSrClientApi(new ApiClient(webClient, null, null).setBasePath(url)), + ReactiveFailover.CONNECTION_REFUSED_EXCEPTION_FILTER, + "No live schemaRegistry instances available", + ReactiveFailover.DEFAULT_RETRY_GRACE_PERIOD_MS + ); + } + + private boolean ksqlConfigured(ClustersProperties.Cluster clusterProperties) { + return clusterProperties.getKsqldbServer() != null; + } + + private ReactiveFailover ksqlClient(ClustersProperties.Cluster clusterProperties) { + return ReactiveFailover.create( + parseUrlList(clusterProperties.getKsqldbServer()), + url -> new KsqlApiClient( + url, + clusterProperties.getKsqldbServerAuth(), + clusterProperties.getSsl(), + clusterProperties.getKsqldbServerSsl(), + webClientMaxBuffSize + ), + ReactiveFailover.CONNECTION_REFUSED_EXCEPTION_FILTER, + "No live ksqldb instances available", + ReactiveFailover.DEFAULT_RETRY_GRACE_PERIOD_MS + ); + } + + private List parseUrlList(String url) { + return Stream.of(url.split(",")).map(String::trim).filter(s -> !s.isBlank()).toList(); + } + + private boolean metricsConfigured(ClustersProperties.Cluster clusterProperties) { + return clusterProperties.getMetrics() != null; + } + + @Nullable + private MetricsConfig metricsConfigDataToMetricsConfig(ClustersProperties.MetricsConfigData metricsConfigData) { + if (metricsConfigData == null) { + return null; + } + MetricsConfig.MetricsConfigBuilder builder = MetricsConfig.builder(); + builder.type(metricsConfigData.getType()); + builder.port(metricsConfigData.getPort()); + builder.ssl(Optional.ofNullable(metricsConfigData.getSsl()).orElse(false)); + builder.username(metricsConfigData.getUsername()); + builder.password(metricsConfigData.getPassword()); + builder.keystoreLocation(metricsConfigData.getKeystoreLocation()); + builder.keystorePassword(metricsConfigData.getKeystorePassword()); + return builder.build(); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConfigSanitizer.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConfigSanitizer.java index 30daa1ca57c..b4cdf144c9f 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConfigSanitizer.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConfigSanitizer.java @@ -1,37 +1,60 @@ package com.provectus.kafka.ui.service; +import static java.util.regex.Pattern.CASE_INSENSITIVE; + +import com.google.common.collect.ImmutableList; import java.util.Arrays; -import java.util.HashSet; +import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; +import javax.annotation.Nullable; import org.apache.kafka.common.config.ConfigDef; import org.apache.kafka.common.config.SaslConfigs; import org.apache.kafka.common.config.SslConfigs; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.actuate.endpoint.Sanitizer; import org.springframework.stereotype.Component; @Component -class KafkaConfigSanitizer extends Sanitizer { - private static final List DEFAULT_PATTERNS_TO_SANITIZE = Arrays.asList( - "basic.auth.user.info", /* For Schema Registry credentials */ - "password", "secret", "token", "key", ".*credentials.*" /* General credential patterns */ - ); +class KafkaConfigSanitizer { + + private static final String SANITIZED_VALUE = "******"; + + private static final String[] REGEX_PARTS = {"*", "$", "^", "+"}; + + private static final List DEFAULT_PATTERNS_TO_SANITIZE = ImmutableList.builder() + .addAll(kafkaConfigKeysToSanitize()) + .add( + "basic.auth.user.info", /* For Schema Registry credentials */ + "password", "secret", "token", "key", ".*credentials.*", /* General credential patterns */ + "aws.access.*", "aws.secret.*", "aws.session.*" /* AWS-related credential patterns */ + ) + .build(); + + private final List sanitizeKeysPatterns; KafkaConfigSanitizer( @Value("${kafka.config.sanitizer.enabled:true}") boolean enabled, @Value("${kafka.config.sanitizer.patterns:}") List patternsToSanitize ) { - if (!enabled) { - setKeysToSanitize(); - } else { - var keysToSanitize = new HashSet<>( - patternsToSanitize.isEmpty() ? DEFAULT_PATTERNS_TO_SANITIZE : patternsToSanitize); - keysToSanitize.addAll(kafkaConfigKeysToSanitize()); - setKeysToSanitize(keysToSanitize.toArray(new String[] {})); - } + this.sanitizeKeysPatterns = enabled + ? compile(patternsToSanitize.isEmpty() ? DEFAULT_PATTERNS_TO_SANITIZE : patternsToSanitize) + : List.of(); + } + + private static List compile(Collection patternStrings) { + return patternStrings.stream() + .map(p -> isRegex(p) + ? Pattern.compile(p, CASE_INSENSITIVE) + : Pattern.compile(".*" + p + "$", CASE_INSENSITIVE)) + .toList(); + } + + private static boolean isRegex(String str) { + return Arrays.stream(REGEX_PARTS).anyMatch(str::contains); } private static Set kafkaConfigKeysToSanitize() { @@ -44,4 +67,22 @@ private static Set kafkaConfigKeysToSanitize() { .collect(Collectors.toSet()); } + @Nullable + public Object sanitize(String key, @Nullable Object value) { + for (Pattern pattern : sanitizeKeysPatterns) { + if (pattern.matcher(key).matches()) { + return SANITIZED_VALUE; + } + } + return value; + } + + public Map sanitizeConnectorConfig(@Nullable Map original) { + var result = new HashMap(); //null-values supporting map! + if (original != null) { + original.forEach((k, v) -> result.put(k, sanitize(k, v))); + } + return result; + } + } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConnectService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConnectService.java index 21680d3dd91..605d5cab205 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConnectService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConnectService.java @@ -2,13 +2,12 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import com.provectus.kafka.ui.client.KafkaConnectClients; import com.provectus.kafka.ui.connect.api.KafkaConnectClientApi; import com.provectus.kafka.ui.connect.model.ConnectorStatus; import com.provectus.kafka.ui.connect.model.ConnectorStatusConnector; import com.provectus.kafka.ui.connect.model.ConnectorTopics; import com.provectus.kafka.ui.connect.model.TaskStatus; -import com.provectus.kafka.ui.exception.ConnectNotFoundException; +import com.provectus.kafka.ui.exception.NotFoundException; import com.provectus.kafka.ui.exception.ValidationException; import com.provectus.kafka.ui.mapper.ClusterMapper; import com.provectus.kafka.ui.mapper.KafkaConnectMapper; @@ -21,17 +20,16 @@ import com.provectus.kafka.ui.model.ConnectorTaskStatusDTO; import com.provectus.kafka.ui.model.FullConnectorInfoDTO; import com.provectus.kafka.ui.model.KafkaCluster; -import com.provectus.kafka.ui.model.KafkaConnectCluster; import com.provectus.kafka.ui.model.NewConnectorDTO; import com.provectus.kafka.ui.model.TaskDTO; import com.provectus.kafka.ui.model.connect.InternalConnectInfo; -import java.util.HashMap; +import com.provectus.kafka.ui.util.ReactiveFailover; import java.util.List; import java.util.Map; -import java.util.function.Function; +import java.util.Optional; import java.util.function.Predicate; -import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.annotation.Nullable; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -40,8 +38,6 @@ import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; @Service @Slf4j @@ -52,109 +48,86 @@ public class KafkaConnectService { private final ObjectMapper objectMapper; private final KafkaConfigSanitizer kafkaConfigSanitizer; - public Mono> getConnects(KafkaCluster cluster) { - return Mono.just( - Flux.fromIterable( - cluster.getKafkaConnect().stream() - .map(clusterMapper::toKafkaConnect) - .collect(Collectors.toList()) - ) + public Flux getConnects(KafkaCluster cluster) { + return Flux.fromIterable( + Optional.ofNullable(cluster.getOriginalProperties().getKafkaConnect()) + .map(lst -> lst.stream().map(clusterMapper::toKafkaConnect).toList()) + .orElse(List.of()) ); } public Flux getAllConnectors(final KafkaCluster cluster, - final String search) { + @Nullable final String search) { return getConnects(cluster) - .flatMapMany(Function.identity()) - .flatMap(connect -> getConnectorNames(cluster, connect.getName())) - .flatMap(pair -> getConnector(cluster, pair.getT1(), pair.getT2())) - .flatMap(connector -> - getConnectorConfig(cluster, connector.getConnect(), connector.getName()) - .map(config -> InternalConnectInfo.builder() - .connector(connector) - .config(config) - .build() - ) - ) - .flatMap(connectInfo -> { - ConnectorDTO connector = connectInfo.getConnector(); - return getConnectorTasks(cluster, connector.getConnect(), connector.getName()) - .collectList() - .map(tasks -> InternalConnectInfo.builder() - .connector(connector) - .config(connectInfo.getConfig()) - .tasks(tasks) - .build() - ); - }) - .flatMap(connectInfo -> { - ConnectorDTO connector = connectInfo.getConnector(); - return getConnectorTopics(cluster, connector.getConnect(), connector.getName()) - .map(ct -> InternalConnectInfo.builder() - .connector(connector) - .config(connectInfo.getConfig()) - .tasks(connectInfo.getTasks()) - .topics(ct.getTopics()) - .build() - ); - }) - .map(kafkaConnectMapper::fullConnectorInfoFromTuple) + .flatMap(connect -> + getConnectorNamesWithErrorsSuppress(cluster, connect.getName()) + .flatMap(connectorName -> + Mono.zip( + getConnector(cluster, connect.getName(), connectorName), + getConnectorConfig(cluster, connect.getName(), connectorName), + getConnectorTasks(cluster, connect.getName(), connectorName).collectList(), + getConnectorTopics(cluster, connect.getName(), connectorName) + ).map(tuple -> + InternalConnectInfo.builder() + .connector(tuple.getT1()) + .config(tuple.getT2()) + .tasks(tuple.getT3()) + .topics(tuple.getT4().getTopics()) + .build()))) + .map(kafkaConnectMapper::fullConnectorInfo) .filter(matchesSearchTerm(search)); } - private Predicate matchesSearchTerm(final String search) { - return connector -> getSearchValues(connector) - .anyMatch(value -> value.contains( - StringUtils.defaultString( - search, - StringUtils.EMPTY) - .toUpperCase())); + private Predicate matchesSearchTerm(@Nullable final String search) { + if (search == null) { + return c -> true; + } + return connector -> getStringsForSearch(connector) + .anyMatch(string -> StringUtils.containsIgnoreCase(string, search)); } - private Stream getSearchValues(FullConnectorInfoDTO fullConnectorInfo) { + private Stream getStringsForSearch(FullConnectorInfoDTO fullConnectorInfo) { return Stream.of( - fullConnectorInfo.getName(), - fullConnectorInfo.getStatus().getState().getValue(), - fullConnectorInfo.getType().getValue()) - .map(String::toUpperCase); + fullConnectorInfo.getName(), + fullConnectorInfo.getConnect(), + fullConnectorInfo.getStatus().getState().getValue(), + fullConnectorInfo.getType().getValue()); } - private Mono getConnectorTopics(KafkaCluster cluster, String connectClusterName, - String connectorName) { - return withConnectClient(cluster, connectClusterName) - .flatMap(c -> c.getConnectorTopics(connectorName).map(result -> result.get(connectorName))) - // old connectors don't have this api, setting empty list for + public Mono getConnectorTopics(KafkaCluster cluster, String connectClusterName, + String connectorName) { + return api(cluster, connectClusterName) + .mono(c -> c.getConnectorTopics(connectorName)) + .map(result -> result.get(connectorName)) + // old Connect API versions don't have this endpoint, setting empty list for // backward-compatibility .onErrorResume(Exception.class, e -> Mono.just(new ConnectorTopics().topics(List.of()))); } - private Flux> getConnectorNames(KafkaCluster cluster, String connectName) { - return getConnectors(cluster, connectName) - .collectList().map(e -> e.get(0)) + public Flux getConnectorNames(KafkaCluster cluster, String connectName) { + return api(cluster, connectName) + .flux(client -> client.getConnectors(null)) // for some reason `getConnectors` method returns the response as a single string - .map(this::parseToList) - .flatMapMany(Flux::fromIterable) - .map(connector -> Tuples.of(connectName, connector)); + .collectList().map(e -> e.get(0)) + .map(this::parseConnectorsNamesStringToList) + .flatMapMany(Flux::fromIterable); + } + + // returns empty flux if there was an error communicating with Connect + public Flux getConnectorNamesWithErrorsSuppress(KafkaCluster cluster, String connectName) { + return getConnectorNames(cluster, connectName).onErrorComplete(); } @SneakyThrows - private List parseToList(String json) { + private List parseConnectorsNamesStringToList(String json) { return objectMapper.readValue(json, new TypeReference<>() { }); } - public Flux getConnectors(KafkaCluster cluster, String connectName) { - return withConnectClient(cluster, connectName) - .flatMapMany(client -> - client.getConnectors(null) - .doOnError(e -> log.error("Unexpected error upon getting connectors", e)) - ); - } - public Mono createConnector(KafkaCluster cluster, String connectName, Mono connector) { - return withConnectClient(cluster, connectName) - .flatMap(client -> + return api(cluster, connectName) + .mono(client -> connector .flatMap(c -> connectorExists(cluster, connectName, c.getName()) .map(exists -> { @@ -173,15 +146,13 @@ public Mono createConnector(KafkaCluster cluster, String connectNa private Mono connectorExists(KafkaCluster cluster, String connectName, String connectorName) { return getConnectorNames(cluster, connectName) - .map(Tuple2::getT2) - .collectList() - .map(connectorNames -> connectorNames.contains(connectorName)); + .any(name -> name.equals(connectorName)); } public Mono getConnector(KafkaCluster cluster, String connectName, String connectorName) { - return withConnectClient(cluster, connectName) - .flatMap(client -> client.getConnector(connectorName) + return api(cluster, connectName) + .mono(client -> client.getConnector(connectorName) .map(kafkaConnectMapper::fromClient) .flatMap(connector -> client.getConnectorStatus(connector.getName()) @@ -190,19 +161,14 @@ public Mono getConnector(KafkaCluster cluster, String connectName, e -> emptyStatus(connectorName)) .map(connectorStatus -> { var status = connectorStatus.getConnector(); - final Map obfuscatedConfig = connector.getConfig().entrySet() - .stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - e -> kafkaConfigSanitizer.sanitize(e.getKey(), e.getValue()) - )); - ConnectorDTO result = (ConnectorDTO) new ConnectorDTO() + var sanitizedConfig = kafkaConfigSanitizer.sanitizeConnectorConfig(connector.getConfig()); + ConnectorDTO result = new ConnectorDTO() .connect(connectName) .status(kafkaConnectMapper.fromClient(status)) .type(connector.getType()) .tasks(connector.getTasks()) .name(connector.getName()) - .config(obfuscatedConfig); + .config(sanitizedConfig); if (connectorStatus.getTasks() != null) { boolean isAnyTaskFailed = connectorStatus.getTasks().stream() @@ -229,35 +195,30 @@ private Mono emptyStatus(String connectorName) { public Mono> getConnectorConfig(KafkaCluster cluster, String connectName, String connectorName) { - return withConnectClient(cluster, connectName) - .flatMap(c -> c.getConnectorConfig(connectorName)) - .map(connectorConfig -> { - final Map obfuscatedMap = new HashMap<>(); - connectorConfig.forEach((key, value) -> - obfuscatedMap.put(key, kafkaConfigSanitizer.sanitize(key, value))); - return obfuscatedMap; - }); + return api(cluster, connectName) + .mono(c -> c.getConnectorConfig(connectorName)) + .map(kafkaConfigSanitizer::sanitizeConnectorConfig); } public Mono setConnectorConfig(KafkaCluster cluster, String connectName, - String connectorName, Mono requestBody) { - return withConnectClient(cluster, connectName) - .flatMap(c -> + String connectorName, Mono> requestBody) { + return api(cluster, connectName) + .mono(c -> requestBody - .flatMap(body -> c.setConnectorConfig(connectorName, (Map) body)) + .flatMap(body -> c.setConnectorConfig(connectorName, body)) .map(kafkaConnectMapper::fromClient)); } public Mono deleteConnector( KafkaCluster cluster, String connectName, String connectorName) { - return withConnectClient(cluster, connectName) - .flatMap(c -> c.deleteConnector(connectorName)); + return api(cluster, connectName) + .mono(c -> c.deleteConnector(connectorName)); } public Mono updateConnectorState(KafkaCluster cluster, String connectName, String connectorName, ConnectorActionDTO action) { - return withConnectClient(cluster, connectName) - .flatMap(client -> { + return api(cluster, connectName) + .mono(client -> { switch (action) { case RESTART: return client.restartConnector(connectorName, false, false); @@ -286,8 +247,8 @@ private Mono restartTasks(KafkaCluster cluster, String connectName, } public Flux getConnectorTasks(KafkaCluster cluster, String connectName, String connectorName) { - return withConnectClient(cluster, connectName) - .flatMapMany(client -> + return api(cluster, connectName) + .flux(client -> client.getConnectorTasks(connectorName) .onErrorResume(WebClientResponseException.NotFound.class, e -> Flux.empty()) .map(kafkaConnectMapper::fromClient) @@ -302,32 +263,33 @@ public Flux getConnectorTasks(KafkaCluster cluster, String connectName, public Mono restartConnectorTask(KafkaCluster cluster, String connectName, String connectorName, Integer taskId) { - return withConnectClient(cluster, connectName) - .flatMap(client -> client.restartConnectorTask(connectorName, taskId)); + return api(cluster, connectName) + .mono(client -> client.restartConnectorTask(connectorName, taskId)); } - public Mono> getConnectorPlugins(KafkaCluster cluster, - String connectName) { - return withConnectClient(cluster, connectName) - .map(client -> client.getConnectorPlugins().map(kafkaConnectMapper::fromClient)); + public Flux getConnectorPlugins(KafkaCluster cluster, + String connectName) { + return api(cluster, connectName) + .flux(client -> client.getConnectorPlugins().map(kafkaConnectMapper::fromClient)); } public Mono validateConnectorPluginConfig( - KafkaCluster cluster, String connectName, String pluginName, Mono requestBody) { - return withConnectClient(cluster, connectName) - .flatMap(client -> + KafkaCluster cluster, String connectName, String pluginName, Mono> requestBody) { + return api(cluster, connectName) + .mono(client -> requestBody .flatMap(body -> - client.validateConnectorPluginConfig(pluginName, (Map) body)) + client.validateConnectorPluginConfig(pluginName, body)) .map(kafkaConnectMapper::fromClient) ); } - private Mono withConnectClient(KafkaCluster cluster, String connectName) { - return Mono.justOrEmpty(cluster.getKafkaConnect().stream() - .filter(connect -> connect.getName().equals(connectName)) - .findFirst()) - .switchIfEmpty(Mono.error(ConnectNotFoundException::new)) - .map(KafkaConnectClients::withKafkaConnectConfig); + private ReactiveFailover api(KafkaCluster cluster, String connectName) { + var client = cluster.getConnectsClients().get(connectName); + if (client == null) { + throw new NotFoundException( + "Connect %s not found for cluster %s".formatted(connectName, cluster.getName())); + } + return client; } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KsqlService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KsqlService.java deleted file mode 100644 index 4e970dc0c8c..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KsqlService.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.provectus.kafka.ui.service; - -import com.provectus.kafka.ui.client.KsqlClient; -import com.provectus.kafka.ui.exception.ClusterNotFoundException; -import com.provectus.kafka.ui.exception.KsqlDbNotFoundException; -import com.provectus.kafka.ui.exception.UnprocessableEntityException; -import com.provectus.kafka.ui.model.KafkaCluster; -import com.provectus.kafka.ui.model.KsqlCommandDTO; -import com.provectus.kafka.ui.model.KsqlCommandResponseDTO; -import com.provectus.kafka.ui.strategy.ksql.statement.BaseStrategy; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import reactor.core.publisher.Mono; - -@Service -@RequiredArgsConstructor -public class KsqlService { - private final KsqlClient ksqlClient; - private final List ksqlStatementStrategies; - - public Mono executeKsqlCommand(KafkaCluster cluster, - Mono ksqlCommand) { - return Mono.justOrEmpty(cluster) - .map(KafkaCluster::getKsqldbServer) - .onErrorResume(e -> { - Throwable throwable = - e instanceof ClusterNotFoundException ? e : new KsqlDbNotFoundException(); - return Mono.error(throwable); - }) - .flatMap(host -> getStatementStrategyForKsqlCommand(ksqlCommand) - .map(statement -> statement.host(host)) - ) - .flatMap(ksqlClient::execute); - } - - private Mono getStatementStrategyForKsqlCommand( - Mono ksqlCommand) { - return ksqlCommand - .map(command -> ksqlStatementStrategies.stream() - .filter(s -> s.test(command.getKsql())) - .map(s -> s.ksqlCommand(command)) - .findFirst()) - .flatMap(Mono::justOrEmpty) - .switchIfEmpty(Mono.error(new UnprocessableEntityException("Invalid sql"))); - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/MessagesService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/MessagesService.java index f79eaf8ba59..620bd840861 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/MessagesService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/MessagesService.java @@ -1,7 +1,9 @@ package com.provectus.kafka.ui.service; -import com.provectus.kafka.ui.emitter.BackwardRecordEmitter; -import com.provectus.kafka.ui.emitter.ForwardRecordEmitter; +import com.google.common.util.concurrent.RateLimiter; +import com.provectus.kafka.ui.config.ClustersProperties; +import com.provectus.kafka.ui.emitter.BackwardEmitter; +import com.provectus.kafka.ui.emitter.ForwardEmitter; import com.provectus.kafka.ui.emitter.MessageFilters; import com.provectus.kafka.ui.emitter.TailingEmitter; import com.provectus.kafka.ui.exception.TopicNotFoundException; @@ -11,21 +13,24 @@ import com.provectus.kafka.ui.model.KafkaCluster; import com.provectus.kafka.ui.model.MessageFilterTypeDTO; import com.provectus.kafka.ui.model.SeekDirectionDTO; +import com.provectus.kafka.ui.model.SmartFilterTestExecutionDTO; +import com.provectus.kafka.ui.model.SmartFilterTestExecutionResultDTO; +import com.provectus.kafka.ui.model.TopicMessageDTO; import com.provectus.kafka.ui.model.TopicMessageEventDTO; -import com.provectus.kafka.ui.serde.DeserializationService; -import com.provectus.kafka.ui.serde.RecordSerDe; -import com.provectus.kafka.ui.util.OffsetsSeekBackward; -import com.provectus.kafka.ui.util.OffsetsSeekForward; -import com.provectus.kafka.ui.util.ResultSizeLimiter; +import com.provectus.kafka.ui.serdes.ProducerRecordCreator; +import com.provectus.kafka.ui.util.SslPropertiesUtil; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Properties; import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; +import java.util.function.UnaryOperator; import java.util.stream.Collectors; import javax.annotation.Nullable; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.kafka.clients.admin.OffsetSpec; @@ -35,23 +40,42 @@ import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.clients.producer.RecordMetadata; import org.apache.kafka.common.TopicPartition; -import org.apache.kafka.common.header.Header; -import org.apache.kafka.common.header.internals.RecordHeader; -import org.apache.kafka.common.header.internals.RecordHeaders; import org.apache.kafka.common.serialization.ByteArraySerializer; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; -import reactor.core.publisher.FluxSink; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; @Service -@RequiredArgsConstructor @Slf4j public class MessagesService { + + private static final int DEFAULT_MAX_PAGE_SIZE = 500; + private static final int DEFAULT_PAGE_SIZE = 100; + // limiting UI messages rate to 20/sec in tailing mode + private static final int TAILING_UI_MESSAGE_THROTTLE_RATE = 20; + private final AdminClientService adminClientService; private final DeserializationService deserializationService; private final ConsumerGroupService consumerGroupService; + private final int maxPageSize; + private final int defaultPageSize; + + public MessagesService(AdminClientService adminClientService, + DeserializationService deserializationService, + ConsumerGroupService consumerGroupService, + ClustersProperties properties) { + this.adminClientService = adminClientService; + this.deserializationService = deserializationService; + this.consumerGroupService = consumerGroupService; + + var pollingProps = Optional.ofNullable(properties.getPolling()) + .orElseGet(ClustersProperties.PollingProperties::new); + this.maxPageSize = Optional.ofNullable(pollingProps.getMaxPageSize()) + .orElse(DEFAULT_MAX_PAGE_SIZE); + this.defaultPageSize = Optional.ofNullable(pollingProps.getDefaultPageSize()) + .orElse(DEFAULT_PAGE_SIZE); + } private Mono withExistingTopic(KafkaCluster cluster, String topicName) { return adminClientService.get(cluster) @@ -59,6 +83,40 @@ private Mono withExistingTopic(KafkaCluster cluster, String to .switchIfEmpty(Mono.error(new TopicNotFoundException())); } + public static SmartFilterTestExecutionResultDTO execSmartFilterTest(SmartFilterTestExecutionDTO execData) { + Predicate predicate; + try { + predicate = MessageFilters.createMsgFilter( + execData.getFilterCode(), + MessageFilterTypeDTO.GROOVY_SCRIPT + ); + } catch (Exception e) { + log.info("Smart filter '{}' compilation error", execData.getFilterCode(), e); + return new SmartFilterTestExecutionResultDTO() + .error("Compilation error : " + e.getMessage()); + } + try { + var result = predicate.test( + new TopicMessageDTO() + .key(execData.getKey()) + .content(execData.getValue()) + .headers(execData.getHeaders()) + .offset(execData.getOffset()) + .partition(execData.getPartition()) + .timestamp( + Optional.ofNullable(execData.getTimestampMs()) + .map(ts -> OffsetDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneOffset.UTC)) + .orElse(null)) + ); + return new SmartFilterTestExecutionResultDTO() + .result(result); + } catch (Exception e) { + log.info("Smart filter {} execution error", execData, e); + return new SmartFilterTestExecutionResultDTO() + .error("Execution error : " + e.getMessage()); + } + } + public Mono deleteTopicMessages(KafkaCluster cluster, String topicName, List partitionsToInclude) { return withExistingTopic(cluster, topicName) @@ -71,8 +129,8 @@ public Mono deleteTopicMessages(KafkaCluster cluster, String topicName, private Mono> offsetsForDeletion(KafkaCluster cluster, String topicName, List partitionsToInclude) { return adminClientService.get(cluster).flatMap(ac -> - ac.listOffsets(topicName, OffsetSpec.earliest()) - .zipWith(ac.listOffsets(topicName, OffsetSpec.latest()), + ac.listTopicOffsets(topicName, OffsetSpec.earliest(), true) + .zipWith(ac.listTopicOffsets(topicName, OffsetSpec.latest(), true), (start, end) -> end.entrySet().stream() .filter(e -> partitionsToInclude.isEmpty() @@ -86,6 +144,7 @@ private Mono> offsetsForDeletion(KafkaCluster cluster, public Mono sendMessage(KafkaCluster cluster, String topic, CreateTopicMessageDTO msg) { return withExistingTopic(cluster, topic) + .publishOn(Schedulers.boundedElastic()) .flatMap(desc -> sendMessageImpl(cluster, desc, msg)); } @@ -96,28 +155,22 @@ private Mono sendMessageImpl(KafkaCluster cluster, && msg.getPartition() > topicDescription.partitions().size() - 1) { return Mono.error(new ValidationException("Invalid partition")); } - RecordSerDe serde = - deserializationService.getRecordDeserializerForCluster(cluster); + ProducerRecordCreator producerRecordCreator = + deserializationService.producerRecordCreator( + cluster, + topicDescription.name(), + msg.getKeySerde().get(), + msg.getValueSerde().get() + ); - Properties properties = new Properties(); - properties.putAll(cluster.getProperties()); - properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers()); - properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class); - properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class); - try (KafkaProducer producer = new KafkaProducer<>(properties)) { - ProducerRecord producerRecord = serde.serialize( + try (KafkaProducer producer = createProducer(cluster, Map.of())) { + ProducerRecord producerRecord = producerRecordCreator.create( topicDescription.name(), + msg.getPartition(), msg.getKey().orElse(null), msg.getContent().orElse(null), - msg.getPartition() + msg.getHeaders() ); - producerRecord = new ProducerRecord<>( - producerRecord.topic(), - producerRecord.partition(), - producerRecord.key(), - producerRecord.value(), - createHeaders(msg.getHeaders())); - CompletableFuture cf = new CompletableFuture<>(); producer.send(producerRecord, (metadata, exception) -> { if (exception != null) { @@ -132,79 +185,88 @@ private Mono sendMessageImpl(KafkaCluster cluster, } } - private Iterable
createHeaders(@Nullable Map clientHeaders) { - if (clientHeaders == null) { - return new RecordHeaders(); - } - RecordHeaders headers = new RecordHeaders(); - clientHeaders.forEach((k, v) -> headers.add(new RecordHeader(k, v.getBytes()))); - return headers; + public static KafkaProducer createProducer(KafkaCluster cluster, + Map additionalProps) { + Properties properties = new Properties(); + SslPropertiesUtil.addKafkaSslProperties(cluster.getOriginalProperties().getSsl(), properties); + properties.putAll(cluster.getProperties()); + properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers()); + properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class); + properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class); + properties.putAll(additionalProps); + return new KafkaProducer<>(properties); } public Flux loadMessages(KafkaCluster cluster, String topic, - ConsumerPosition consumerPosition, String query, + ConsumerPosition consumerPosition, + @Nullable String query, MessageFilterTypeDTO filterQueryType, - int limit) { + @Nullable Integer pageSize, + SeekDirectionDTO seekDirection, + @Nullable String keySerde, + @Nullable String valueSerde) { return withExistingTopic(cluster, topic) .flux() - .flatMap(td -> loadMessagesImpl(cluster, topic, consumerPosition, query, filterQueryType, limit)); + .publishOn(Schedulers.boundedElastic()) + .flatMap(td -> loadMessagesImpl(cluster, topic, consumerPosition, query, + filterQueryType, fixPageSize(pageSize), seekDirection, keySerde, valueSerde)); } - private Flux loadMessagesImpl(KafkaCluster cluster, String topic, - ConsumerPosition consumerPosition, String query, - MessageFilterTypeDTO filterQueryType, - int limit) { + private int fixPageSize(@Nullable Integer pageSize) { + return Optional.ofNullable(pageSize) + .filter(ps -> ps > 0 && ps <= maxPageSize) + .orElse(defaultPageSize); + } - java.util.function.Consumer> emitter; - RecordSerDe recordDeserializer = - deserializationService.getRecordDeserializerForCluster(cluster); - if (consumerPosition.getSeekDirection().equals(SeekDirectionDTO.FORWARD)) { - emitter = new ForwardRecordEmitter( + private Flux loadMessagesImpl(KafkaCluster cluster, + String topic, + ConsumerPosition consumerPosition, + @Nullable String query, + MessageFilterTypeDTO filterQueryType, + int limit, + SeekDirectionDTO seekDirection, + @Nullable String keySerde, + @Nullable String valueSerde) { + + var deserializer = deserializationService.deserializerFor(cluster, topic, keySerde, valueSerde); + var filter = getMsgFilter(query, filterQueryType); + var emitter = switch (seekDirection) { + case FORWARD -> new ForwardEmitter( () -> consumerGroupService.createConsumer(cluster), - new OffsetsSeekForward(topic, consumerPosition), - recordDeserializer + consumerPosition, limit, deserializer, filter, cluster.getPollingSettings() ); - } else if (consumerPosition.getSeekDirection().equals(SeekDirectionDTO.BACKWARD)) { - emitter = new BackwardRecordEmitter( - (Map props) -> consumerGroupService.createConsumer(cluster, props), - new OffsetsSeekBackward(topic, consumerPosition, limit), - recordDeserializer + case BACKWARD -> new BackwardEmitter( + () -> consumerGroupService.createConsumer(cluster), + consumerPosition, limit, deserializer, filter, cluster.getPollingSettings() ); - } else { - emitter = new TailingEmitter( - recordDeserializer, + case TAILING -> new TailingEmitter( () -> consumerGroupService.createConsumer(cluster), - new OffsetsSeekForward(topic, consumerPosition) + consumerPosition, deserializer, filter, cluster.getPollingSettings() ); - } + }; return Flux.create(emitter) - .filter(getMsgFilter(query, filterQueryType)) - .takeWhile(createTakeWhilePredicate(consumerPosition, limit)) - .subscribeOn(Schedulers.boundedElastic()) - .share(); - } - - private Predicate createTakeWhilePredicate( - ConsumerPosition consumerPosition, int limit) { - return consumerPosition.getSeekDirection() == SeekDirectionDTO.TAILING - ? evt -> true // no limit for tailing - : new ResultSizeLimiter(limit); + .map(throttleUiPublish(seekDirection)); } - private Predicate getMsgFilter(String query, MessageFilterTypeDTO filterQueryType) { + private Predicate getMsgFilter(String query, + MessageFilterTypeDTO filterQueryType) { if (StringUtils.isEmpty(query)) { return evt -> true; } - filterQueryType = Optional.ofNullable(filterQueryType) - .orElse(MessageFilterTypeDTO.STRING_CONTAINS); - var messageFilter = MessageFilters.createMsgFilter(query, filterQueryType); - return evt -> { - // we only apply filter for message events - if (evt.getType() == TopicMessageEventDTO.TypeEnum.MESSAGE) { - return messageFilter.test(evt.getMessage()); - } - return true; - }; + return MessageFilters.createMsgFilter(query, filterQueryType); + } + + private UnaryOperator throttleUiPublish(SeekDirectionDTO seekDirection) { + if (seekDirection == SeekDirectionDTO.TAILING) { + RateLimiter rateLimiter = RateLimiter.create(TAILING_UI_MESSAGE_THROTTLE_RATE); + return m -> { + rateLimiter.acquire(1); + return m; + }; + } + // there is no need to throttle UI production rate for non-tailing modes, since max number of produced + // messages is limited for them (with page size) + return UnaryOperator.identity(); } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/MetricsCache.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/MetricsCache.java deleted file mode 100644 index c568ef9df0f..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/MetricsCache.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.provectus.kafka.ui.service; - -import com.provectus.kafka.ui.model.Feature; -import com.provectus.kafka.ui.model.InternalLogDirStats; -import com.provectus.kafka.ui.model.KafkaCluster; -import com.provectus.kafka.ui.model.ServerStatusDTO; -import com.provectus.kafka.ui.util.JmxClusterUtil; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import lombok.Builder; -import lombok.Value; -import org.apache.kafka.clients.admin.ConfigEntry; -import org.apache.kafka.clients.admin.TopicDescription; -import org.springframework.stereotype.Component; - -@Component -public class MetricsCache { - - @Value - @Builder(toBuilder = true) - public static class Metrics { - ServerStatusDTO status; - Throwable lastKafkaException; - String version; - List features; - ReactiveAdminClient.ClusterDescription clusterDescription; - JmxClusterUtil.JmxMetrics jmxMetrics; - InternalLogDirStats logDirInfo; - Map topicDescriptions; - Map> topicConfigs; - - public static Metrics empty() { - return builder() - .status(ServerStatusDTO.OFFLINE) - .version("Unknown") - .features(List.of()) - .clusterDescription( - new ReactiveAdminClient.ClusterDescription(null, null, List.of(), Set.of())) - .jmxMetrics(JmxClusterUtil.JmxMetrics.empty()) - .logDirInfo(InternalLogDirStats.empty()) - .topicDescriptions(Map.of()) - .topicConfigs(Map.of()) - .build(); - } - } - - private final Map cache = new ConcurrentHashMap<>(); - - public MetricsCache(ClustersStorage clustersStorage) { - var initializing = Metrics.empty().toBuilder().status(ServerStatusDTO.INITIALIZING).build(); - clustersStorage.getKafkaClusters().forEach(c -> cache.put(c.getName(), initializing)); - } - - public synchronized void replace(KafkaCluster c, Metrics stats) { - cache.put(c.getName(), stats); - } - - public synchronized void update(KafkaCluster c, - Map descriptions, - Map> configs) { - var metrics = get(c); - var updatedDescriptions = new HashMap<>(metrics.getTopicDescriptions()); - updatedDescriptions.putAll(descriptions); - var updatedConfigs = new HashMap<>(metrics.getTopicConfigs()); - updatedConfigs.putAll(configs); - replace( - c, - metrics.toBuilder() - .topicDescriptions(updatedDescriptions) - .topicConfigs(updatedConfigs) - .build() - ); - } - - public synchronized void onTopicDelete(KafkaCluster c, String topic) { - var metrics = get(c); - var updatedDescriptions = new HashMap<>(metrics.getTopicDescriptions()); - updatedDescriptions.remove(topic); - var updatedConfigs = new HashMap<>(metrics.getTopicConfigs()); - updatedConfigs.remove(topic); - replace( - c, - metrics.toBuilder() - .topicDescriptions(updatedDescriptions) - .topicConfigs(updatedConfigs) - .build() - ); - } - - public Metrics get(KafkaCluster c) { - return Objects.requireNonNull(cache.get(c.getName()), "Unknown cluster metrics requested"); - } - -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/MetricsService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/MetricsService.java deleted file mode 100644 index ac58ba8b773..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/MetricsService.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.provectus.kafka.ui.service; - -import com.provectus.kafka.ui.model.Feature; -import com.provectus.kafka.ui.model.InternalLogDirStats; -import com.provectus.kafka.ui.model.KafkaCluster; -import com.provectus.kafka.ui.model.ServerStatusDTO; -import com.provectus.kafka.ui.util.JmxClusterUtil; -import java.util.List; -import java.util.Map; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.kafka.clients.admin.ConfigEntry; -import org.apache.kafka.clients.admin.TopicDescription; -import org.springframework.stereotype.Service; -import reactor.core.publisher.Mono; - -@Service -@RequiredArgsConstructor -@Slf4j -public class MetricsService { - - private final JmxClusterUtil jmxClusterUtil; - private final AdminClientService adminClientService; - private final FeatureService featureService; - private final MetricsCache cache; - - public Mono updateCache(KafkaCluster c) { - return getMetrics(c).doOnSuccess(m -> cache.replace(c, m)); - } - - private Mono getMetrics(KafkaCluster cluster) { - return adminClientService.get(cluster).flatMap(ac -> - ac.describeCluster().flatMap(description -> - Mono.zip( - List.of( - jmxClusterUtil.getBrokerMetrics(cluster, description.getNodes()), - getLogDirInfo(cluster, ac), - featureService.getAvailableFeatures(cluster, description.getController()), - loadTopicConfigs(cluster), - describeTopics(cluster)), - results -> - MetricsCache.Metrics.builder() - .status(ServerStatusDTO.ONLINE) - .clusterDescription(description) - .version(ac.getVersion()) - .jmxMetrics((JmxClusterUtil.JmxMetrics) results[0]) - .logDirInfo((InternalLogDirStats) results[1]) - .features((List) results[2]) - .topicConfigs((Map>) results[3]) - .topicDescriptions((Map) results[4]) - .build() - ))) - .doOnError(e -> - log.error("Failed to collect cluster {} info", cluster.getName(), e)) - .onErrorResume( - e -> Mono.just(MetricsCache.Metrics.empty().toBuilder().lastKafkaException(e).build())); - } - - private Mono getLogDirInfo(KafkaCluster cluster, ReactiveAdminClient c) { - if (!cluster.isDisableLogDirsCollection()) { - return c.describeLogDirs().map(InternalLogDirStats::new); - } - return Mono.just(InternalLogDirStats.empty()); - } - - private Mono> describeTopics(KafkaCluster c) { - return adminClientService.get(c).flatMap(ReactiveAdminClient::describeTopics); - } - - private Mono>> loadTopicConfigs(KafkaCluster c) { - return adminClientService.get(c).flatMap(ReactiveAdminClient::getTopicsConfig); - } - -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/OffsetsResetService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/OffsetsResetService.java index b2675d51be8..67fc268d428 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/OffsetsResetService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/OffsetsResetService.java @@ -47,11 +47,12 @@ private Mono> offsets(ReactiveAdminClient client, @Nullable Collection partitions, OffsetSpec spec) { if (partitions == null) { - return client.listOffsets(topic, spec); + return client.listTopicOffsets(topic, spec, true); } return client.listOffsets( partitions.stream().map(idx -> new TopicPartition(topic, idx)).collect(toSet()), - spec + spec, + true ); } @@ -84,9 +85,9 @@ public Mono resetToOffsets( .collect(toMap(e -> new TopicPartition(topic, e.getKey()), Map.Entry::getValue)); return checkGroupCondition(cluster, group).flatMap( ac -> - ac.listOffsets(partitionOffsets.keySet(), OffsetSpec.earliest()) + ac.listOffsets(partitionOffsets.keySet(), OffsetSpec.earliest(), true) .flatMap(earliest -> - ac.listOffsets(partitionOffsets.keySet(), OffsetSpec.latest()) + ac.listOffsets(partitionOffsets.keySet(), OffsetSpec.latest(), true) .map(latest -> editOffsetsBounds(partitionOffsets, earliest, latest)) .flatMap(offsetsToCommit -> resetOffsets(ac, group, offsetsToCommit))) ); @@ -97,7 +98,7 @@ private Mono checkGroupCondition(KafkaCluster cluster, Stri .flatMap(ac -> // we need to call listConsumerGroups() to check group existence, because // describeConsumerGroups() will return consumer group even if it doesn't exist - ac.listConsumerGroups() + ac.listConsumerGroupNames() .filter(cgs -> cgs.stream().anyMatch(g -> g.equals(groupId))) .flatMap(cgs -> ac.describeConsumerGroups(List.of(groupId))) .filter(cgs -> cgs.containsKey(groupId)) diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ReactiveAdminClient.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ReactiveAdminClient.java index dedd609743c..6defb074237 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ReactiveAdminClient.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ReactiveAdminClient.java @@ -1,27 +1,43 @@ package com.provectus.kafka.ui.service; -import static com.google.common.util.concurrent.Uninterruptibles.getUninterruptibly; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; +import static org.apache.kafka.clients.admin.ListOffsetsResult.ListOffsetsResultInfo; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableTable; +import com.google.common.collect.Iterables; +import com.google.common.collect.Table; import com.provectus.kafka.ui.exception.IllegalEntityStateException; import com.provectus.kafka.ui.exception.NotFoundException; -import com.provectus.kafka.ui.util.MapUtil; -import com.provectus.kafka.ui.util.NumberUtil; +import com.provectus.kafka.ui.exception.ValidationException; +import com.provectus.kafka.ui.util.KafkaVersion; +import com.provectus.kafka.ui.util.annotation.KafkaClientInternalsDependant; import java.io.Closeable; +import java.time.Duration; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; import javax.annotation.Nullable; -import lombok.RequiredArgsConstructor; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; import lombok.Value; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.admin.AdminClient; @@ -30,13 +46,17 @@ import org.apache.kafka.clients.admin.ConfigEntry; import org.apache.kafka.clients.admin.ConsumerGroupDescription; import org.apache.kafka.clients.admin.ConsumerGroupListing; +import org.apache.kafka.clients.admin.DescribeClusterOptions; +import org.apache.kafka.clients.admin.DescribeClusterResult; import org.apache.kafka.clients.admin.DescribeConfigsOptions; -import org.apache.kafka.clients.admin.ListConsumerGroupOffsetsOptions; +import org.apache.kafka.clients.admin.ListConsumerGroupOffsetsSpec; +import org.apache.kafka.clients.admin.ListOffsetsResult; import org.apache.kafka.clients.admin.ListTopicsOptions; import org.apache.kafka.clients.admin.NewPartitionReassignment; import org.apache.kafka.clients.admin.NewPartitions; import org.apache.kafka.clients.admin.NewTopic; import org.apache.kafka.clients.admin.OffsetSpec; +import org.apache.kafka.clients.admin.ProducerState; import org.apache.kafka.clients.admin.RecordsToDelete; import org.apache.kafka.clients.admin.TopicDescription; import org.apache.kafka.clients.consumer.OffsetAndMetadata; @@ -44,13 +64,24 @@ import org.apache.kafka.common.KafkaFuture; import org.apache.kafka.common.Node; import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.common.TopicPartitionInfo; import org.apache.kafka.common.TopicPartitionReplica; +import org.apache.kafka.common.acl.AccessControlEntryFilter; +import org.apache.kafka.common.acl.AclBinding; +import org.apache.kafka.common.acl.AclBindingFilter; import org.apache.kafka.common.acl.AclOperation; import org.apache.kafka.common.config.ConfigResource; +import org.apache.kafka.common.errors.ClusterAuthorizationException; import org.apache.kafka.common.errors.GroupIdNotFoundException; import org.apache.kafka.common.errors.GroupNotEmptyException; +import org.apache.kafka.common.errors.InvalidRequestException; +import org.apache.kafka.common.errors.SecurityDisabledException; +import org.apache.kafka.common.errors.TopicAuthorizationException; import org.apache.kafka.common.errors.UnknownTopicOrPartitionException; +import org.apache.kafka.common.errors.UnsupportedVersionException; import org.apache.kafka.common.requests.DescribeLogDirsResponse; +import org.apache.kafka.common.resource.ResourcePatternFilter; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import reactor.util.function.Tuple2; @@ -58,12 +89,33 @@ @Slf4j -@RequiredArgsConstructor +@AllArgsConstructor public class ReactiveAdminClient implements Closeable { - private enum SupportedFeature { - INCREMENTAL_ALTER_CONFIGS, - ALTER_CONFIGS + public enum SupportedFeature { + INCREMENTAL_ALTER_CONFIGS(2.3f), + CONFIG_DOCUMENTATION_RETRIEVAL(2.6f), + DESCRIBE_CLUSTER_INCLUDE_AUTHORIZED_OPERATIONS(2.3f), + AUTHORIZED_SECURITY_ENABLED(ReactiveAdminClient::isAuthorizedSecurityEnabled); + + private final BiFunction> predicate; + + SupportedFeature(BiFunction> predicate) { + this.predicate = predicate; + } + + SupportedFeature(float fromVersion) { + this.predicate = (admin, ver) -> Mono.just(ver != null && ver >= fromVersion); + } + + static Mono> forVersion(AdminClient ac, String kafkaVersionStr) { + @Nullable Float kafkaVersion = KafkaVersion.parse(kafkaVersionStr).orElse(null); + return Flux.fromArray(SupportedFeature.values()) + .flatMap(f -> f.predicate.apply(ac, kafkaVersion).map(enabled -> Tuples.of(f, enabled))) + .filter(Tuple2::getT2) + .map(Tuple2::getT1) + .collect(Collectors.toSet()); + } } @Value @@ -72,30 +124,75 @@ public static class ClusterDescription { Node controller; String clusterId; Collection nodes; + @Nullable // null, if ACL is disabled Set authorizedOperations; } + @Builder + private record ConfigRelatedInfo(String version, + Set features, + boolean topicDeletionIsAllowed) { + + static final Duration UPDATE_DURATION = Duration.of(1, ChronoUnit.HOURS); + + private static Mono extract(AdminClient ac) { + return ReactiveAdminClient.describeClusterImpl(ac, Set.of()) + .flatMap(desc -> { + // choosing node from which we will get configs (starting with controller) + var targetNodeId = Optional.ofNullable(desc.controller) + .map(Node::id) + .orElse(desc.getNodes().iterator().next().id()); + return loadBrokersConfig(ac, List.of(targetNodeId)) + .map(map -> map.isEmpty() ? List.of() : map.get(targetNodeId)) + .flatMap(configs -> { + String version = "1.0-UNKNOWN"; + boolean topicDeletionEnabled = true; + for (ConfigEntry entry : configs) { + if (entry.name().contains("inter.broker.protocol.version")) { + version = entry.value(); + } + if (entry.name().equals("delete.topic.enable")) { + topicDeletionEnabled = Boolean.parseBoolean(entry.value()); + } + } + final String finalVersion = version; + final boolean finalTopicDeletionEnabled = topicDeletionEnabled; + return SupportedFeature.forVersion(ac, version) + .map(features -> new ConfigRelatedInfo(finalVersion, features, finalTopicDeletionEnabled)); + }); + }) + .cache(UPDATE_DURATION); + } + } + public static Mono create(AdminClient adminClient) { - return getClusterVersionImpl(adminClient) - .map(ver -> - new ReactiveAdminClient( - adminClient, - ver, - Set.of(getSupportedUpdateFeatureForVersion(ver)))); + Mono configRelatedInfoMono = ConfigRelatedInfo.extract(adminClient); + return configRelatedInfoMono.map(info -> new ReactiveAdminClient(adminClient, configRelatedInfoMono, info)); } - private static SupportedFeature getSupportedUpdateFeatureForVersion(String versionStr) { - float version = NumberUtil.parserClusterVersion(versionStr); - return version <= 2.3f - ? SupportedFeature.ALTER_CONFIGS - : SupportedFeature.INCREMENTAL_ALTER_CONFIGS; + + private static Mono isAuthorizedSecurityEnabled(AdminClient ac, @Nullable Float kafkaVersion) { + return toMono(ac.describeAcls(AclBindingFilter.ANY).values()) + .thenReturn(true) + .doOnError(th -> !(th instanceof SecurityDisabledException) + && !(th instanceof InvalidRequestException) + && !(th instanceof UnsupportedVersionException), + th -> log.debug("Error checking if security enabled", th)) + .onErrorReturn(false); } - //TODO: discuss - maybe we should map kafka-library's exceptions to our exceptions here - private static Mono toMono(KafkaFuture future) { + // NOTE: if KafkaFuture returns null, that Mono will be empty(!), since Reactor does not support nullable results + // (see MonoSink.success(..) javadoc for details) + public static Mono toMono(KafkaFuture future) { return Mono.create(sink -> future.whenComplete((res, ex) -> { if (ex != null) { - sink.error(ex); + // KafkaFuture doc is unclear about what exception wrapper will be used + // (from docs it should be ExecutionException, be we actually see CompletionException, so checking both + if (ex instanceof CompletionException || ex instanceof ExecutionException) { + sink.error(ex.getCause()); //unwrapping exception + } else { + sink.error(ex); + } } else { sink.success(res); } @@ -110,9 +207,15 @@ private static Mono toMono(KafkaFuture future) { //--------------------------------------------------------------------------------- + @Getter(AccessLevel.PACKAGE) // visible for testing private final AdminClient client; - private final String version; - private final Set features; + private final Mono configRelatedInfoMono; + + private volatile ConfigRelatedInfo configRelatedInfo; + + public Set getClusterFeatures() { + return configRelatedInfo.features(); + } public Mono> listTopics(boolean listInternal) { return toMono(client.listTopics(new ListTopicsOptions().listInternal(listInternal)).names()); @@ -123,14 +226,40 @@ public Mono deleteTopic(String topicName) { } public String getVersion() { - return version; + return configRelatedInfo.version(); + } + + public boolean isTopicDeletionEnabled() { + return configRelatedInfo.topicDeletionIsAllowed(); + } + + public Mono updateInternalStats(@Nullable Node controller) { + if (controller == null) { + return Mono.empty(); + } + return configRelatedInfoMono + .doOnNext(info -> this.configRelatedInfo = info) + .then(); } public Mono>> getTopicsConfig() { - return listTopics(true).flatMap(this::getTopicsConfig); + return listTopics(true).flatMap(topics -> getTopicsConfig(topics, false)); + } + + //NOTE: skips not-found topics (for which UnknownTopicOrPartitionException was thrown by AdminClient) + //and topics for which DESCRIBE_CONFIGS permission is not set (TopicAuthorizationException was thrown) + public Mono>> getTopicsConfig(Collection topicNames, boolean includeDoc) { + var includeDocFixed = includeDoc && getClusterFeatures().contains(SupportedFeature.CONFIG_DOCUMENTATION_RETRIEVAL); + // we need to partition calls, because it can lead to AdminClient timeouts in case of large topics count + return partitionCalls( + topicNames, + 200, + part -> getTopicsConfigImpl(part, includeDocFixed), + mapMerger() + ); } - public Mono>> getTopicsConfig(Collection topicNames) { + private Mono>> getTopicsConfigImpl(Collection topicNames, boolean includeDoc) { List resources = topicNames.stream() .map(topicName -> new ConfigResource(ConfigResource.Type.TOPIC, topicName)) .collect(toList()); @@ -138,38 +267,77 @@ public Mono>> getTopicsConfig(Collection t return toMonoWithExceptionFilter( client.describeConfigs( resources, - new DescribeConfigsOptions().includeSynonyms(true)).values(), - UnknownTopicOrPartitionException.class + new DescribeConfigsOptions().includeSynonyms(true).includeDocumentation(includeDoc)).values(), + UnknownTopicOrPartitionException.class, + TopicAuthorizationException.class ).map(config -> config.entrySet().stream() .collect(toMap( c -> c.getKey().name(), c -> List.copyOf(c.getValue().entries())))); } - public Mono>> loadBrokersConfig(List brokerIds) { + private static Mono>> loadBrokersConfig(AdminClient client, List brokerIds) { List resources = brokerIds.stream() .map(brokerId -> new ConfigResource(ConfigResource.Type.BROKER, Integer.toString(brokerId))) .collect(toList()); return toMono(client.describeConfigs(resources).all()) + // some kafka backends don't support broker's configs retrieval, + // and throw various exceptions on describeConfigs() call + .onErrorResume(th -> th instanceof InvalidRequestException // MSK Serverless + || th instanceof UnknownTopicOrPartitionException, // Azure event hub + th -> { + log.trace("Error while getting configs for brokers {}", brokerIds, th); + return Mono.just(Map.of()); + }) + // there are situations when kafka-ui user has no DESCRIBE_CONFIGS permission on cluster + .onErrorResume(ClusterAuthorizationException.class, th -> { + log.trace("AuthorizationException while getting configs for brokers {}", brokerIds, th); + return Mono.just(Map.of()); + }) + // catching all remaining exceptions, but logging on WARN level + .onErrorResume(th -> true, th -> { + log.warn("Unexpected error while getting configs for brokers {}", brokerIds, th); + return Mono.just(Map.of()); + }) .map(config -> config.entrySet().stream() .collect(toMap( c -> Integer.valueOf(c.getKey().name()), c -> new ArrayList<>(c.getValue().entries())))); } + /** + * Return per-broker configs or empty map if broker's configs retrieval not supported. + */ + public Mono>> loadBrokersConfig(List brokerIds) { + return loadBrokersConfig(client, brokerIds); + } + public Mono> describeTopics() { return listTopics(true).flatMap(this::describeTopics); } public Mono> describeTopics(Collection topics) { + // we need to partition calls, because it can lead to AdminClient timeouts in case of large topics count + return partitionCalls( + topics, + 200, + this::describeTopicsImpl, + mapMerger() + ); + } + + private Mono> describeTopicsImpl(Collection topics) { return toMonoWithExceptionFilter( - client.describeTopics(topics).values(), - UnknownTopicOrPartitionException.class + client.describeTopics(topics).topicNameValues(), + UnknownTopicOrPartitionException.class, + // we only describe topics that we see from listTopics() API, so we should have permission to do it, + // but also adding this exception here for rare case when access restricted after we called listTopics() + TopicAuthorizationException.class ); } /** - * Returns TopicDescription mono, or Empty Mono if topic not found. + * Returns TopicDescription mono, or Empty Mono if topic not visible. */ public Mono describeTopic(String topic) { return describeTopics(List.of(topic)).flatMap(m -> Mono.justOrEmpty(m.get(topic))); @@ -183,37 +351,33 @@ public Mono describeTopic(String topic) { * such topics in resulting map. *

* This method converts input map into Mono[Map] ignoring keys for which KafkaFutures - * finished with clazz exception. + * finished with classes exceptions and empty Monos. */ - private Mono> toMonoWithExceptionFilter(Map> values, - Class clazz) { + @SafeVarargs + static Mono> toMonoWithExceptionFilter(Map> values, + Class... classes) { if (values.isEmpty()) { return Mono.just(Map.of()); } - List>> monos = values.entrySet().stream() - .map(e -> toMono(e.getValue()).map(r -> Tuples.of(e.getKey(), r))) - .collect(toList()); - - return Mono.create(sink -> { - var finishedCnt = new AtomicInteger(); - var results = new ConcurrentHashMap(); - monos.forEach(mono -> mono.subscribe( - r -> { - results.put(r.getT1(), r.getT2()); - if (finishedCnt.incrementAndGet() == monos.size()) { - sink.success(results); - } - }, - th -> { - if (!th.getClass().isAssignableFrom(clazz)) { - sink.error(th); - } else if (finishedCnt.incrementAndGet() == monos.size()) { - sink.success(results); - } - } - )); - }); + List>>> monos = values.entrySet().stream() + .map(e -> + toMono(e.getValue()) + .map(r -> Tuples.of(e.getKey(), Optional.of(r))) + .defaultIfEmpty(Tuples.of(e.getKey(), Optional.empty())) //tracking empty Monos + .onErrorResume( + // tracking Monos with suppressible error + th -> Stream.of(classes).anyMatch(clazz -> th.getClass().isAssignableFrom(clazz)), + th -> Mono.just(Tuples.of(e.getKey(), Optional.empty())))) + .toList(); + + return Mono.zip( + monos, + resultsArr -> Stream.of(resultsArr) + .map(obj -> (Tuple2>) obj) + .filter(t -> t.getT2().isPresent()) //skipping empty & suppressible-errors + .collect(Collectors.toMap(Tuple2::getT1, t -> t.getT2().get())) + ); } public Mono>> describeLogDirs() { @@ -224,46 +388,36 @@ public Mono>> descr public Mono>> describeLogDirs( Collection brokerIds) { - return toMono(client.describeLogDirs(brokerIds).all()); + return toMono(client.describeLogDirs(brokerIds).all()) + .onErrorResume(UnsupportedVersionException.class, th -> Mono.just(Map.of())) + .onErrorResume(ClusterAuthorizationException.class, th -> Mono.just(Map.of())) + .onErrorResume(th -> true, th -> { + log.warn("Error while calling describeLogDirs", th); + return Mono.just(Map.of()); + }); } public Mono describeCluster() { - var r = client.describeCluster(); - var all = KafkaFuture.allOf(r.nodes(), r.clusterId(), r.controller(), r.authorizedOperations()); - return Mono.create(sink -> all.whenComplete((res, ex) -> { - if (ex != null) { - sink.error(ex); - } else { - try { - sink.success( - new ClusterDescription( - getUninterruptibly(r.controller()), - getUninterruptibly(r.clusterId()), - getUninterruptibly(r.nodes()), - getUninterruptibly(r.authorizedOperations()) - ) - ); - } catch (ExecutionException e) { - // can't be here, because all futures already completed - } - } - })); - } - - private static Mono getClusterVersionImpl(AdminClient client) { - return toMono(client.describeCluster().controller()).flatMap(controller -> - toMono(client.describeConfigs( - List.of(new ConfigResource( - ConfigResource.Type.BROKER, String.valueOf(controller.id())))) - .all() - .thenApply(configs -> - configs.values().stream() - .map(Config::entries) - .flatMap(Collection::stream) - .filter(entry -> entry.name().contains("inter.broker.protocol.version")) - .findFirst().map(ConfigEntry::value) - .orElse("1.0-UNKNOWN") - ))); + return describeClusterImpl(client, getClusterFeatures()); + } + + private static Mono describeClusterImpl(AdminClient client, Set features) { + boolean includeAuthorizedOperations = + features.contains(SupportedFeature.DESCRIBE_CLUSTER_INCLUDE_AUTHORIZED_OPERATIONS); + DescribeClusterResult result = client.describeCluster( + new DescribeClusterOptions().includeAuthorizedOperations(includeAuthorizedOperations)); + var allOfFuture = KafkaFuture.allOf( + result.controller(), result.clusterId(), result.nodes(), result.authorizedOperations()); + return toMono(allOfFuture).then( + Mono.fromCallable(() -> + new ClusterDescription( + result.controller().get(), + result.clusterId().get(), + result.nodes().get(), + result.authorizedOperations().get() + ) + ) + ); } public Mono deleteConsumerGroups(Collection groupIds) { @@ -276,10 +430,14 @@ public Mono deleteConsumerGroups(Collection groupIds) { public Mono createTopic(String name, int numPartitions, - short replicationFactor, + @Nullable Integer replicationFactor, Map configs) { - return toMono(client.createTopics( - List.of(new NewTopic(name, numPartitions, replicationFactor).configs(configs))).all()); + var newTopic = new NewTopic( + name, + Optional.of(numPartitions), + Optional.ofNullable(replicationFactor).map(Integer::shortValue) + ).configs(configs); + return toMono(client.createTopics(List.of(newTopic)).all()); } public Mono alterPartitionReassignments( @@ -291,40 +449,70 @@ public Mono createPartitions(Map newPartitionsMap) return toMono(client.createPartitions(newPartitionsMap).all()); } + + // NOTE: places whole current topic config with new one. Entries that were present in old config, + // but missed in new will be set to default public Mono updateTopicConfig(String topicName, Map configs) { - if (features.contains(SupportedFeature.INCREMENTAL_ALTER_CONFIGS)) { - return incrementalAlterConfig(topicName, configs); + if (getClusterFeatures().contains(SupportedFeature.INCREMENTAL_ALTER_CONFIGS)) { + return getTopicsConfigImpl(List.of(topicName), false) + .map(conf -> conf.getOrDefault(topicName, List.of())) + .flatMap(currentConfigs -> incrementalAlterConfig(topicName, currentConfigs, configs)); } else { return alterConfig(topicName, configs); } } - public Mono> listConsumerGroups() { - return toMono(client.listConsumerGroups().all()) - .map(lst -> lst.stream().map(ConsumerGroupListing::groupId).collect(toList())); + public Mono> listConsumerGroupNames() { + return listConsumerGroups().map(lst -> lst.stream().map(ConsumerGroupListing::groupId).toList()); } - public Mono> describeConsumerGroups(List groupIds) { - return toMono(client.describeConsumerGroups(groupIds).all()); + public Mono> listConsumerGroups() { + return toMono(client.listConsumerGroups().all()); } - public Mono> listConsumerGroupOffsets(String groupId) { - return listConsumerGroupOffsets(groupId, new ListConsumerGroupOffsetsOptions()); + public Mono> describeConsumerGroups(Collection groupIds) { + return partitionCalls( + groupIds, + 25, + 4, + ids -> toMono(client.describeConsumerGroups(ids).all()), + mapMerger() + ); } - public Mono> listConsumerGroupOffsets( - String groupId, List partitions) { - return listConsumerGroupOffsets(groupId, - new ListConsumerGroupOffsetsOptions().topicPartitions(partitions)); - } + // group -> partition -> offset + // NOTE: partitions with no committed offsets will be skipped + public Mono> listConsumerGroupOffsets(List consumerGroups, + // all partitions if null passed + @Nullable List partitions) { + Function, Mono>>> call = + groups -> toMono( + client.listConsumerGroupOffsets( + groups.stream() + .collect(Collectors.toMap( + g -> g, + g -> new ListConsumerGroupOffsetsSpec().topicPartitions(partitions) + ))).all() + ); - private Mono> listConsumerGroupOffsets( - String groupId, ListConsumerGroupOffsetsOptions options) { - return toMono(client.listConsumerGroupOffsets(groupId, options).partitionsToOffsetAndMetadata()) - .map(MapUtil::removeNullValues) - .map(m -> m.entrySet().stream() - .map(e -> Tuples.of(e.getKey(), e.getValue().offset())) - .collect(Collectors.toMap(Tuple2::getT1, Tuple2::getT2))); + Mono>> merged = partitionCalls( + consumerGroups, + 25, + 4, + call, + mapMerger() + ); + + return merged.map(map -> { + var table = ImmutableTable.builder(); + map.forEach((g, tpOffsets) -> tpOffsets.forEach((tp, offset) -> { + if (offset != null) { + // offset will be null for partitions that don't have committed offset for this group + table.put(g, tp, offset.offset()); + } + })); + return table.build(); + }); } public Mono alterConsumerGroupOffsets(String groupId, Map offsets) { @@ -335,31 +523,124 @@ public Mono alterConsumerGroupOffsets(String groupId, Map> listOffsets(String topic, - OffsetSpec offsetSpec) { - return topicPartitions(topic).flatMap(tps -> listOffsets(tps, offsetSpec)); + /** + * List offset for the topic's partitions and OffsetSpec. + * + * @param failOnUnknownLeader true - throw exception in case of no-leader partitions, + * false - skip partitions with no leader + */ + public Mono> listTopicOffsets(String topic, + OffsetSpec offsetSpec, + boolean failOnUnknownLeader) { + return describeTopic(topic) + .map(td -> filterPartitionsWithLeaderCheck(List.of(td), p -> true, failOnUnknownLeader)) + .flatMap(partitions -> listOffsetsUnsafe(partitions, offsetSpec)); } + /** + * List offset for the specified partitions and OffsetSpec. + * + * @param failOnUnknownLeader true - throw exception in case of no-leader partitions, + * false - skip partitions with no leader + */ public Mono> listOffsets(Collection partitions, + OffsetSpec offsetSpec, + boolean failOnUnknownLeader) { + return filterPartitionsWithLeaderCheck(partitions, failOnUnknownLeader) + .flatMap(parts -> listOffsetsUnsafe(parts, offsetSpec)); + } + + /** + * List offset for the specified topics, skipping no-leader partitions. + */ + public Mono> listOffsets(Collection topicDescriptions, OffsetSpec offsetSpec) { - return toMono( - client.listOffsets(partitions.stream().collect(toMap(tp -> tp, tp -> offsetSpec))).all()) - .map(offsets -> offsets.entrySet() - .stream() - // filtering partitions for which offsets were not found - .filter(e -> e.getValue().offset() >= 0) - .collect(toMap(Map.Entry::getKey, e -> e.getValue().offset()))); - } - - private Mono> topicPartitions(String topic) { - return toMono(client.describeTopics(List.of(topic)).all()) - .map(r -> r.values().stream() - .findFirst() - .stream() - .flatMap(d -> d.partitions().stream()) - .map(p -> new TopicPartition(topic, p.partition())) - .collect(Collectors.toSet()) - ); + return listOffsetsUnsafe(filterPartitionsWithLeaderCheck(topicDescriptions, p -> true, false), offsetSpec); + } + + private Mono> filterPartitionsWithLeaderCheck(Collection partitions, + boolean failOnUnknownLeader) { + var targetTopics = partitions.stream().map(TopicPartition::topic).collect(Collectors.toSet()); + return describeTopicsImpl(targetTopics) + .map(descriptions -> + filterPartitionsWithLeaderCheck( + descriptions.values(), partitions::contains, failOnUnknownLeader)); + } + + @VisibleForTesting + static Set filterPartitionsWithLeaderCheck(Collection topicDescriptions, + Predicate partitionPredicate, + boolean failOnUnknownLeader) { + var goodPartitions = new HashSet(); + for (TopicDescription description : topicDescriptions) { + var goodTopicPartitions = new ArrayList(); + for (TopicPartitionInfo partitionInfo : description.partitions()) { + TopicPartition topicPartition = new TopicPartition(description.name(), partitionInfo.partition()); + if (partitionInfo.leader() == null) { + if (failOnUnknownLeader) { + throw new ValidationException(String.format("Topic partition %s has no leader", topicPartition)); + } else { + // if ANY of topic partitions has no leader - we have to skip all topic partitions + goodTopicPartitions.clear(); + break; + } + } + if (partitionPredicate.test(topicPartition)) { + goodTopicPartitions.add(topicPartition); + } + } + goodPartitions.addAll(goodTopicPartitions); + } + return goodPartitions; + } + + // 1. NOTE(!): should only apply for partitions from topics where all partitions have leaders, + // otherwise AdminClient will try to fetch topic metadata, fail and retry infinitely (until timeout) + // 2. NOTE(!): Skips partitions that were not initialized yet + // (UnknownTopicOrPartitionException thrown, ex. after topic creation) + // 3. TODO: check if it is a bug that AdminClient never throws LeaderNotAvailableException and just retrying instead + @KafkaClientInternalsDependant + @VisibleForTesting + Mono> listOffsetsUnsafe(Collection partitions, OffsetSpec offsetSpec) { + if (partitions.isEmpty()) { + return Mono.just(Map.of()); + } + + Function, Mono>> call = + parts -> { + ListOffsetsResult r = client.listOffsets(parts.stream().collect(toMap(tp -> tp, tp -> offsetSpec))); + Map> perPartitionResults = new HashMap<>(); + parts.forEach(p -> perPartitionResults.put(p, r.partitionResult(p))); + + return toMonoWithExceptionFilter(perPartitionResults, UnknownTopicOrPartitionException.class) + .map(offsets -> offsets.entrySet().stream() + // filtering partitions for which offsets were not found + .filter(e -> e.getValue().offset() >= 0) + .collect(toMap(Map.Entry::getKey, e -> e.getValue().offset()))); + }; + + return partitionCalls( + partitions, + 200, + call, + mapMerger() + ); + } + + public Mono> listAcls(ResourcePatternFilter filter) { + Preconditions.checkArgument(getClusterFeatures().contains(SupportedFeature.AUTHORIZED_SECURITY_ENABLED)); + return toMono(client.describeAcls(new AclBindingFilter(filter, AccessControlEntryFilter.ANY)).values()); + } + + public Mono createAcls(Collection aclBindings) { + Preconditions.checkArgument(getClusterFeatures().contains(SupportedFeature.AUTHORIZED_SECURITY_ENABLED)); + return toMono(client.createAcls(aclBindings).all()); + } + + public Mono deleteAcls(Collection aclBindings) { + Preconditions.checkArgument(getClusterFeatures().contains(SupportedFeature.AUTHORIZED_SECURITY_ENABLED)); + var filters = aclBindings.stream().map(AclBinding::toFilter).collect(Collectors.toSet()); + return toMono(client.deleteAcls(filters).all()).then(); } public Mono updateBrokerConfigByName(Integer brokerId, String name, String value) { @@ -379,17 +660,37 @@ public Mono alterReplicaLogDirs(Map replica return toMono(client.alterReplicaLogDirs(replicaAssignment).all()); } - private Mono incrementalAlterConfig(String topicName, Map configs) { - var config = configs.entrySet().stream() - .flatMap(cfg -> Stream.of( - new AlterConfigOp( - new ConfigEntry( - cfg.getKey(), - cfg.getValue()), - AlterConfigOp.OpType.SET))) - .collect(toList()); - var topicResource = new ConfigResource(ConfigResource.Type.TOPIC, topicName); - return toMono(client.incrementalAlterConfigs(Map.of(topicResource, config)).all()); + // returns tp -> list of active producer's states (if any) + public Mono>> getActiveProducersState(String topic) { + return describeTopic(topic) + .map(td -> client.describeProducers( + IntStream.range(0, td.partitions().size()) + .mapToObj(i -> new TopicPartition(topic, i)) + .toList() + ).all() + ) + .flatMap(ReactiveAdminClient::toMono) + .map(map -> map.entrySet().stream() + .filter(e -> !e.getValue().activeProducers().isEmpty()) // skipping partitions without producers + .collect(toMap(Map.Entry::getKey, e -> e.getValue().activeProducers()))); + } + + private Mono incrementalAlterConfig(String topicName, + List currentConfigs, + Map newConfigs) { + var configsToDelete = currentConfigs.stream() + .filter(e -> e.source() == ConfigEntry.ConfigSource.DYNAMIC_TOPIC_CONFIG) //manually set configs only + .filter(e -> !newConfigs.containsKey(e.name())) + .map(e -> new AlterConfigOp(e, AlterConfigOp.OpType.DELETE)); + + var configsToSet = newConfigs.entrySet().stream() + .map(e -> new AlterConfigOp(new ConfigEntry(e.getKey(), e.getValue()), AlterConfigOp.OpType.SET)); + + return toMono(client.incrementalAlterConfigs( + Map.of( + new ConfigResource(ConfigResource.Type.TOPIC, topicName), + Stream.concat(configsToDelete, configsToSet).toList() + )).all()); } @SuppressWarnings("deprecation") @@ -402,6 +703,50 @@ private Mono alterConfig(String topicName, Map configs) { return toMono(client.alterConfigs(Map.of(topicResource, config)).all()); } + /** + * Splits input collection into batches, converts each batch into Mono, sequentially subscribes to them + * and merges output Monos into one Mono. + */ + private static Mono partitionCalls(Collection items, + int partitionSize, + Function, Mono> call, + BiFunction merger) { + if (items.isEmpty()) { + return call.apply(items); + } + Iterable> parts = Iterables.partition(items, partitionSize); + return Flux.fromIterable(parts) + .concatMap(call) + .reduce(merger); + } + + /** + * Splits input collection into batches, converts each batch into Mono, subscribes to them (concurrently, + * with specified concurrency level) and merges output Monos into one Mono. + */ + private static Mono partitionCalls(Collection items, + int partitionSize, + int concurrency, + Function, Mono> call, + BiFunction merger) { + if (items.isEmpty()) { + return call.apply(items); + } + Iterable> parts = Iterables.partition(items, partitionSize); + return Flux.fromIterable(parts) + .flatMap(call, concurrency) + .reduce(merger); + } + + private static BiFunction, Map, Map> mapMerger() { + return (m1, m2) -> { + var merged = new HashMap(); + merged.putAll(m1); + merged.putAll(m2); + return merged; + }; + } + @Override public void close() { client.close(); diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/SchemaRegistryService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/SchemaRegistryService.java index f4c9355804e..fd7efff606a 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/SchemaRegistryService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/SchemaRegistryService.java @@ -1,52 +1,30 @@ package com.provectus.kafka.ui.service; -import static org.springframework.http.HttpStatus.CONFLICT; -import static org.springframework.http.HttpStatus.NOT_FOUND; -import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; - +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.json.JsonMapper; import com.provectus.kafka.ui.exception.SchemaCompatibilityException; -import com.provectus.kafka.ui.exception.SchemaFailedToDeleteException; import com.provectus.kafka.ui.exception.SchemaNotFoundException; -import com.provectus.kafka.ui.exception.SchemaTypeNotSupportedException; -import com.provectus.kafka.ui.exception.UnprocessableEntityException; import com.provectus.kafka.ui.exception.ValidationException; -import com.provectus.kafka.ui.model.CompatibilityLevelDTO; -import com.provectus.kafka.ui.model.InternalSchemaRegistry; import com.provectus.kafka.ui.model.KafkaCluster; -import com.provectus.kafka.ui.model.NewSchemaSubjectDTO; -import com.provectus.kafka.ui.model.SchemaSubjectDTO; -import com.provectus.kafka.ui.model.SchemaTypeDTO; -import com.provectus.kafka.ui.model.schemaregistry.ErrorResponse; -import com.provectus.kafka.ui.model.schemaregistry.InternalCompatibilityCheck; -import com.provectus.kafka.ui.model.schemaregistry.InternalCompatibilityLevel; -import com.provectus.kafka.ui.model.schemaregistry.InternalNewSchema; -import com.provectus.kafka.ui.model.schemaregistry.SubjectIdResponse; -import java.io.IOException; -import java.net.URI; -import java.util.Collections; -import java.util.Formatter; +import com.provectus.kafka.ui.sr.api.KafkaSrClientApi; +import com.provectus.kafka.ui.sr.model.Compatibility; +import com.provectus.kafka.ui.sr.model.CompatibilityCheckResponse; +import com.provectus.kafka.ui.sr.model.CompatibilityConfig; +import com.provectus.kafka.ui.sr.model.CompatibilityLevelChange; +import com.provectus.kafka.ui.sr.model.NewSubject; +import com.provectus.kafka.ui.sr.model.SchemaSubject; +import com.provectus.kafka.ui.util.ReactiveFailover; +import java.nio.charset.Charset; import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Function; -import java.util.function.Supplier; import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.experimental.Delegate; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.stereotype.Service; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.reactive.function.BodyInserters; -import org.springframework.web.reactive.function.client.ClientResponse; -import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.reactive.function.client.WebClientRequestException; -import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -55,396 +33,141 @@ @RequiredArgsConstructor public class SchemaRegistryService { - public static final String NO_SUCH_SCHEMA_VERSION = "No such schema %s with version %s"; - public static final String NO_SUCH_SCHEMA = "No such schema %s"; - - private static final String URL_SUBJECTS = "/subjects"; - private static final String URL_SUBJECT = "/subjects/{schemaName}"; - private static final String URL_SUBJECT_VERSIONS = "/subjects/{schemaName}/versions"; - private static final String URL_SUBJECT_BY_VERSION = "/subjects/{schemaName}/versions/{version}"; private static final String LATEST = "latest"; - private static final String UNRECOGNIZED_FIELD_SCHEMA_TYPE = "Unrecognized field: schemaType"; - private static final String INCOMPATIBLE_WITH_AN_EARLIER_SCHEMA = "incompatible with an earlier schema"; + @AllArgsConstructor + public static class SubjectWithCompatibilityLevel { + @Delegate + SchemaSubject subject; + @Getter + Compatibility compatibility; + } - private final WebClient webClient; + private ReactiveFailover api(KafkaCluster cluster) { + return cluster.getSchemaRegistryClient(); + } - public Mono> getAllLatestVersionSchemas(KafkaCluster cluster, - List subjects) { + public Mono> getAllLatestVersionSchemas(KafkaCluster cluster, + List subjects) { return Flux.fromIterable(subjects) .concatMap(subject -> getLatestSchemaVersionBySubject(cluster, subject)) .collect(Collectors.toList()); } - public Mono getAllSubjectNames(KafkaCluster cluster) { - return configuredWebClient( - cluster, - HttpMethod.GET, - URL_SUBJECTS) - .retrieve() - .bodyToMono(String[].class) - .doOnError(e -> log.error("Unexpected error", e)) - .as(m -> failoverAble(m, - new FailoverMono<>(cluster.getSchemaRegistry(), () -> this.getAllSubjectNames(cluster)))); + public Mono> getAllSubjectNames(KafkaCluster cluster) { + return api(cluster) + .mono(c -> c.getAllSubjectNames(null, false)) + .flatMapIterable(this::parseSubjectListString) + .collectList(); } - public Flux getAllVersionsBySubject(KafkaCluster cluster, String subject) { + @SneakyThrows + private List parseSubjectListString(String subjectNamesStr) { + //workaround for https://github.com/spring-projects/spring-framework/issues/24734 + return new JsonMapper().readValue(subjectNamesStr, new TypeReference>() { + }); + } + + public Flux getAllVersionsBySubject(KafkaCluster cluster, String subject) { Flux versions = getSubjectVersions(cluster, subject); return versions.flatMap(version -> getSchemaSubjectByVersion(cluster, subject, version)); } private Flux getSubjectVersions(KafkaCluster cluster, String schemaName) { - return configuredWebClient( - cluster, - HttpMethod.GET, - URL_SUBJECT_VERSIONS, - schemaName) - .retrieve() - .onStatus(NOT_FOUND::equals, - throwIfNotFoundStatus(formatted(NO_SUCH_SCHEMA, schemaName))) - .bodyToFlux(Integer.class) - .as(f -> failoverAble(f, new FailoverFlux<>(cluster.getSchemaRegistry(), - () -> this.getSubjectVersions(cluster, schemaName)))); + return api(cluster).flux(c -> c.getSubjectVersions(schemaName)); } - public Mono getSchemaSubjectByVersion(KafkaCluster cluster, String schemaName, - Integer version) { - return this.getSchemaSubject(cluster, schemaName, String.valueOf(version)); + public Mono getSchemaSubjectByVersion(KafkaCluster cluster, + String schemaName, + Integer version) { + return getSchemaSubject(cluster, schemaName, String.valueOf(version)); } - public Mono getLatestSchemaVersionBySubject(KafkaCluster cluster, - String schemaName) { - return this.getSchemaSubject(cluster, schemaName, LATEST); + public Mono getLatestSchemaVersionBySubject(KafkaCluster cluster, + String schemaName) { + return getSchemaSubject(cluster, schemaName, LATEST); } - private Mono getSchemaSubject(KafkaCluster cluster, String schemaName, - String version) { - return configuredWebClient( - cluster, - HttpMethod.GET, - SchemaRegistryService.URL_SUBJECT_BY_VERSION, - List.of(schemaName, version)) - .retrieve() - .onStatus(NOT_FOUND::equals, - throwIfNotFoundStatus(formatted(NO_SUCH_SCHEMA_VERSION, schemaName, version)) - ) - .bodyToMono(SchemaSubjectDTO.class) - .map(this::withSchemaType) + private Mono getSchemaSubject(KafkaCluster cluster, String schemaName, + String version) { + return api(cluster) + .mono(c -> c.getSubjectVersion(schemaName, version, false)) .zipWith(getSchemaCompatibilityInfoOrGlobal(cluster, schemaName)) - .map(tuple -> { - SchemaSubjectDTO schema = tuple.getT1(); - String compatibilityLevel = tuple.getT2().getCompatibilityLevel(); - schema.setCompatibilityLevel(compatibilityLevel); - return schema; - }) - .as(m -> failoverAble(m, new FailoverMono<>(cluster.getSchemaRegistry(), - () -> this.getSchemaSubject(cluster, schemaName, version)))); - } - - /** - * If {@link SchemaSubjectDTO#getSchemaType()} is null, then AVRO, otherwise, - * adds the schema type as is. - */ - @NotNull - private SchemaSubjectDTO withSchemaType(SchemaSubjectDTO s) { - return s.schemaType(Optional.ofNullable(s.getSchemaType()).orElse(SchemaTypeDTO.AVRO)); + .map(t -> new SubjectWithCompatibilityLevel(t.getT1(), t.getT2())) + .onErrorResume(WebClientResponseException.NotFound.class, th -> Mono.error(new SchemaNotFoundException())); } - public Mono deleteSchemaSubjectByVersion(KafkaCluster cluster, - String schemaName, - Integer version) { - return this.deleteSchemaSubject(cluster, schemaName, String.valueOf(version)); + public Mono deleteSchemaSubjectByVersion(KafkaCluster cluster, String schemaName, Integer version) { + return deleteSchemaSubject(cluster, schemaName, String.valueOf(version)); } - public Mono deleteLatestSchemaSubject(KafkaCluster cluster, - String schemaName) { - return this.deleteSchemaSubject(cluster, schemaName, LATEST); + public Mono deleteLatestSchemaSubject(KafkaCluster cluster, String schemaName) { + return deleteSchemaSubject(cluster, schemaName, LATEST); } - private Mono deleteSchemaSubject(KafkaCluster cluster, String schemaName, - String version) { - return configuredWebClient( - cluster, - HttpMethod.DELETE, - SchemaRegistryService.URL_SUBJECT_BY_VERSION, - List.of(schemaName, version)) - .retrieve() - .onStatus(NOT_FOUND::equals, - throwIfNotFoundStatus(formatted(NO_SUCH_SCHEMA_VERSION, schemaName, version)) - ) - .toBodilessEntity() - .then() - .as(m -> failoverAble(m, new FailoverMono<>(cluster.getSchemaRegistry(), - () -> this.deleteSchemaSubject(cluster, schemaName, version)))); + private Mono deleteSchemaSubject(KafkaCluster cluster, String schemaName, String version) { + return api(cluster).mono(c -> c.deleteSubjectVersion(schemaName, version, false)); } - public Mono deleteSchemaSubjectEntirely(KafkaCluster cluster, - String schemaName) { - return configuredWebClient( - cluster, - HttpMethod.DELETE, - URL_SUBJECT, - schemaName) - .retrieve() - .onStatus(HttpStatus::isError, errorOnSchemaDeleteFailure(schemaName)) - .toBodilessEntity() - .then() - .as(m -> failoverAble(m, new FailoverMono<>(cluster.getSchemaRegistry(), - () -> this.deleteSchemaSubjectEntirely(cluster, schemaName)))); + public Mono deleteSchemaSubjectEntirely(KafkaCluster cluster, String schemaName) { + return api(cluster).mono(c -> c.deleteAllSubjectVersions(schemaName, false)); } /** * Checks whether the provided schema duplicates the previous or not, creates a new schema * and then returns the whole content by requesting its latest version. */ - public Mono registerNewSchema(KafkaCluster cluster, - Mono newSchemaSubject) { - return newSchemaSubject - .flatMap(schema -> { - SchemaTypeDTO schemaType = - SchemaTypeDTO.AVRO == schema.getSchemaType() ? null : schema.getSchemaType(); - Mono newSchema = - Mono.just(new InternalNewSchema(schema.getSchema(), schemaType)); - String subject = schema.getSubject(); - return submitNewSchema(subject, newSchema, cluster) - .flatMap(resp -> getLatestSchemaVersionBySubject(cluster, subject)); - }); - } - - @NotNull - private Mono submitNewSchema(String subject, - Mono newSchemaSubject, - KafkaCluster cluster) { - return configuredWebClient( - cluster, - HttpMethod.POST, - URL_SUBJECT_VERSIONS, subject) - .contentType(MediaType.APPLICATION_JSON) - .body(BodyInserters.fromPublisher(newSchemaSubject, InternalNewSchema.class)) - .retrieve() - .onStatus(status -> UNPROCESSABLE_ENTITY.equals(status) || CONFLICT.equals(status), - r -> r.bodyToMono(ErrorResponse.class) - .flatMap(this::getMonoError)) - .bodyToMono(SubjectIdResponse.class) - .as(m -> failoverAble(m, new FailoverMono<>(cluster.getSchemaRegistry(), - () -> submitNewSchema(subject, newSchemaSubject, cluster)))); - } - - @NotNull - private Mono getMonoError(ErrorResponse x) { - if (isUnrecognizedFieldSchemaTypeMessage(x.getMessage())) { - return Mono.error(new SchemaTypeNotSupportedException()); - } else if (isIncompatibleSchemaMessage(x.getMessage())) { - return Mono.error(new SchemaCompatibilityException(x.getMessage())); - } else { - return Mono.error(new UnprocessableEntityException(x.getMessage())); - } - } - - @NotNull - private Function> throwIfNotFoundStatus( - String formatted) { - return resp -> Mono.error(new SchemaNotFoundException(formatted)); - } - - /** - * Updates a compatibility level for a schemaName. - * - * @param schemaName is a schema subject name - * @see com.provectus.kafka.ui.model.CompatibilityLevelDTO.CompatibilityEnum - */ - public Mono updateSchemaCompatibility(KafkaCluster cluster, @Nullable String schemaName, - Mono compatibilityLevel) { - String configEndpoint = Objects.isNull(schemaName) ? "/config" : "/config/{schemaName}"; - return configuredWebClient( - cluster, - HttpMethod.PUT, - configEndpoint, - schemaName) - .contentType(MediaType.APPLICATION_JSON) - .body(BodyInserters.fromPublisher(compatibilityLevel, CompatibilityLevelDTO.class)) - .retrieve() - .onStatus(NOT_FOUND::equals, - throwIfNotFoundStatus(formatted(NO_SUCH_SCHEMA, schemaName))) - .bodyToMono(Void.class) - .as(m -> failoverAble(m, new FailoverMono<>(cluster.getSchemaRegistry(), - () -> this.updateSchemaCompatibility(cluster, schemaName, compatibilityLevel)))); + public Mono registerNewSchema(KafkaCluster cluster, + String subject, + NewSubject newSchemaSubject) { + return api(cluster) + .mono(c -> c.registerNewSchema(subject, newSchemaSubject)) + .onErrorMap(WebClientResponseException.Conflict.class, + th -> new SchemaCompatibilityException()) + .onErrorMap(WebClientResponseException.UnprocessableEntity.class, + th -> new ValidationException("Invalid schema. Error from registry: " + th.getResponseBodyAsString())) + .then(getLatestSchemaVersionBySubject(cluster, subject)); } public Mono updateSchemaCompatibility(KafkaCluster cluster, - Mono compatibilityLevel) { - return updateSchemaCompatibility(cluster, null, compatibilityLevel); - } - - public Mono getSchemaCompatibilityLevel(KafkaCluster cluster, - String schemaName) { - String globalConfig = Objects.isNull(schemaName) ? "/config" : "/config/{schemaName}"; - final var values = new LinkedMultiValueMap(); - values.add("defaultToGlobal", "true"); - return configuredWebClient( - cluster, - HttpMethod.GET, - globalConfig, - (schemaName == null ? Collections.emptyList() : List.of(schemaName)), - values) - .retrieve() - .bodyToMono(InternalCompatibilityLevel.class) + String schemaName, + Compatibility compatibility) { + return api(cluster) + .mono(c -> c.updateSubjectCompatibilityLevel( + schemaName, new CompatibilityLevelChange().compatibility(compatibility))) + .then(); + } + + public Mono updateGlobalSchemaCompatibility(KafkaCluster cluster, + Compatibility compatibility) { + return api(cluster) + .mono(c -> c.updateGlobalCompatibilityLevel(new CompatibilityLevelChange().compatibility(compatibility))) + .then(); + } + + public Mono getSchemaCompatibilityLevel(KafkaCluster cluster, + String schemaName) { + return api(cluster) + .mono(c -> c.getSubjectCompatibilityLevel(schemaName, true)) + .map(CompatibilityConfig::getCompatibilityLevel) .onErrorResume(error -> Mono.empty()); } - public Mono getGlobalSchemaCompatibilityLevel(KafkaCluster cluster) { - return this.getSchemaCompatibilityLevel(cluster, null); + public Mono getGlobalSchemaCompatibilityLevel(KafkaCluster cluster) { + return api(cluster) + .mono(KafkaSrClientApi::getGlobalCompatibilityLevel) + .map(CompatibilityConfig::getCompatibilityLevel); } - private Mono getSchemaCompatibilityInfoOrGlobal(KafkaCluster cluster, - String schemaName) { - return this.getSchemaCompatibilityLevel(cluster, schemaName) + private Mono getSchemaCompatibilityInfoOrGlobal(KafkaCluster cluster, + String schemaName) { + return getSchemaCompatibilityLevel(cluster, schemaName) .switchIfEmpty(this.getGlobalSchemaCompatibilityLevel(cluster)); } - public Mono checksSchemaCompatibility( - KafkaCluster cluster, String schemaName, Mono newSchemaSubject) { - return configuredWebClient( - cluster, - HttpMethod.POST, - "/compatibility/subjects/{schemaName}/versions/latest", - schemaName) - .contentType(MediaType.APPLICATION_JSON) - .body(BodyInserters.fromPublisher(newSchemaSubject, NewSchemaSubjectDTO.class)) - .retrieve() - .onStatus(NOT_FOUND::equals, - throwIfNotFoundStatus(formatted(NO_SUCH_SCHEMA, schemaName))) - .bodyToMono(InternalCompatibilityCheck.class) - .as(m -> failoverAble(m, new FailoverMono<>(cluster.getSchemaRegistry(), - () -> this.checksSchemaCompatibility(cluster, schemaName, newSchemaSubject)))); - } - - public String formatted(String str, Object... args) { - try (Formatter formatter = new Formatter()) { - return formatter.format(str, args).toString(); - } - } - - private void setBasicAuthIfEnabled(InternalSchemaRegistry schemaRegistry, HttpHeaders headers) { - if (schemaRegistry.getUsername() != null && schemaRegistry.getPassword() != null) { - headers.setBasicAuth( - schemaRegistry.getUsername(), - schemaRegistry.getPassword() - ); - } else if (schemaRegistry.getUsername() != null) { - throw new ValidationException( - "You specified username but did not specify password"); - } else if (schemaRegistry.getPassword() != null) { - throw new ValidationException( - "You specified password but did not specify username"); - } - } - - private boolean isUnrecognizedFieldSchemaTypeMessage(String errorMessage) { - return errorMessage.contains(UNRECOGNIZED_FIELD_SCHEMA_TYPE); - } - - private boolean isIncompatibleSchemaMessage(String message) { - return message.contains(INCOMPATIBLE_WITH_AN_EARLIER_SCHEMA); - } - - private WebClient.RequestBodySpec configuredWebClient(KafkaCluster cluster, HttpMethod method, - String uri) { - return configuredWebClient(cluster, method, uri, Collections.emptyList(), - new LinkedMultiValueMap<>()); - } - - private WebClient.RequestBodySpec configuredWebClient(KafkaCluster cluster, HttpMethod method, - String uri, List uriVariables) { - return configuredWebClient(cluster, method, uri, uriVariables, new LinkedMultiValueMap<>()); - } - - private WebClient.RequestBodySpec configuredWebClient(KafkaCluster cluster, HttpMethod method, - String uri, @Nullable String uriVariable) { - List uriVariables = uriVariable == null ? Collections.emptyList() : List.of(uriVariable); - return configuredWebClient(cluster, method, uri, uriVariables, new LinkedMultiValueMap<>()); - } - - private WebClient.RequestBodySpec configuredWebClient(KafkaCluster cluster, - HttpMethod method, String path, - List uriVariables, - MultiValueMap queryParams) { - final var schemaRegistry = cluster.getSchemaRegistry(); - return webClient - .method(method) - .uri(buildUri(schemaRegistry, path, uriVariables, queryParams)) - .headers(headers -> setBasicAuthIfEnabled(schemaRegistry, headers)); - } - - private URI buildUri(InternalSchemaRegistry schemaRegistry, String path, List uriVariables, - MultiValueMap queryParams) { - final var builder = UriComponentsBuilder - .fromHttpUrl(schemaRegistry.getUri() + path); - builder.queryParams(queryParams); - return builder.buildAndExpand(uriVariables.toArray()).toUri(); - } - - private Function> errorOnSchemaDeleteFailure(String schemaName) { - return resp -> { - if (NOT_FOUND.equals(resp.statusCode())) { - return Mono.error(new SchemaNotFoundException(schemaName)); - } - return Mono.error(new SchemaFailedToDeleteException(schemaName)); - }; - } - - private Mono failoverAble(Mono request, FailoverMono failoverMethod) { - return request.onErrorResume(failoverMethod::failover); - } - - private Flux failoverAble(Flux request, FailoverFlux failoverMethod) { - return request.onErrorResume(failoverMethod::failover); - } - - private abstract static class Failover { - private final InternalSchemaRegistry schemaRegistry; - private final Supplier failover; - - private Failover(InternalSchemaRegistry schemaRegistry, Supplier failover) { - this.schemaRegistry = Objects.requireNonNull(schemaRegistry); - this.failover = Objects.requireNonNull(failover); - } - - abstract E error(Throwable error); - - public E failover(Throwable error) { - if (error instanceof WebClientRequestException - && error.getCause() instanceof IOException - && schemaRegistry.isFailoverAvailable()) { - var uri = ((WebClientRequestException) error).getUri(); - schemaRegistry.markAsUnavailable(String.format("%s://%s", uri.getScheme(), uri.getAuthority())); - return failover.get(); - } - return error(error); - } - } - - private static class FailoverMono extends Failover> { - - private FailoverMono(InternalSchemaRegistry schemaRegistry, Supplier> failover) { - super(schemaRegistry, failover); - } - - @Override - Mono error(Throwable error) { - return Mono.error(error); - } - } - - private static class FailoverFlux extends Failover> { - - private FailoverFlux(InternalSchemaRegistry schemaRegistry, Supplier> failover) { - super(schemaRegistry, failover); - } - - @Override - Flux error(Throwable error) { - return Flux.error(error); - } + public Mono checksSchemaCompatibility(KafkaCluster cluster, + String schemaName, + NewSubject newSchemaSubject) { + return api(cluster).mono(c -> c.checkSchemaCompatibility(schemaName, LATEST, true, newSchemaSubject)); } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/StatisticsCache.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/StatisticsCache.java new file mode 100644 index 00000000000..3acd64262b0 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/StatisticsCache.java @@ -0,0 +1,65 @@ +package com.provectus.kafka.ui.service; + +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.ServerStatusDTO; +import com.provectus.kafka.ui.model.Statistics; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.kafka.clients.admin.ConfigEntry; +import org.apache.kafka.clients.admin.TopicDescription; +import org.springframework.stereotype.Component; + +@Component +public class StatisticsCache { + + private final Map cache = new ConcurrentHashMap<>(); + + public StatisticsCache(ClustersStorage clustersStorage) { + var initializing = Statistics.empty().toBuilder().status(ServerStatusDTO.INITIALIZING).build(); + clustersStorage.getKafkaClusters().forEach(c -> cache.put(c.getName(), initializing)); + } + + public synchronized void replace(KafkaCluster c, Statistics stats) { + cache.put(c.getName(), stats); + } + + public synchronized void update(KafkaCluster c, + Map descriptions, + Map> configs) { + var metrics = get(c); + var updatedDescriptions = new HashMap<>(metrics.getTopicDescriptions()); + updatedDescriptions.putAll(descriptions); + var updatedConfigs = new HashMap<>(metrics.getTopicConfigs()); + updatedConfigs.putAll(configs); + replace( + c, + metrics.toBuilder() + .topicDescriptions(updatedDescriptions) + .topicConfigs(updatedConfigs) + .build() + ); + } + + public synchronized void onTopicDelete(KafkaCluster c, String topic) { + var metrics = get(c); + var updatedDescriptions = new HashMap<>(metrics.getTopicDescriptions()); + updatedDescriptions.remove(topic); + var updatedConfigs = new HashMap<>(metrics.getTopicConfigs()); + updatedConfigs.remove(topic); + replace( + c, + metrics.toBuilder() + .topicDescriptions(updatedDescriptions) + .topicConfigs(updatedConfigs) + .build() + ); + } + + public Statistics get(KafkaCluster c) { + return Objects.requireNonNull(cache.get(c.getName()), "Unknown cluster metrics requested"); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/StatisticsService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/StatisticsService.java new file mode 100644 index 00000000000..19d946590c4 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/StatisticsService.java @@ -0,0 +1,79 @@ +package com.provectus.kafka.ui.service; + +import static com.provectus.kafka.ui.service.ReactiveAdminClient.ClusterDescription; + +import com.provectus.kafka.ui.model.ClusterFeature; +import com.provectus.kafka.ui.model.InternalLogDirStats; +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.Metrics; +import com.provectus.kafka.ui.model.ServerStatusDTO; +import com.provectus.kafka.ui.model.Statistics; +import com.provectus.kafka.ui.service.metrics.MetricsCollector; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.admin.ConfigEntry; +import org.apache.kafka.clients.admin.TopicDescription; +import org.apache.kafka.common.Node; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +@Service +@RequiredArgsConstructor +@Slf4j +public class StatisticsService { + + private final MetricsCollector metricsCollector; + private final AdminClientService adminClientService; + private final FeatureService featureService; + private final StatisticsCache cache; + + public Mono updateCache(KafkaCluster c) { + return getStatistics(c).doOnSuccess(m -> cache.replace(c, m)); + } + + private Mono getStatistics(KafkaCluster cluster) { + return adminClientService.get(cluster).flatMap(ac -> + ac.describeCluster().flatMap(description -> + ac.updateInternalStats(description.getController()).then( + Mono.zip( + List.of( + metricsCollector.getBrokerMetrics(cluster, description.getNodes()), + getLogDirInfo(description, ac), + featureService.getAvailableFeatures(ac, cluster, description), + loadTopicConfigs(cluster), + describeTopics(cluster)), + results -> + Statistics.builder() + .status(ServerStatusDTO.ONLINE) + .clusterDescription(description) + .version(ac.getVersion()) + .metrics((Metrics) results[0]) + .logDirInfo((InternalLogDirStats) results[1]) + .features((List) results[2]) + .topicConfigs((Map>) results[3]) + .topicDescriptions((Map) results[4]) + .build() + )))) + .doOnError(e -> + log.error("Failed to collect cluster {} info", cluster.getName(), e)) + .onErrorResume( + e -> Mono.just(Statistics.empty().toBuilder().lastKafkaException(e).build())); + } + + private Mono getLogDirInfo(ClusterDescription desc, ReactiveAdminClient ac) { + var brokerIds = desc.getNodes().stream().map(Node::id).collect(Collectors.toSet()); + return ac.describeLogDirs(brokerIds).map(InternalLogDirStats::new); + } + + private Mono> describeTopics(KafkaCluster c) { + return adminClientService.get(c).flatMap(ReactiveAdminClient::describeTopics); + } + + private Mono>> loadTopicConfigs(KafkaCluster c) { + return adminClientService.get(c).flatMap(ReactiveAdminClient::getTopicsConfig); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/TopicsService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/TopicsService.java index 75c32984617..8e976906d56 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/TopicsService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/TopicsService.java @@ -3,11 +3,13 @@ import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; +import com.google.common.collect.Sets; +import com.provectus.kafka.ui.config.ClustersProperties; import com.provectus.kafka.ui.exception.TopicMetadataException; import com.provectus.kafka.ui.exception.TopicNotFoundException; import com.provectus.kafka.ui.exception.TopicRecreationException; import com.provectus.kafka.ui.exception.ValidationException; -import com.provectus.kafka.ui.model.Feature; +import com.provectus.kafka.ui.model.ClusterFeature; import com.provectus.kafka.ui.model.InternalLogDirStats; import com.provectus.kafka.ui.model.InternalPartition; import com.provectus.kafka.ui.model.InternalPartitionsOffsets; @@ -15,15 +17,14 @@ import com.provectus.kafka.ui.model.InternalTopic; import com.provectus.kafka.ui.model.InternalTopicConfig; import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.Metrics; import com.provectus.kafka.ui.model.PartitionsIncreaseDTO; import com.provectus.kafka.ui.model.PartitionsIncreaseResponseDTO; import com.provectus.kafka.ui.model.ReplicationFactorChangeDTO; import com.provectus.kafka.ui.model.ReplicationFactorChangeResponseDTO; +import com.provectus.kafka.ui.model.Statistics; import com.provectus.kafka.ui.model.TopicCreationDTO; -import com.provectus.kafka.ui.model.TopicMessageSchemaDTO; import com.provectus.kafka.ui.model.TopicUpdateDTO; -import com.provectus.kafka.ui.serde.DeserializationService; -import com.provectus.kafka.ui.util.JmxClusterUtil; import java.time.Duration; import java.util.Collection; import java.util.Collections; @@ -38,6 +39,7 @@ import org.apache.kafka.clients.admin.NewPartitionReassignment; import org.apache.kafka.clients.admin.NewPartitions; import org.apache.kafka.clients.admin.OffsetSpec; +import org.apache.kafka.clients.admin.ProducerState; import org.apache.kafka.clients.admin.TopicDescription; import org.apache.kafka.common.Node; import org.apache.kafka.common.TopicPartition; @@ -52,8 +54,8 @@ public class TopicsService { private final AdminClientService adminClientService; - private final DeserializationService deserializationService; - private final MetricsCache metricsCache; + private final StatisticsCache statisticsCache; + private final ClustersProperties clustersProperties; @Value("${topic.recreate.maxRetries:15}") private int recreateMaxRetries; @Value("${topic.recreate.delay.seconds:1}") @@ -69,17 +71,17 @@ public Mono> loadTopics(KafkaCluster c, List topics) } return adminClientService.get(c) .flatMap(ac -> - ac.describeTopics(topics).zipWith(ac.getTopicsConfig(topics), + ac.describeTopics(topics).zipWith(ac.getTopicsConfig(topics, false), (descriptions, configs) -> { - metricsCache.update(c, descriptions, configs); + statisticsCache.update(c, descriptions, configs); return getPartitionOffsets(descriptions, ac).map(offsets -> { - var metrics = metricsCache.get(c); + var metrics = statisticsCache.get(c); return createList( topics, descriptions, configs, offsets, - metrics.getJmxMetrics(), + metrics.getMetrics(), metrics.getLogDirInfo() ); }); @@ -120,7 +122,7 @@ private List createList(List orderedNames, Map descriptions, Map> configs, InternalPartitionsOffsets partitionsOffsets, - JmxClusterUtil.JmxMetrics jmxMetrics, + Metrics metrics, InternalLogDirStats logDirInfo) { return orderedNames.stream() .filter(descriptions::containsKey) @@ -128,25 +130,22 @@ private List createList(List orderedNames, descriptions.get(t), configs.getOrDefault(t, List.of()), partitionsOffsets, - jmxMetrics, - logDirInfo + metrics, + logDirInfo, + clustersProperties.getInternalTopicPrefix() )) .collect(toList()); } private Mono getPartitionOffsets(Map - descriptions, + descriptionsMap, ReactiveAdminClient ac) { - var topicPartitions = descriptions.values().stream() - .flatMap(desc -> - desc.partitions().stream().map(p -> new TopicPartition(desc.name(), p.partition()))) - .collect(toList()); - - return ac.listOffsets(topicPartitions, OffsetSpec.earliest()) - .zipWith(ac.listOffsets(topicPartitions, OffsetSpec.latest()), + var descriptions = descriptionsMap.values(); + return ac.listOffsets(descriptions, OffsetSpec.earliest()) + .zipWith(ac.listOffsets(descriptions, OffsetSpec.latest()), (earliest, latest) -> - topicPartitions.stream() - .filter(tp -> earliest.containsKey(tp) && latest.containsKey(tp)) + Sets.intersection(earliest.keySet(), latest.keySet()) + .stream() .map(tp -> Map.entry(tp, new InternalPartitionsOffsets.Offsets( @@ -160,26 +159,28 @@ public Mono getTopicDetails(KafkaCluster cluster, String topicNam } public Mono> getTopicConfigs(KafkaCluster cluster, String topicName) { + // there 2 case that we cover here: + // 1. topic not found/visible - describeTopic() will be empty and we will throw TopicNotFoundException + // 2. topic is visible, but we don't have DESCRIBE_CONFIG permission - we should return empty list return adminClientService.get(cluster) - .flatMap(ac -> ac.getTopicsConfig(List.of(topicName))) - .map(m -> m.values().stream().findFirst().orElseThrow(TopicNotFoundException::new)); + .flatMap(ac -> ac.describeTopic(topicName) + .switchIfEmpty(Mono.error(new TopicNotFoundException())) + .then(ac.getTopicsConfig(List.of(topicName), true)) + .map(m -> m.values().stream().findFirst().orElse(List.of()))); } - private Mono createTopic(KafkaCluster c, ReactiveAdminClient adminClient, - Mono topicCreation) { - return topicCreation.flatMap(topicData -> - adminClient.createTopic( - topicData.getName(), - topicData.getPartitions(), - topicData.getReplicationFactor().shortValue(), - topicData.getConfigs() - ).thenReturn(topicData) - ) - .onErrorResume(t -> Mono.error(new TopicMetadataException(t.getMessage()))) - .flatMap(topicData -> loadTopicAfterCreation(c, topicData.getName())); + private Mono createTopic(KafkaCluster c, ReactiveAdminClient adminClient, TopicCreationDTO topicData) { + return adminClient.createTopic( + topicData.getName(), + topicData.getPartitions(), + topicData.getReplicationFactor(), + topicData.getConfigs()) + .thenReturn(topicData) + .onErrorMap(t -> new TopicMetadataException(t.getMessage(), t)) + .then(loadTopicAfterCreation(c, topicData.getName())); } - public Mono createTopic(KafkaCluster cluster, Mono topicCreation) { + public Mono createTopic(KafkaCluster cluster, TopicCreationDTO topicCreation) { return adminClientService.get(cluster) .flatMap(ac -> createTopic(cluster, ac, topicCreation)); } @@ -195,7 +196,7 @@ public Mono recreateTopic(KafkaCluster cluster, String topicName) ac.createTopic( topic.getName(), topic.getPartitionCount(), - (short) topic.getReplicationFactor(), + topic.getReplicationFactor(), topic.getTopicConfigs() .stream() .collect(Collectors.toMap(InternalTopicConfig::getName, @@ -251,7 +252,7 @@ public Mono changeReplicationFactor( .flatMap(ac -> { Integer actual = topic.getReplicationFactor(); Integer requested = replicationFactorChange.getTotalReplicationFactor(); - Integer brokersCount = metricsCache.get(cluster).getClusterDescription() + Integer brokersCount = statisticsCache.get(cluster).getClusterDescription() .getNodes().size(); if (requested.equals(actual)) { @@ -259,6 +260,11 @@ public Mono changeReplicationFactor( new ValidationException( String.format("Topic already has replicationFactor %s.", actual))); } + if (requested <= 0) { + return Mono.error( + new ValidationException( + String.format("Requested replication factor (%s) should be greater or equal to 1.", requested))); + } if (requested > brokersCount) { return Mono.error( new ValidationException( @@ -362,7 +368,7 @@ private Map> getCurrentAssignment(InternalTopic topic) { private Map getBrokersMap(KafkaCluster cluster, Map> currentAssignment) { - Map result = metricsCache.get(cluster).getClusterDescription().getNodes() + Map result = statisticsCache.get(cluster).getClusterDescription().getNodes() .stream() .map(Node::id) .collect(toMap( @@ -410,23 +416,14 @@ public Mono increaseTopicPartitions( } public Mono deleteTopic(KafkaCluster cluster, String topicName) { - if (metricsCache.get(cluster).getFeatures().contains(Feature.TOPIC_DELETION)) { + if (statisticsCache.get(cluster).getFeatures().contains(ClusterFeature.TOPIC_DELETION)) { return adminClientService.get(cluster).flatMap(c -> c.deleteTopic(topicName)) - .doOnSuccess(t -> metricsCache.onTopicDelete(cluster, topicName)); + .doOnSuccess(t -> statisticsCache.onTopicDelete(cluster, topicName)); } else { return Mono.error(new ValidationException("Topic deletion restricted")); } } - public TopicMessageSchemaDTO getTopicSchema(KafkaCluster cluster, String topicName) { - if (!metricsCache.get(cluster).getTopicDescriptions().containsKey(topicName)) { - throw new TopicNotFoundException(); - } - return deserializationService - .getRecordDeserializerForCluster(cluster) - .getTopicSchema(topicName); - } - public Mono cloneTopic( KafkaCluster cluster, String topicName, String newTopicName) { return loadTopic(cluster, topicName).flatMap(topic -> @@ -435,7 +432,7 @@ public Mono cloneTopic( ac.createTopic( newTopicName, topic.getPartitionCount(), - (short) topic.getReplicationFactor(), + topic.getReplicationFactor(), topic.getTopicConfigs() .stream() .collect(Collectors @@ -447,23 +444,34 @@ public Mono cloneTopic( } public Mono> getTopicsForPagination(KafkaCluster cluster) { - MetricsCache.Metrics metrics = metricsCache.get(cluster); - return filterExisting(cluster, metrics.getTopicDescriptions().keySet()) - .map(lst -> lst.stream() - .map(topicName -> - InternalTopic.from( - metrics.getTopicDescriptions().get(topicName), - metrics.getTopicConfigs().getOrDefault(topicName, List.of()), - InternalPartitionsOffsets.empty(), - metrics.getJmxMetrics(), - metrics.getLogDirInfo())) - .collect(toList()) - ); + Statistics stats = statisticsCache.get(cluster); + return filterExisting(cluster, stats.getTopicDescriptions().keySet()) + .map(lst -> lst.stream() + .map(topicName -> + InternalTopic.from( + stats.getTopicDescriptions().get(topicName), + stats.getTopicConfigs().getOrDefault(topicName, List.of()), + InternalPartitionsOffsets.empty(), + stats.getMetrics(), + stats.getLogDirInfo(), + clustersProperties.getInternalTopicPrefix() + )) + .collect(toList()) + ); + } + + public Mono>> getActiveProducersState(KafkaCluster cluster, String topic) { + return adminClientService.get(cluster) + .flatMap(ac -> ac.getActiveProducersState(topic)); } private Mono> filterExisting(KafkaCluster cluster, Collection topics) { - return adminClientService.get(cluster).flatMap(ac -> ac.listTopics(true)) - .map(existing -> existing.stream().filter(topics::contains).collect(toList())); + return adminClientService.get(cluster) + .flatMap(ac -> ac.listTopics(true)) + .map(existing -> existing + .stream() + .filter(topics::contains) + .collect(toList())); } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/acl/AclCsv.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/acl/AclCsv.java new file mode 100644 index 00000000000..673b17ee1f8 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/acl/AclCsv.java @@ -0,0 +1,81 @@ +package com.provectus.kafka.ui.service.acl; + +import com.provectus.kafka.ui.exception.ValidationException; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.kafka.common.acl.AccessControlEntry; +import org.apache.kafka.common.acl.AclBinding; +import org.apache.kafka.common.acl.AclOperation; +import org.apache.kafka.common.acl.AclPermissionType; +import org.apache.kafka.common.resource.PatternType; +import org.apache.kafka.common.resource.ResourcePattern; +import org.apache.kafka.common.resource.ResourceType; + +public class AclCsv { + + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String VALUES_SEPARATOR = ","; + private static final String HEADER = "Principal,ResourceType,PatternType,ResourceName,Operation,PermissionType,Host"; + + public static String transformToCsvString(Collection acls) { + return Stream.concat(Stream.of(HEADER), acls.stream().map(AclCsv::createAclString)) + .collect(Collectors.joining(System.lineSeparator())); + } + + public static String createAclString(AclBinding binding) { + var pattern = binding.pattern(); + var filter = binding.toFilter().entryFilter(); + return String.format( + "%s,%s,%s,%s,%s,%s,%s", + filter.principal(), + pattern.resourceType(), + pattern.patternType(), + pattern.name(), + filter.operation(), + filter.permissionType(), + filter.host() + ); + } + + private static AclBinding parseCsvLine(String csv, int line) { + String[] values = csv.split(VALUES_SEPARATOR); + if (values.length != 7) { + throw new ValidationException("Input csv is not valid - there should be 7 columns in line " + line); + } + for (int i = 0; i < values.length; i++) { + if ((values[i] = values[i].trim()).isBlank()) { + throw new ValidationException("Input csv is not valid - blank value in colum " + i + ", line " + line); + } + } + try { + return new AclBinding( + new ResourcePattern( + ResourceType.valueOf(values[1]), values[3], PatternType.valueOf(values[2])), + new AccessControlEntry( + values[0], values[6], AclOperation.valueOf(values[4]), AclPermissionType.valueOf(values[5])) + ); + } catch (IllegalArgumentException enumParseError) { + throw new ValidationException("Error parsing enum value in line " + line); + } + } + + public static Collection parseCsv(String csvString) { + String[] lines = csvString.split(LINE_SEPARATOR); + if (lines.length == 0) { + throw new ValidationException("Error parsing ACL csv file: no lines in file"); + } + boolean firstLineIsHeader = HEADER.equalsIgnoreCase(lines[0].trim().replace(" ", "")); + Set result = new HashSet<>(); + for (int i = firstLineIsHeader ? 1 : 0; i < lines.length; i++) { + String line = lines[i]; + if (!line.isBlank()) { + AclBinding aclBinding = parseCsvLine(line, i); + result.add(aclBinding); + } + } + return result; + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/acl/AclsService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/acl/AclsService.java new file mode 100644 index 00000000000..a621ce99cc3 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/acl/AclsService.java @@ -0,0 +1,272 @@ +package com.provectus.kafka.ui.service.acl; + +import static org.apache.kafka.common.acl.AclOperation.ALL; +import static org.apache.kafka.common.acl.AclOperation.CREATE; +import static org.apache.kafka.common.acl.AclOperation.DESCRIBE; +import static org.apache.kafka.common.acl.AclOperation.IDEMPOTENT_WRITE; +import static org.apache.kafka.common.acl.AclOperation.READ; +import static org.apache.kafka.common.acl.AclOperation.WRITE; +import static org.apache.kafka.common.acl.AclPermissionType.ALLOW; +import static org.apache.kafka.common.resource.PatternType.LITERAL; +import static org.apache.kafka.common.resource.PatternType.PREFIXED; +import static org.apache.kafka.common.resource.ResourceType.CLUSTER; +import static org.apache.kafka.common.resource.ResourceType.GROUP; +import static org.apache.kafka.common.resource.ResourceType.TOPIC; +import static org.apache.kafka.common.resource.ResourceType.TRANSACTIONAL_ID; + +import com.google.common.collect.Sets; +import com.provectus.kafka.ui.model.CreateConsumerAclDTO; +import com.provectus.kafka.ui.model.CreateProducerAclDTO; +import com.provectus.kafka.ui.model.CreateStreamAppAclDTO; +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.service.AdminClientService; +import com.provectus.kafka.ui.service.ReactiveAdminClient; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import javax.annotation.Nullable; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.common.acl.AccessControlEntry; +import org.apache.kafka.common.acl.AclBinding; +import org.apache.kafka.common.acl.AclOperation; +import org.apache.kafka.common.resource.Resource; +import org.apache.kafka.common.resource.ResourcePattern; +import org.apache.kafka.common.resource.ResourcePatternFilter; +import org.apache.kafka.common.resource.ResourceType; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AclsService { + + private final AdminClientService adminClientService; + + public Mono createAcl(KafkaCluster cluster, AclBinding aclBinding) { + return adminClientService.get(cluster) + .flatMap(ac -> createAclsWithLogging(ac, List.of(aclBinding))); + } + + private Mono createAclsWithLogging(ReactiveAdminClient ac, Collection bindings) { + bindings.forEach(b -> log.info("CREATING ACL: [{}]", AclCsv.createAclString(b))); + return ac.createAcls(bindings) + .doOnSuccess(v -> bindings.forEach(b -> log.info("ACL CREATED: [{}]", AclCsv.createAclString(b)))); + } + + public Mono deleteAcl(KafkaCluster cluster, AclBinding aclBinding) { + var aclString = AclCsv.createAclString(aclBinding); + log.info("DELETING ACL: [{}]", aclString); + return adminClientService.get(cluster) + .flatMap(ac -> ac.deleteAcls(List.of(aclBinding))) + .doOnSuccess(v -> log.info("ACL DELETED: [{}]", aclString)); + } + + public Flux listAcls(KafkaCluster cluster, ResourcePatternFilter filter) { + return adminClientService.get(cluster) + .flatMap(c -> c.listAcls(filter)) + .flatMapIterable(acls -> acls) + .sort(Comparator.comparing(AclBinding::toString)); //sorting to keep stable order on different calls + } + + public Mono getAclAsCsvString(KafkaCluster cluster) { + return adminClientService.get(cluster) + .flatMap(c -> c.listAcls(ResourcePatternFilter.ANY)) + .map(AclCsv::transformToCsvString); + } + + public Mono syncAclWithAclCsv(KafkaCluster cluster, String csv) { + return adminClientService.get(cluster) + .flatMap(ac -> ac.listAcls(ResourcePatternFilter.ANY).flatMap(existingAclList -> { + var existingSet = Set.copyOf(existingAclList); + var newAcls = Set.copyOf(AclCsv.parseCsv(csv)); + var toDelete = Sets.difference(existingSet, newAcls); + var toAdd = Sets.difference(newAcls, existingSet); + logAclSyncPlan(cluster, toAdd, toDelete); + if (toAdd.isEmpty() && toDelete.isEmpty()) { + return Mono.empty(); + } + log.info("Starting new ACLs creation"); + return ac.createAcls(toAdd) + .doOnSuccess(v -> { + log.info("{} new ACLs created", toAdd.size()); + log.info("Starting ACLs deletion"); + }) + .then(ac.deleteAcls(toDelete) + .doOnSuccess(v -> log.info("{} ACLs deleted", toDelete.size()))); + })); + } + + private void logAclSyncPlan(KafkaCluster cluster, Set toBeAdded, Set toBeDeleted) { + log.info("'{}' cluster ACL sync plan: ", cluster.getName()); + if (toBeAdded.isEmpty() && toBeDeleted.isEmpty()) { + log.info("Nothing to do, ACL is already in sync"); + return; + } + if (!toBeAdded.isEmpty()) { + log.info("ACLs to be added ({}): ", toBeAdded.size()); + for (AclBinding aclBinding : toBeAdded) { + log.info(" " + AclCsv.createAclString(aclBinding)); + } + } + if (!toBeDeleted.isEmpty()) { + log.info("ACLs to be deleted ({}): ", toBeDeleted.size()); + for (AclBinding aclBinding : toBeDeleted) { + log.info(" " + AclCsv.createAclString(aclBinding)); + } + } + } + + // creates allow binding for resources by prefix or specific names list + private List createAllowBindings(ResourceType resourceType, + List opsToAllow, + String principal, + String host, + @Nullable String resourcePrefix, + @Nullable Collection resourceNames) { + List bindings = new ArrayList<>(); + if (resourcePrefix != null) { + for (var op : opsToAllow) { + bindings.add( + new AclBinding( + new ResourcePattern(resourceType, resourcePrefix, PREFIXED), + new AccessControlEntry(principal, host, op, ALLOW))); + } + } + if (!CollectionUtils.isEmpty(resourceNames)) { + resourceNames.stream() + .distinct() + .forEach(resource -> + opsToAllow.forEach(op -> + bindings.add( + new AclBinding( + new ResourcePattern(resourceType, resource, LITERAL), + new AccessControlEntry(principal, host, op, ALLOW))))); + } + return bindings; + } + + public Mono createConsumerAcl(KafkaCluster cluster, CreateConsumerAclDTO request) { + return adminClientService.get(cluster) + .flatMap(ac -> createAclsWithLogging(ac, createConsumerBindings(request))) + .then(); + } + + //Read, Describe on topics, Read on consumerGroups + private List createConsumerBindings(CreateConsumerAclDTO request) { + List bindings = new ArrayList<>(); + bindings.addAll( + createAllowBindings(TOPIC, + List.of(READ, DESCRIBE), + request.getPrincipal(), + request.getHost(), + request.getTopicsPrefix(), + request.getTopics())); + + bindings.addAll( + createAllowBindings( + GROUP, + List.of(READ), + request.getPrincipal(), + request.getHost(), + request.getConsumerGroupsPrefix(), + request.getConsumerGroups())); + return bindings; + } + + public Mono createProducerAcl(KafkaCluster cluster, CreateProducerAclDTO request) { + return adminClientService.get(cluster) + .flatMap(ac -> createAclsWithLogging(ac, createProducerBindings(request))) + .then(); + } + + //Write, Describe, Create permission on topics, Write, Describe on transactionalIds + //IDEMPOTENT_WRITE on cluster if idempotent is enabled + private List createProducerBindings(CreateProducerAclDTO request) { + List bindings = new ArrayList<>(); + bindings.addAll( + createAllowBindings( + TOPIC, + List.of(WRITE, DESCRIBE, CREATE), + request.getPrincipal(), + request.getHost(), + request.getTopicsPrefix(), + request.getTopics())); + + bindings.addAll( + createAllowBindings( + TRANSACTIONAL_ID, + List.of(WRITE, DESCRIBE), + request.getPrincipal(), + request.getHost(), + request.getTransactionsIdPrefix(), + Optional.ofNullable(request.getTransactionalId()).map(List::of).orElse(null))); + + if (Boolean.TRUE.equals(request.getIdempotent())) { + bindings.addAll( + createAllowBindings( + CLUSTER, + List.of(IDEMPOTENT_WRITE), + request.getPrincipal(), + request.getHost(), + null, + List.of(Resource.CLUSTER_NAME))); // cluster name is a const string in ACL api + } + return bindings; + } + + public Mono createStreamAppAcl(KafkaCluster cluster, CreateStreamAppAclDTO request) { + return adminClientService.get(cluster) + .flatMap(ac -> createAclsWithLogging(ac, createStreamAppBindings(request))) + .then(); + } + + // Read on input topics, Write on output topics + // ALL on applicationId-prefixed Groups and Topics + private List createStreamAppBindings(CreateStreamAppAclDTO request) { + List bindings = new ArrayList<>(); + bindings.addAll( + createAllowBindings( + TOPIC, + List.of(READ), + request.getPrincipal(), + request.getHost(), + null, + request.getInputTopics())); + + bindings.addAll( + createAllowBindings( + TOPIC, + List.of(WRITE), + request.getPrincipal(), + request.getHost(), + null, + request.getOutputTopics())); + + bindings.addAll( + createAllowBindings( + GROUP, + List.of(ALL), + request.getPrincipal(), + request.getHost(), + request.getApplicationId(), + null)); + + bindings.addAll( + createAllowBindings( + TOPIC, + List.of(ALL), + request.getPrincipal(), + request.getHost(), + request.getApplicationId(), + null)); + return bindings; + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/AnalysisTasksStore.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/AnalysisTasksStore.java new file mode 100644 index 00000000000..f7bf4a8f2be --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/AnalysisTasksStore.java @@ -0,0 +1,115 @@ +package com.provectus.kafka.ui.service.analyze; + +import com.google.common.base.Throwables; +import com.provectus.kafka.ui.model.TopicAnalysisDTO; +import com.provectus.kafka.ui.model.TopicAnalysisProgressDTO; +import com.provectus.kafka.ui.model.TopicAnalysisResultDTO; +import java.io.Closeable; +import java.math.BigDecimal; +import java.time.Instant; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import lombok.Builder; +import lombok.SneakyThrows; +import lombok.Value; + +class AnalysisTasksStore { + + private final Map running = new ConcurrentHashMap<>(); + private final Map completed = new ConcurrentHashMap<>(); + + void setAnalysisError(TopicIdentity topicId, + Instant collectionStartedAt, + Throwable th) { + running.remove(topicId); + completed.put( + topicId, + new TopicAnalysisResultDTO() + .startedAt(collectionStartedAt.toEpochMilli()) + .finishedAt(System.currentTimeMillis()) + .error(Throwables.getStackTraceAsString(th)) + ); + } + + void setAnalysisResult(TopicIdentity topicId, + Instant collectionStartedAt, + TopicAnalysisStats totalStats, + Map partitionStats) { + running.remove(topicId); + completed.put(topicId, + new TopicAnalysisResultDTO() + .startedAt(collectionStartedAt.toEpochMilli()) + .finishedAt(System.currentTimeMillis()) + .totalStats(totalStats.toDto(null)) + .partitionStats( + partitionStats.entrySet().stream() + .map(e -> e.getValue().toDto(e.getKey())) + .collect(Collectors.toList()) + )); + } + + void updateProgress(TopicIdentity topicId, + long msgsScanned, + long bytesScanned, + Double completeness) { + running.computeIfPresent(topicId, (k, state) -> + state.toBuilder() + .msgsScanned(msgsScanned) + .bytesScanned(bytesScanned) + .completenessPercent(completeness) + .build()); + } + + void registerNewTask(TopicIdentity topicId, Closeable task) { + running.put(topicId, new RunningAnalysis(Instant.now(), 0.0, 0, 0, task)); + } + + void cancelAnalysis(TopicIdentity topicId) { + Optional.ofNullable(running.remove(topicId)) + .ifPresent(RunningAnalysis::stopTask); + } + + boolean isAnalysisInProgress(TopicIdentity id) { + return running.containsKey(id); + } + + Optional getTopicAnalysis(TopicIdentity id) { + var runningState = running.get(id); + var completedState = completed.get(id); + if (runningState == null && completedState == null) { + return Optional.empty(); + } + return Optional.of(createAnalysisDto(runningState, completedState)); + } + + private TopicAnalysisDTO createAnalysisDto(@Nullable RunningAnalysis runningState, + @Nullable TopicAnalysisResultDTO completedState) { + return new TopicAnalysisDTO() + .progress(runningState != null ? runningState.toDto() : null) + .result(completedState); + } + + @Builder(toBuilder = true) + private record RunningAnalysis(Instant startedAt, + double completenessPercent, + long msgsScanned, + long bytesScanned, + Closeable task) { + + TopicAnalysisProgressDTO toDto() { + return new TopicAnalysisProgressDTO() + .startedAt(startedAt.toEpochMilli()) + .bytesScanned(bytesScanned) + .msgsScanned(msgsScanned) + .completenessPercent(BigDecimal.valueOf(completenessPercent)); + } + + @SneakyThrows + void stopTask() { + task.close(); + } + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisService.java new file mode 100644 index 00000000000..2523aae89ec --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisService.java @@ -0,0 +1,144 @@ +package com.provectus.kafka.ui.service.analyze; + +import static com.provectus.kafka.ui.model.SeekTypeDTO.BEGINNING; + +import com.provectus.kafka.ui.emitter.EnhancedConsumer; +import com.provectus.kafka.ui.emitter.SeekOperations; +import com.provectus.kafka.ui.exception.TopicAnalysisException; +import com.provectus.kafka.ui.model.ConsumerPosition; +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.TopicAnalysisDTO; +import com.provectus.kafka.ui.service.ConsumerGroupService; +import com.provectus.kafka.ui.service.TopicsService; +import java.io.Closeable; +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.common.errors.InterruptException; +import org.apache.kafka.common.errors.WakeupException; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + + +@Slf4j +@Component +@RequiredArgsConstructor +public class TopicAnalysisService { + + private static final Scheduler SCHEDULER = Schedulers.newBoundedElastic( + Schedulers.DEFAULT_BOUNDED_ELASTIC_SIZE, + Schedulers.DEFAULT_BOUNDED_ELASTIC_QUEUESIZE, + "topic-analysis-tasks", + 10, //ttl for idle threads (in sec) + true //daemon + ); + + private final AnalysisTasksStore analysisTasksStore = new AnalysisTasksStore(); + + private final TopicsService topicsService; + private final ConsumerGroupService consumerGroupService; + + public Mono analyze(KafkaCluster cluster, String topicName) { + return topicsService.getTopicDetails(cluster, topicName) + .doOnNext(topic -> startAnalysis(cluster, topicName)) + .then(); + } + + private synchronized void startAnalysis(KafkaCluster cluster, String topic) { + var topicId = new TopicIdentity(cluster, topic); + if (analysisTasksStore.isAnalysisInProgress(topicId)) { + throw new TopicAnalysisException("Topic is already analyzing"); + } + var task = new AnalysisTask(cluster, topicId); + analysisTasksStore.registerNewTask(topicId, task); + SCHEDULER.schedule(task); + } + + public void cancelAnalysis(KafkaCluster cluster, String topicName) { + analysisTasksStore.cancelAnalysis(new TopicIdentity(cluster, topicName)); + } + + public Optional getTopicAnalysis(KafkaCluster cluster, String topicName) { + return analysisTasksStore.getTopicAnalysis(new TopicIdentity(cluster, topicName)); + } + + class AnalysisTask implements Runnable, Closeable { + + private final Instant startedAt = Instant.now(); + + private final TopicIdentity topicId; + + private final TopicAnalysisStats totalStats = new TopicAnalysisStats(); + private final Map partitionStats = new HashMap<>(); + + private final EnhancedConsumer consumer; + + AnalysisTask(KafkaCluster cluster, TopicIdentity topicId) { + this.topicId = topicId; + this.consumer = consumerGroupService.createConsumer( + cluster, + // to improve polling throughput + Map.of( + ConsumerConfig.RECEIVE_BUFFER_CONFIG, "-1", //let OS tune buffer size + ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "100000" + ) + ); + } + + @Override + public void close() { + consumer.wakeup(); + } + + @Override + public void run() { + try { + log.info("Starting {} topic analysis", topicId); + consumer.partitionsFor(topicId.topicName) + .forEach(tp -> partitionStats.put(tp.partition(), new TopicAnalysisStats())); + + var seekOperations = SeekOperations.create(consumer, new ConsumerPosition(BEGINNING, topicId.topicName, null)); + long summaryOffsetsRange = seekOperations.summaryOffsetsRange(); + seekOperations.assignAndSeekNonEmptyPartitions(); + + while (!seekOperations.assignedPartitionsFullyPolled()) { + var polled = consumer.pollEnhanced(Duration.ofSeconds(3)); + polled.forEach(r -> { + totalStats.apply(r); + partitionStats.get(r.partition()).apply(r); + }); + updateProgress(seekOperations.offsetsProcessedFromSeek(), summaryOffsetsRange); + } + analysisTasksStore.setAnalysisResult(topicId, startedAt, totalStats, partitionStats); + log.info("{} topic analysis finished", topicId); + } catch (WakeupException | InterruptException cancelException) { + log.info("{} topic analysis stopped", topicId); + // calling cancel for cases when our thread was interrupted by some non-user cancellation reason + analysisTasksStore.cancelAnalysis(topicId); + } catch (Throwable th) { + log.error("Error analyzing topic {}", topicId, th); + analysisTasksStore.setAnalysisError(topicId, startedAt, th); + } finally { + consumer.close(); + } + } + + private void updateProgress(long processedOffsets, long summaryOffsetsRange) { + if (processedOffsets > 0 && summaryOffsetsRange != 0) { + analysisTasksStore.updateProgress( + topicId, + totalStats.totalMsgs, + totalStats.keysSize.sum + totalStats.valuesSize.sum, + Math.min(100.0, (((double) processedOffsets) / summaryOffsetsRange) * 100) + ); + } + } + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisStats.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisStats.java new file mode 100644 index 00000000000..f36d3bec4de --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisStats.java @@ -0,0 +1,140 @@ +package com.provectus.kafka.ui.service.analyze; + +import com.provectus.kafka.ui.model.TopicAnalysisSizeStatsDTO; +import com.provectus.kafka.ui.model.TopicAnalysisStatsDTO; +import com.provectus.kafka.ui.model.TopicAnalysisStatsHourlyMsgCountsInnerDTO; +import java.time.Duration; +import java.time.Instant; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.apache.datasketches.hll.HllSketch; +import org.apache.datasketches.quantiles.DoublesSketch; +import org.apache.datasketches.quantiles.UpdateDoublesSketch; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.common.utils.Bytes; + +class TopicAnalysisStats { + + Long totalMsgs = 0L; + Long minOffset; + Long maxOffset; + + Long minTimestamp; + Long maxTimestamp; + + long nullKeys = 0L; + long nullValues = 0L; + + final SizeStats keysSize = new SizeStats(); + final SizeStats valuesSize = new SizeStats(); + + final HllSketch uniqKeys = new HllSketch(); + final HllSketch uniqValues = new HllSketch(); + + final HourlyCounts hourlyCounts = new HourlyCounts(); + + static class SizeStats { + long sum = 0; + Long min; + Long max; + final UpdateDoublesSketch sizeSketch = DoublesSketch.builder().build(); + + void apply(int len) { + sum += len; + min = minNullable(min, len); + max = maxNullable(max, len); + sizeSketch.update(len); + } + + TopicAnalysisSizeStatsDTO toDto() { + return new TopicAnalysisSizeStatsDTO() + .sum(sum) + .min(min) + .max(max) + .avg((long) (((double) sum) / sizeSketch.getN())) + .prctl50((long) sizeSketch.getQuantile(0.5)) + .prctl75((long) sizeSketch.getQuantile(0.75)) + .prctl95((long) sizeSketch.getQuantile(0.95)) + .prctl99((long) sizeSketch.getQuantile(0.99)) + .prctl999((long) sizeSketch.getQuantile(0.999)); + } + } + + static class HourlyCounts { + + // hour start ms -> count + private final Map hourlyStats = new HashMap<>(); + private final long minTs = Instant.now().minus(Duration.ofDays(14)).toEpochMilli(); + + void apply(ConsumerRecord rec) { + if (rec.timestamp() > minTs) { + var hourStart = rec.timestamp() - rec.timestamp() % (1_000 * 60 * 60); + hourlyStats.compute(hourStart, (h, cnt) -> cnt == null ? 1 : cnt + 1); + } + } + + List toDto() { + return hourlyStats.entrySet().stream() + .sorted(Comparator.comparingLong(Map.Entry::getKey)) + .map(e -> new TopicAnalysisStatsHourlyMsgCountsInnerDTO() + .hourStart(e.getKey()) + .count(e.getValue())) + .collect(Collectors.toList()); + } + } + + void apply(ConsumerRecord rec) { + totalMsgs++; + minTimestamp = minNullable(minTimestamp, rec.timestamp()); + maxTimestamp = maxNullable(maxTimestamp, rec.timestamp()); + minOffset = minNullable(minOffset, rec.offset()); + maxOffset = maxNullable(maxOffset, rec.offset()); + hourlyCounts.apply(rec); + + if (rec.key() != null) { + byte[] keyBytes = rec.key().get(); + keysSize.apply(rec.serializedKeySize()); + uniqKeys.update(keyBytes); + } else { + nullKeys++; + } + + if (rec.value() != null) { + byte[] valueBytes = rec.value().get(); + valuesSize.apply(rec.serializedValueSize()); + uniqValues.update(valueBytes); + } else { + nullValues++; + } + } + + TopicAnalysisStatsDTO toDto(@Nullable Integer partition) { + return new TopicAnalysisStatsDTO() + .partition(partition) + .totalMsgs(totalMsgs) + .minOffset(minOffset) + .maxOffset(maxOffset) + .minTimestamp(minTimestamp) + .maxTimestamp(maxTimestamp) + .nullKeys(nullKeys) + .nullValues(nullValues) + // because of hll error estimated size can be greater that actual msgs count + .approxUniqKeys(Math.min(totalMsgs, (long) uniqKeys.getEstimate())) + .approxUniqValues(Math.min(totalMsgs, (long) uniqValues.getEstimate())) + .keySize(keysSize.toDto()) + .valueSize(valuesSize.toDto()) + .hourlyMsgCounts(hourlyCounts.toDto()); + } + + private static Long maxNullable(@Nullable Long v1, long v2) { + return v1 == null ? v2 : Math.max(v1, v2); + } + + private static Long minNullable(@Nullable Long v1, long v2) { + return v1 == null ? v2 : Math.min(v1, v2); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicIdentity.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicIdentity.java new file mode 100644 index 00000000000..bfe75c1772b --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicIdentity.java @@ -0,0 +1,17 @@ +package com.provectus.kafka.ui.service.analyze; + +import com.provectus.kafka.ui.model.KafkaCluster; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@ToString +@EqualsAndHashCode +class TopicIdentity { + final String clusterName; + final String topicName; + + public TopicIdentity(KafkaCluster cluster, String topic) { + this.clusterName = cluster.getName(); + this.topicName = topic; + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/audit/AuditRecord.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/audit/AuditRecord.java new file mode 100644 index 00000000000..3f7fb44aacd --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/audit/AuditRecord.java @@ -0,0 +1,102 @@ +package com.provectus.kafka.ui.service.audit; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.provectus.kafka.ui.exception.CustomBaseException; +import com.provectus.kafka.ui.exception.ValidationException; +import com.provectus.kafka.ui.model.rbac.AccessContext; +import com.provectus.kafka.ui.model.rbac.Resource; +import com.provectus.kafka.ui.model.rbac.permission.PermissibleAction; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import lombok.SneakyThrows; +import org.springframework.security.access.AccessDeniedException; + +record AuditRecord(String timestamp, + String username, + String clusterName, + List resources, + String operation, + Object operationParams, + OperationResult result) { + + static final JsonMapper MAPPER = new JsonMapper(); + + static { + MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + + @SneakyThrows + String toJson() { + return MAPPER.writeValueAsString(this); + } + + record AuditResource(String accessType, boolean alter, Resource type, @Nullable Object id) { + + private static AuditResource create(PermissibleAction action, Resource type, @Nullable Object id) { + return new AuditResource(action.name(), action.isAlter(), type, id); + } + + static List getAccessedResources(AccessContext ctx) { + List resources = new ArrayList<>(); + ctx.getClusterConfigActions() + .forEach(a -> resources.add(create(a, Resource.CLUSTERCONFIG, null))); + ctx.getTopicActions() + .forEach(a -> resources.add(create(a, Resource.TOPIC, nameId(ctx.getTopic())))); + ctx.getConsumerGroupActions() + .forEach(a -> resources.add(create(a, Resource.CONSUMER, nameId(ctx.getConsumerGroup())))); + ctx.getConnectActions() + .forEach(a -> { + Map resourceId = new LinkedHashMap<>(); + resourceId.put("connect", ctx.getConnect()); + if (ctx.getConnector() != null) { + resourceId.put("connector", ctx.getConnector()); + } + resources.add(create(a, Resource.CONNECT, resourceId)); + }); + ctx.getSchemaActions() + .forEach(a -> resources.add(create(a, Resource.SCHEMA, nameId(ctx.getSchema())))); + ctx.getKsqlActions() + .forEach(a -> resources.add(create(a, Resource.KSQL, null))); + ctx.getAclActions() + .forEach(a -> resources.add(create(a, Resource.ACL, null))); + ctx.getAuditAction() + .forEach(a -> resources.add(create(a, Resource.AUDIT, null))); + return resources; + } + + @Nullable + private static Map nameId(@Nullable String name) { + return name != null ? Map.of("name", name) : null; + } + } + + record OperationResult(boolean success, OperationError error) { + + static OperationResult successful() { + return new OperationResult(true, null); + } + + static OperationResult error(Throwable th) { + OperationError err = OperationError.UNRECOGNIZED_ERROR; + if (th instanceof AccessDeniedException) { + err = OperationError.ACCESS_DENIED; + } else if (th instanceof ValidationException) { + err = OperationError.VALIDATION_ERROR; + } else if (th instanceof CustomBaseException) { + err = OperationError.EXECUTION_ERROR; + } + return new OperationResult(false, err); + } + + enum OperationError { + ACCESS_DENIED, + VALIDATION_ERROR, + EXECUTION_ERROR, + UNRECOGNIZED_ERROR + } + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/audit/AuditService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/audit/AuditService.java new file mode 100644 index 00000000000..fc6000dfe8e --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/audit/AuditService.java @@ -0,0 +1,235 @@ +package com.provectus.kafka.ui.service.audit; + +import static com.provectus.kafka.ui.config.ClustersProperties.AuditProperties.LogLevel.ALTER_ONLY; +import static com.provectus.kafka.ui.service.MessagesService.createProducer; + +import com.google.common.annotations.VisibleForTesting; +import com.provectus.kafka.ui.config.ClustersProperties; +import com.provectus.kafka.ui.config.auth.AuthenticatedUser; +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.rbac.AccessContext; +import com.provectus.kafka.ui.service.AdminClientService; +import com.provectus.kafka.ui.service.ClustersStorage; +import com.provectus.kafka.ui.service.ReactiveAdminClient; +import java.io.Closeable; +import java.io.IOException; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Signal; + + +@Slf4j +@Service +public class AuditService implements Closeable { + + private static final Mono NO_AUTH_USER = Mono.just(new AuthenticatedUser("Unknown", Set.of())); + private static final Duration BLOCK_TIMEOUT = Duration.ofSeconds(5); + + private static final String DEFAULT_AUDIT_TOPIC_NAME = "__kui-audit-log"; + private static final int DEFAULT_AUDIT_TOPIC_PARTITIONS = 1; + private static final Map DEFAULT_AUDIT_TOPIC_CONFIG = Map.of( + "retention.ms", String.valueOf(TimeUnit.DAYS.toMillis(7)), + "cleanup.policy", "delete" + ); + private static final Map AUDIT_PRODUCER_CONFIG = Map.of( + ProducerConfig.COMPRESSION_TYPE_CONFIG, "gzip" + ); + + private static final Logger AUDIT_LOGGER = LoggerFactory.getLogger("audit"); + + private final Map auditWriters; + + @Autowired + public AuditService(AdminClientService adminClientService, ClustersStorage clustersStorage) { + Map auditWriters = new HashMap<>(); + for (var cluster : clustersStorage.getKafkaClusters()) { + Supplier adminClientSupplier = () -> adminClientService.get(cluster).block(BLOCK_TIMEOUT); + createAuditWriter(cluster, adminClientSupplier, () -> createProducer(cluster, AUDIT_PRODUCER_CONFIG)) + .ifPresent(writer -> auditWriters.put(cluster.getName(), writer)); + } + this.auditWriters = auditWriters; + } + + @VisibleForTesting + AuditService(Map auditWriters) { + this.auditWriters = auditWriters; + } + + @VisibleForTesting + static Optional createAuditWriter(KafkaCluster cluster, + Supplier acSupplier, + Supplier> producerFactory) { + var auditProps = cluster.getOriginalProperties().getAudit(); + if (auditProps == null) { + return Optional.empty(); + } + boolean topicAudit = Optional.ofNullable(auditProps.getTopicAuditEnabled()).orElse(false); + boolean consoleAudit = Optional.ofNullable(auditProps.getConsoleAuditEnabled()).orElse(false); + boolean alterLogOnly = Optional.ofNullable(auditProps.getLevel()).map(lvl -> lvl == ALTER_ONLY).orElse(true); + if (!topicAudit && !consoleAudit) { + return Optional.empty(); + } + if (!topicAudit) { + log.info("Audit initialization finished for cluster '{}' (console only)", cluster.getName()); + return Optional.of(consoleOnlyWriter(cluster, alterLogOnly)); + } + String auditTopicName = Optional.ofNullable(auditProps.getTopic()).orElse(DEFAULT_AUDIT_TOPIC_NAME); + boolean topicAuditCanBeDone = createTopicIfNeeded(cluster, acSupplier, auditTopicName, auditProps); + if (!topicAuditCanBeDone) { + if (consoleAudit) { + log.info( + "Audit initialization finished for cluster '{}' (console only, topic audit init failed)", + cluster.getName() + ); + return Optional.of(consoleOnlyWriter(cluster, alterLogOnly)); + } + return Optional.empty(); + } + log.info("Audit initialization finished for cluster '{}'", cluster.getName()); + return Optional.of( + new AuditWriter( + cluster.getName(), + alterLogOnly, + auditTopicName, + producerFactory.get(), + consoleAudit ? AUDIT_LOGGER : null + ) + ); + } + + private static AuditWriter consoleOnlyWriter(KafkaCluster cluster, boolean alterLogOnly) { + return new AuditWriter(cluster.getName(), alterLogOnly, null, null, AUDIT_LOGGER); + } + + /** + * return true if topic created/existing and producing can be enabled. + */ + private static boolean createTopicIfNeeded(KafkaCluster cluster, + Supplier acSupplier, + String auditTopicName, + ClustersProperties.AuditProperties auditProps) { + ReactiveAdminClient ac; + try { + ac = acSupplier.get(); + } catch (Exception e) { + printAuditInitError(cluster, "Error while connecting to the cluster", e); + return false; + } + boolean topicExists; + try { + topicExists = ac.listTopics(true).block(BLOCK_TIMEOUT).contains(auditTopicName); + } catch (Exception e) { + printAuditInitError(cluster, "Error checking audit topic existence", e); + return false; + } + if (topicExists) { + return true; + } + try { + int topicPartitions = + Optional.ofNullable(auditProps.getAuditTopicsPartitions()) + .orElse(DEFAULT_AUDIT_TOPIC_PARTITIONS); + + Map topicConfig = new HashMap<>(DEFAULT_AUDIT_TOPIC_CONFIG); + Optional.ofNullable(auditProps.getAuditTopicProperties()) + .ifPresent(topicConfig::putAll); + + log.info("Creating audit topic '{}' for cluster '{}'", auditTopicName, cluster.getName()); + ac.createTopic(auditTopicName, topicPartitions, null, topicConfig).block(BLOCK_TIMEOUT); + log.info("Audit topic created for cluster '{}'", cluster.getName()); + return true; + } catch (Exception e) { + printAuditInitError(cluster, "Error creating topic '%s'".formatted(auditTopicName), e); + return false; + } + } + + private static void printAuditInitError(KafkaCluster cluster, String errorMsg, Exception cause) { + log.error("-----------------------------------------------------------------"); + log.error( + "Error initializing Audit for cluster '{}'. Audit will be disabled. See error below: ", + cluster.getName() + ); + log.error("{}", errorMsg, cause); + log.error("-----------------------------------------------------------------"); + } + + public boolean isAuditTopic(KafkaCluster cluster, String topic) { + var writer = auditWriters.get(cluster.getName()); + return writer != null + && topic.equals(writer.targetTopic()) + && writer.isTopicWritingEnabled(); + } + + public void audit(AccessContext acxt, Signal sig) { + if (sig.isOnComplete()) { + extractUser(sig) + .doOnNext(u -> sendAuditRecord(acxt, u)) + .subscribe(); + } else if (sig.isOnError()) { + extractUser(sig) + .doOnNext(u -> sendAuditRecord(acxt, u, sig.getThrowable())) + .subscribe(); + } + } + + private Mono extractUser(Signal sig) { + //see ReactiveSecurityContextHolder for impl details + Object key = SecurityContext.class; + if (sig.getContextView().hasKey(key)) { + return sig.getContextView().>get(key) + .map(context -> context.getAuthentication().getPrincipal()) + .cast(UserDetails.class) + .map(user -> { + var roles = user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet()); + return new AuthenticatedUser(user.getUsername(), roles); + }) + .switchIfEmpty(NO_AUTH_USER); + } else { + return NO_AUTH_USER; + } + } + + private void sendAuditRecord(AccessContext ctx, AuthenticatedUser user) { + sendAuditRecord(ctx, user, null); + } + + private void sendAuditRecord(AccessContext ctx, AuthenticatedUser user, @Nullable Throwable th) { + try { + if (ctx.getCluster() != null) { + var writer = auditWriters.get(ctx.getCluster()); + if (writer != null) { + writer.write(ctx, user, th); + } + } else { + // cluster-independent operation + AuditWriter.writeAppOperation(AUDIT_LOGGER, ctx, user, th); + } + } catch (Exception e) { + log.warn("Error sending audit record", e); + } + } + + @Override + public void close() throws IOException { + auditWriters.values().forEach(AuditWriter::close); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/audit/AuditWriter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/audit/AuditWriter.java new file mode 100644 index 00000000000..fabcba70ae6 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/audit/AuditWriter.java @@ -0,0 +1,83 @@ +package com.provectus.kafka.ui.service.audit; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.provectus.kafka.ui.config.auth.AuthenticatedUser; +import com.provectus.kafka.ui.model.rbac.AccessContext; +import com.provectus.kafka.ui.service.audit.AuditRecord.AuditResource; +import com.provectus.kafka.ui.service.audit.AuditRecord.OperationResult; +import java.io.Closeable; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Optional; +import javax.annotation.Nullable; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.slf4j.Logger; + +@Slf4j +record AuditWriter(String clusterName, + boolean logAlterOperationsOnly, + @Nullable String targetTopic, + @Nullable KafkaProducer producer, + @Nullable Logger consoleLogger) implements Closeable { + + boolean isTopicWritingEnabled() { + return producer != null; + } + + // application-level (cluster-independent) operation + static void writeAppOperation(Logger consoleLogger, + AccessContext ctx, + AuthenticatedUser user, + @Nullable Throwable th) { + consoleLogger.info(createRecord(ctx, user, th).toJson()); + } + + void write(AccessContext ctx, AuthenticatedUser user, @Nullable Throwable th) { + write(createRecord(ctx, user, th)); + } + + private void write(AuditRecord rec) { + if (logAlterOperationsOnly && rec.resources().stream().noneMatch(AuditResource::alter)) { + //we should only log alter operations, but this is read-only op + return; + } + String json = rec.toJson(); + if (consoleLogger != null) { + consoleLogger.info(json); + } + if (targetTopic != null && producer != null) { + producer.send( + new ProducerRecord<>(targetTopic, null, json.getBytes(UTF_8)), + (metadata, ex) -> { + if (ex != null) { + log.warn("Error sending Audit record to kafka for cluster {}", clusterName, ex); + } + }); + } + } + + private static AuditRecord createRecord(AccessContext ctx, + AuthenticatedUser user, + @Nullable Throwable th) { + return new AuditRecord( + DateTimeFormatter.ISO_INSTANT.format(Instant.now()), + user.principal(), + ctx.getCluster(), //can be null, if it is application-level action + AuditResource.getAccessedResources(ctx), + ctx.getOperationName(), + ctx.getOperationParams(), + th == null ? OperationResult.successful() : OperationResult.error(th) + ); + } + + @Override + public void close() { + Optional.ofNullable(producer).ifPresent(KafkaProducer::close); + } + +} + + diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/ConnectorInfo.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/ConnectorInfo.java new file mode 100644 index 00000000000..daec183a4cc --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/ConnectorInfo.java @@ -0,0 +1,167 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import com.provectus.kafka.ui.model.ConnectorTypeDTO; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.apache.commons.collections4.CollectionUtils; +import org.opendatadiscovery.oddrn.JdbcUrlParser; +import org.opendatadiscovery.oddrn.model.HivePath; +import org.opendatadiscovery.oddrn.model.MysqlPath; +import org.opendatadiscovery.oddrn.model.PostgreSqlPath; +import org.opendatadiscovery.oddrn.model.SnowflakePath; + +record ConnectorInfo(List inputs, + List outputs) { + + static ConnectorInfo extract(String className, + ConnectorTypeDTO type, + Map config, + List topicsFromApi, // can be empty for old Connect API versions + Function topicOddrnBuilder) { + return switch (className) { + case "org.apache.kafka.connect.file.FileStreamSinkConnector", + "org.apache.kafka.connect.file.FileStreamSourceConnector", + "FileStreamSource", + "FileStreamSink" -> extractFileIoConnector(type, topicsFromApi, config, topicOddrnBuilder); + case "io.confluent.connect.s3.S3SinkConnector" -> extractS3Sink(type, topicsFromApi, config, topicOddrnBuilder); + case "io.confluent.connect.jdbc.JdbcSinkConnector" -> + extractJdbcSink(type, topicsFromApi, config, topicOddrnBuilder); + case "io.debezium.connector.postgresql.PostgresConnector" -> extractDebeziumPg(config); + case "io.debezium.connector.mysql.MySqlConnector" -> extractDebeziumMysql(config); + default -> new ConnectorInfo( + extractInputs(type, topicsFromApi, config, topicOddrnBuilder), + extractOutputs(type, topicsFromApi, config, topicOddrnBuilder) + ); + }; + } + + private static ConnectorInfo extractFileIoConnector(ConnectorTypeDTO type, + List topics, + Map config, + Function topicOddrnBuilder) { + return new ConnectorInfo( + extractInputs(type, topics, config, topicOddrnBuilder), + extractOutputs(type, topics, config, topicOddrnBuilder) + ); + } + + private static ConnectorInfo extractJdbcSink(ConnectorTypeDTO type, + List topics, + Map config, + Function topicOddrnBuilder) { + String tableNameFormat = (String) config.getOrDefault("table.name.format", "${topic}"); + List targetTables = extractTopicNamesBestEffort(topics, config) + .map(topic -> tableNameFormat.replace("${kafka}", topic)) + .toList(); + + String connectionUrl = (String) config.get("connection.url"); + List outputs = new ArrayList<>(); + @Nullable var knownJdbcPath = new JdbcUrlParser().parse(connectionUrl); + if (knownJdbcPath instanceof PostgreSqlPath p) { + targetTables.forEach(t -> outputs.add(p.toBuilder().table(t).build().oddrn())); + } + if (knownJdbcPath instanceof MysqlPath p) { + targetTables.forEach(t -> outputs.add(p.toBuilder().table(t).build().oddrn())); + } + if (knownJdbcPath instanceof HivePath p) { + targetTables.forEach(t -> outputs.add(p.toBuilder().table(t).build().oddrn())); + } + if (knownJdbcPath instanceof SnowflakePath p) { + targetTables.forEach(t -> outputs.add(p.toBuilder().table(t).build().oddrn())); + } + return new ConnectorInfo( + extractInputs(type, topics, config, topicOddrnBuilder), + outputs + ); + } + + private static ConnectorInfo extractDebeziumPg(Map config) { + String host = (String) config.get("database.hostname"); + String dbName = (String) config.get("database.dbname"); + var inputs = List.of( + PostgreSqlPath.builder() + .host(host) + .database(dbName) + .build().oddrn() + ); + return new ConnectorInfo(inputs, List.of()); + } + + private static ConnectorInfo extractDebeziumMysql(Map config) { + String host = (String) config.get("database.hostname"); + var inputs = List.of( + MysqlPath.builder() + .host(host) + .build() + .oddrn() + ); + return new ConnectorInfo(inputs, List.of()); + } + + private static ConnectorInfo extractS3Sink(ConnectorTypeDTO type, + List topics, + Map config, + Function topicOrrdnBuilder) { + String bucketName = (String) config.get("s3.bucket.name"); + String topicsDir = (String) config.getOrDefault("topics.dir", "topics"); + String directoryDelim = (String) config.getOrDefault("directory.delim", "/"); + List outputs = extractTopicNamesBestEffort(topics, config) + .map(topic -> Oddrn.awsS3Oddrn(bucketName, topicsDir + directoryDelim + topic)) + .toList(); + return new ConnectorInfo( + extractInputs(type, topics, config, topicOrrdnBuilder), + outputs + ); + } + + private static List extractInputs(ConnectorTypeDTO type, + List topicsFromApi, + Map config, + Function topicOrrdnBuilder) { + return type == ConnectorTypeDTO.SINK + ? extractTopicsOddrns(config, topicsFromApi, topicOrrdnBuilder) + : List.of(); + } + + private static List extractOutputs(ConnectorTypeDTO type, + List topicsFromApi, + Map config, + Function topicOrrdnBuilder) { + return type == ConnectorTypeDTO.SOURCE + ? extractTopicsOddrns(config, topicsFromApi, topicOrrdnBuilder) + : List.of(); + } + + private static Stream extractTopicNamesBestEffort( + // topic list can be empty for old Connect API versions + List topicsFromApi, + Map config + ) { + if (CollectionUtils.isNotEmpty(topicsFromApi)) { + return topicsFromApi.stream(); + } + + // trying to extract topic names from config + String topicsString = (String) config.get("topics"); + String topicString = (String) config.get("topic"); + return Stream.of(topicsString, topicString) + .filter(Objects::nonNull) + .flatMap(str -> Stream.of(str.split(","))) + .map(String::trim) + .filter(s -> !s.isBlank()); + } + + private static List extractTopicsOddrns(Map config, + List topicsFromApi, + Function topicOrrdnBuilder) { + return extractTopicNamesBestEffort(topicsFromApi, config) + .map(topicOrrdnBuilder) + .toList(); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/ConnectorsExporter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/ConnectorsExporter.java new file mode 100644 index 00000000000..2259d5ebb1b --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/ConnectorsExporter.java @@ -0,0 +1,96 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import com.provectus.kafka.ui.connect.model.ConnectorTopics; +import com.provectus.kafka.ui.model.ConnectDTO; +import com.provectus.kafka.ui.model.ConnectorDTO; +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.service.KafkaConnectService; +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.opendatadiscovery.client.model.DataEntity; +import org.opendatadiscovery.client.model.DataEntityList; +import org.opendatadiscovery.client.model.DataEntityType; +import org.opendatadiscovery.client.model.DataSource; +import org.opendatadiscovery.client.model.DataTransformer; +import org.opendatadiscovery.client.model.MetadataExtension; +import reactor.core.publisher.Flux; + +@RequiredArgsConstructor +class ConnectorsExporter { + + private final KafkaConnectService kafkaConnectService; + + Flux export(KafkaCluster cluster) { + return kafkaConnectService.getConnects(cluster) + .flatMap(connect -> kafkaConnectService.getConnectorNamesWithErrorsSuppress(cluster, connect.getName()) + .flatMap(connectorName -> kafkaConnectService.getConnector(cluster, connect.getName(), connectorName)) + .flatMap(connectorDTO -> + kafkaConnectService.getConnectorTopics(cluster, connect.getName(), connectorDTO.getName()) + .map(topics -> createConnectorDataEntity(cluster, connect, connectorDTO, topics))) + .buffer(100) + .map(connectDataEntities -> { + String dsOddrn = Oddrn.connectDataSourceOddrn(connect.getAddress()); + return new DataEntityList() + .dataSourceOddrn(dsOddrn) + .items(connectDataEntities); + }) + ); + } + + Flux getConnectDataSources(KafkaCluster cluster) { + return kafkaConnectService.getConnects(cluster) + .map(ConnectorsExporter::toDataSource); + } + + private static DataSource toDataSource(ConnectDTO connect) { + return new DataSource() + .oddrn(Oddrn.connectDataSourceOddrn(connect.getAddress())) + .name(connect.getName()) + .description("Kafka Connect"); + } + + private static DataEntity createConnectorDataEntity(KafkaCluster cluster, + ConnectDTO connect, + ConnectorDTO connector, + ConnectorTopics connectorTopics) { + var metadata = new HashMap<>(extractMetadata(connector)); + metadata.put("type", connector.getType().name()); + + var info = extractConnectorInfo(cluster, connector, connectorTopics); + DataTransformer transformer = new DataTransformer(); + transformer.setInputs(info.inputs()); + transformer.setOutputs(info.outputs()); + + return new DataEntity() + .oddrn(Oddrn.connectorOddrn(connect.getAddress(), connector.getName())) + .name(connector.getName()) + .description("Kafka Connector \"%s\" (%s)".formatted(connector.getName(), connector.getType())) + .type(DataEntityType.JOB) + .dataTransformer(transformer) + .metadata(List.of( + new MetadataExtension() + .schemaUrl(URI.create("wontbeused.oops")) + .metadata(metadata))); + } + + private static Map extractMetadata(ConnectorDTO connector) { + // will be sanitized by KafkaConfigSanitizer (if it's enabled) + return connector.getConfig(); + } + + private static ConnectorInfo extractConnectorInfo(KafkaCluster cluster, + ConnectorDTO connector, + ConnectorTopics topics) { + return ConnectorInfo.extract( + (String) connector.getConfig().get("connector.class"), + connector.getType(), + connector.getConfig(), + topics.getTopics(), + topic -> Oddrn.topicOddrn(cluster, topic) + ); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddExporter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddExporter.java new file mode 100644 index 00000000000..c95062cc1ef --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddExporter.java @@ -0,0 +1,103 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.service.KafkaConnectService; +import com.provectus.kafka.ui.service.StatisticsCache; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import org.opendatadiscovery.client.ApiClient; +import org.opendatadiscovery.client.api.OpenDataDiscoveryIngestionApi; +import org.opendatadiscovery.client.model.DataEntityList; +import org.opendatadiscovery.client.model.DataSource; +import org.opendatadiscovery.client.model.DataSourceList; +import org.springframework.http.HttpHeaders; +import reactor.core.publisher.Mono; + +class OddExporter { + + private final OpenDataDiscoveryIngestionApi oddApi; + private final TopicsExporter topicsExporter; + private final ConnectorsExporter connectorsExporter; + + public OddExporter(StatisticsCache statisticsCache, + KafkaConnectService connectService, + OddIntegrationProperties oddIntegrationProperties) { + this( + createApiClient(oddIntegrationProperties), + new TopicsExporter(createTopicsFilter(oddIntegrationProperties), statisticsCache), + new ConnectorsExporter(connectService) + ); + } + + @VisibleForTesting + OddExporter(OpenDataDiscoveryIngestionApi oddApi, + TopicsExporter topicsExporter, + ConnectorsExporter connectorsExporter) { + this.oddApi = oddApi; + this.topicsExporter = topicsExporter; + this.connectorsExporter = connectorsExporter; + } + + private static Predicate createTopicsFilter(OddIntegrationProperties properties) { + if (properties.getTopicsRegex() == null) { + return topic -> !topic.startsWith("_"); + } + Pattern pattern = Pattern.compile(properties.getTopicsRegex()); + return topic -> pattern.matcher(topic).matches(); + } + + private static OpenDataDiscoveryIngestionApi createApiClient(OddIntegrationProperties properties) { + Preconditions.checkNotNull(properties.getUrl(), "ODD url not set"); + Preconditions.checkNotNull(properties.getToken(), "ODD token not set"); + var apiClient = new ApiClient() + .setBasePath(properties.getUrl()) + .addDefaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + properties.getToken()); + return new OpenDataDiscoveryIngestionApi(apiClient); + } + + public Mono export(KafkaCluster cluster) { + return exportTopics(cluster) + .then(exportKafkaConnects(cluster)); + } + + private Mono exportTopics(KafkaCluster c) { + return createKafkaDataSource(c) + .thenMany(topicsExporter.export(c)) + .concatMap(this::sendDataEntities) + .then(); + } + + private Mono exportKafkaConnects(KafkaCluster cluster) { + return createConnectDataSources(cluster) + .thenMany(connectorsExporter.export(cluster)) + .concatMap(this::sendDataEntities) + .then(); + } + + private Mono createConnectDataSources(KafkaCluster cluster) { + return connectorsExporter.getConnectDataSources(cluster) + .buffer(100) + .concatMap(dataSources -> oddApi.createDataSource(new DataSourceList().items(dataSources))) + .then(); + } + + private Mono createKafkaDataSource(KafkaCluster cluster) { + String clusterOddrn = Oddrn.clusterOddrn(cluster); + return oddApi.createDataSource( + new DataSourceList() + .addItemsItem( + new DataSource() + .oddrn(clusterOddrn) + .name(cluster.getName()) + .description("Kafka cluster") + ) + ); + } + + private Mono sendDataEntities(DataEntityList dataEntityList) { + return oddApi.postDataEntityList(dataEntityList); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddExporterScheduler.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddExporterScheduler.java new file mode 100644 index 00000000000..7201737f9f8 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddExporterScheduler.java @@ -0,0 +1,27 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import com.provectus.kafka.ui.service.ClustersStorage; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import reactor.core.publisher.Flux; +import reactor.core.scheduler.Schedulers; + +@RequiredArgsConstructor +class OddExporterScheduler { + + private final ClustersStorage clustersStorage; + private final OddExporter oddExporter; + + @Scheduled(fixedRateString = "${kafka.send-stats-to-odd-millis:30000}") + public void sendMetricsToOdd() { + Flux.fromIterable(clustersStorage.getKafkaClusters()) + .parallel() + .runOn(Schedulers.parallel()) + .flatMap(oddExporter::export) + .then() + .block(); + } + + +} + diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddIntegrationConfig.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddIntegrationConfig.java new file mode 100644 index 00000000000..6bade3022ac --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddIntegrationConfig.java @@ -0,0 +1,31 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import com.provectus.kafka.ui.service.ClustersStorage; +import com.provectus.kafka.ui.service.KafkaConnectService; +import com.provectus.kafka.ui.service.StatisticsCache; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnProperty(value = "integration.odd.url") +class OddIntegrationConfig { + + @Bean + OddIntegrationProperties oddIntegrationProperties() { + return new OddIntegrationProperties(); + } + + @Bean + OddExporter oddExporter(StatisticsCache statisticsCache, + KafkaConnectService connectService, + OddIntegrationProperties oddIntegrationProperties) { + return new OddExporter(statisticsCache, connectService, oddIntegrationProperties); + } + + @Bean + OddExporterScheduler oddExporterScheduler(ClustersStorage storage, OddExporter exporter) { + return new OddExporterScheduler(storage, exporter); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddIntegrationProperties.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddIntegrationProperties.java new file mode 100644 index 00000000000..cbb8d89238e --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddIntegrationProperties.java @@ -0,0 +1,15 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + + +@Data +@ConfigurationProperties("integration.odd") +public class OddIntegrationProperties { + + String url; + String token; + String topicsRegex; + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/Oddrn.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/Oddrn.java new file mode 100644 index 00000000000..00b29b3b8b4 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/Oddrn.java @@ -0,0 +1,77 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import com.provectus.kafka.ui.model.KafkaCluster; +import java.net.URI; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.opendatadiscovery.oddrn.model.AwsS3Path; +import org.opendatadiscovery.oddrn.model.KafkaConnectorPath; +import org.opendatadiscovery.oddrn.model.KafkaPath; + +public final class Oddrn { + + private Oddrn() { + } + + static String clusterOddrn(KafkaCluster cluster) { + return KafkaPath.builder() + .cluster(bootstrapServersForOddrn(cluster.getBootstrapServers())) + .build() + .oddrn(); + } + + static KafkaPath topicOddrnPath(KafkaCluster cluster, String topic) { + return KafkaPath.builder() + .cluster(bootstrapServersForOddrn(cluster.getBootstrapServers())) + .topic(topic) + .build(); + } + + static String topicOddrn(KafkaCluster cluster, String topic) { + return topicOddrnPath(cluster, topic).oddrn(); + } + + static String awsS3Oddrn(String bucket, String key) { + return AwsS3Path.builder() + .bucket(bucket) + .key(key) + .build() + .oddrn(); + } + + static String connectDataSourceOddrn(String connectUrl) { + return KafkaConnectorPath.builder() + .host(normalizedConnectHosts(connectUrl)) + .build() + .oddrn(); + } + + private static String normalizedConnectHosts(String connectUrlStr) { + return Stream.of(connectUrlStr.split(",")) + .map(String::trim) + .sorted() + .map(url -> { + var uri = URI.create(url); + String host = uri.getHost(); + String portSuffix = (uri.getPort() > 0 ? (":" + uri.getPort()) : ""); + return host + portSuffix; + }) + .collect(Collectors.joining(",")); + } + + static String connectorOddrn(String connectUrl, String connectorName) { + return KafkaConnectorPath.builder() + .host(normalizedConnectHosts(connectUrl)) + .connector(connectorName) + .build() + .oddrn(); + } + + private static String bootstrapServersForOddrn(String bootstrapServers) { + return Stream.of(bootstrapServers.split(",")) + .map(String::trim) + .sorted() + .collect(Collectors.joining(",")); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/SchemaReferencesResolver.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/SchemaReferencesResolver.java new file mode 100644 index 00000000000..4ff1f8695b3 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/SchemaReferencesResolver.java @@ -0,0 +1,55 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.provectus.kafka.ui.sr.api.KafkaSrClientApi; +import com.provectus.kafka.ui.sr.model.SchemaReference; +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; +import reactor.core.publisher.Mono; + +// logic copied from AbstractSchemaProvider:resolveReferences +// https://github.com/confluentinc/schema-registry/blob/fd59613e2c5adf62e36705307f420712e4c8c1ea/client/src/main/java/io/confluent/kafka/schemaregistry/AbstractSchemaProvider.java#L54 +class SchemaReferencesResolver { + + private final KafkaSrClientApi client; + + SchemaReferencesResolver(KafkaSrClientApi client) { + this.client = client; + } + + Mono> resolve(List refs) { + return resolveReferences(refs, new Resolving(ImmutableMap.of(), ImmutableSet.of())) + .map(Resolving::resolved); + } + + private record Resolving(ImmutableMap resolved, ImmutableSet visited) { + + Resolving visit(String name) { + return new Resolving(resolved, ImmutableSet.builder().addAll(visited).add(name).build()); + } + + Resolving resolve(String ref, String schema) { + return new Resolving(ImmutableMap.builder().putAll(resolved).put(ref, schema).build(), visited); + } + } + + private Mono resolveReferences(@Nullable List refs, Resolving initState) { + Mono result = Mono.just(initState); + for (SchemaReference reference : Optional.ofNullable(refs).orElse(List.of())) { + result = result.flatMap(state -> { + if (state.visited().contains(reference.getName())) { + return Mono.just(state); + } else { + final var newState = state.visit(reference.getName()); + return client.getSubjectVersion(reference.getSubject(), String.valueOf(reference.getVersion()), true) + .flatMap(subj -> + resolveReferences(subj.getReferences(), newState) + .map(withNewRefs -> withNewRefs.resolve(reference.getName(), subj.getSchema()))); + } + }); + } + return result; + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/TopicsExporter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/TopicsExporter.java new file mode 100644 index 00000000000..8f4ef2781be --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/TopicsExporter.java @@ -0,0 +1,122 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import com.google.common.collect.ImmutableMap; +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.Statistics; +import com.provectus.kafka.ui.service.StatisticsCache; +import com.provectus.kafka.ui.service.integration.odd.schema.DataSetFieldsExtractors; +import com.provectus.kafka.ui.sr.model.SchemaSubject; +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.admin.ConfigEntry; +import org.apache.kafka.clients.admin.TopicDescription; +import org.opendatadiscovery.client.model.DataEntity; +import org.opendatadiscovery.client.model.DataEntityList; +import org.opendatadiscovery.client.model.DataEntityType; +import org.opendatadiscovery.client.model.DataSet; +import org.opendatadiscovery.client.model.DataSetField; +import org.opendatadiscovery.client.model.MetadataExtension; +import org.opendatadiscovery.oddrn.model.KafkaPath; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +@Slf4j +@RequiredArgsConstructor +class TopicsExporter { + + private final Predicate topicFilter; + private final StatisticsCache statisticsCache; + + Flux export(KafkaCluster cluster) { + String clusterOddrn = Oddrn.clusterOddrn(cluster); + Statistics stats = statisticsCache.get(cluster); + return Flux.fromIterable(stats.getTopicDescriptions().keySet()) + .filter(topicFilter) + .flatMap(topic -> createTopicDataEntity(cluster, topic, stats)) + .onErrorContinue( + (th, topic) -> log.warn("Error exporting data for topic {}, cluster {}", topic, cluster.getName(), th)) + .buffer(100) + .map(topicsEntities -> + new DataEntityList() + .dataSourceOddrn(clusterOddrn) + .items(topicsEntities)); + } + + private Mono createTopicDataEntity(KafkaCluster cluster, String topic, Statistics stats) { + KafkaPath topicOddrnPath = Oddrn.topicOddrnPath(cluster, topic); + return + Mono.zip( + getTopicSchema(cluster, topic, topicOddrnPath, true), + getTopicSchema(cluster, topic, topicOddrnPath, false) + ) + .map(keyValueFields -> { + var dataset = new DataSet(); + keyValueFields.getT1().forEach(dataset::addFieldListItem); + keyValueFields.getT2().forEach(dataset::addFieldListItem); + return new DataEntity() + .name(topic) + .description("Kafka topic \"%s\"".formatted(topic)) + .oddrn(Oddrn.topicOddrn(cluster, topic)) + .type(DataEntityType.KAFKA_TOPIC) + .dataset(dataset) + .addMetadataItem( + new MetadataExtension() + .schemaUrl(URI.create("wontbeused.oops")) + .metadata(getTopicMetadata(topic, stats))); + } + ); + } + + private Map getNonDefaultConfigs(String topic, Statistics stats) { + List config = stats.getTopicConfigs().get(topic); + if (config == null) { + return Map.of(); + } + return config.stream() + .filter(c -> c.source() == ConfigEntry.ConfigSource.DYNAMIC_TOPIC_CONFIG) + .collect(Collectors.toMap(ConfigEntry::name, ConfigEntry::value)); + } + + private Map getTopicMetadata(String topic, Statistics stats) { + TopicDescription topicDescription = stats.getTopicDescriptions().get(topic); + return ImmutableMap.builder() + .put("partitions", topicDescription.partitions().size()) + .put("replication_factor", topicDescription.partitions().get(0).replicas().size()) + .putAll(getNonDefaultConfigs(topic, stats)) + .build(); + } + + //returns empty list if schemaRegistry is not configured or assumed subject not found + private Mono> getTopicSchema(KafkaCluster cluster, + String topic, + KafkaPath topicOddrn, + boolean isKey) { + if (cluster.getSchemaRegistryClient() == null) { + return Mono.just(List.of()); + } + String subject = topic + (isKey ? "-key" : "-value"); + return getSubjWithResolvedRefs(cluster, subject) + .map(t -> DataSetFieldsExtractors.extract(t.getT1(), t.getT2(), topicOddrn, isKey)) + .onErrorResume(WebClientResponseException.NotFound.class, th -> Mono.just(List.of())) + .onErrorMap(WebClientResponseException.class, err -> + new IllegalStateException("Error retrieving subject %s".formatted(subject), err)); + } + + private Mono>> getSubjWithResolvedRefs(KafkaCluster cluster, + String subjectName) { + return cluster.getSchemaRegistryClient() + .mono(client -> + client.getSubjectVersion(subjectName, "latest", false) + .flatMap(subj -> new SchemaReferencesResolver(client).resolve(subj.getReferences()) + .map(resolvedRefs -> Tuples.of(subj, resolvedRefs)))); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/AvroExtractor.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/AvroExtractor.java new file mode 100644 index 00000000000..f9423962933 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/AvroExtractor.java @@ -0,0 +1,262 @@ +package com.provectus.kafka.ui.service.integration.odd.schema; + +import com.google.common.collect.ImmutableSet; +import io.confluent.kafka.schemaregistry.avro.AvroSchema; +import java.util.ArrayList; +import java.util.List; +import org.apache.avro.Schema; +import org.opendatadiscovery.client.model.DataSetField; +import org.opendatadiscovery.client.model.DataSetFieldType; +import org.opendatadiscovery.oddrn.model.KafkaPath; + +final class AvroExtractor { + + private AvroExtractor() { + } + + static List extract(AvroSchema avroSchema, KafkaPath topicOddrn, boolean isKey) { + var schema = avroSchema.rawSchema(); + List result = new ArrayList<>(); + result.add(DataSetFieldsExtractors.rootField(topicOddrn, isKey)); + extract( + schema, + topicOddrn.oddrn() + "/columns/" + (isKey ? "key" : "value"), + null, + null, + null, + false, + ImmutableSet.of(), + result + ); + return result; + } + + private static void extract(Schema schema, + String parentOddr, + String oddrn, //null for root + String name, + String doc, + Boolean nullable, + ImmutableSet registeredRecords, + List sink + ) { + switch (schema.getType()) { + case RECORD -> extractRecord(schema, parentOddr, oddrn, name, doc, nullable, registeredRecords, sink); + case UNION -> extractUnion(schema, parentOddr, oddrn, name, doc, registeredRecords, sink); + case ARRAY -> extractArray(schema, parentOddr, oddrn, name, doc, nullable, registeredRecords, sink); + case MAP -> extractMap(schema, parentOddr, oddrn, name, doc, nullable, registeredRecords, sink); + default -> extractPrimitive(schema, parentOddr, oddrn, name, doc, nullable, sink); + } + } + + private static DataSetField createDataSetField(String name, + String doc, + String parentOddrn, + String oddrn, + Schema schema, + Boolean nullable) { + return new DataSetField() + .name(name) + .description(doc) + .parentFieldOddrn(parentOddrn) + .oddrn(oddrn) + .type(mapSchema(schema, nullable)); + } + + private static void extractRecord(Schema schema, + String parentOddr, + String oddrn, //null for root + String name, + String doc, + Boolean nullable, + ImmutableSet registeredRecords, + List sink) { + boolean isRoot = oddrn == null; + if (!isRoot) { + sink.add(createDataSetField(name, doc, parentOddr, oddrn, schema, nullable)); + if (registeredRecords.contains(schema.getFullName())) { + // avoiding recursion by checking if record already registered in parsing chain + return; + } + } + var newRegisteredRecords = ImmutableSet.builder() + .addAll(registeredRecords) + .add(schema.getFullName()) + .build(); + + schema.getFields().forEach(f -> + extract( + f.schema(), + isRoot ? parentOddr : oddrn, + isRoot + ? parentOddr + "/" + f.name() + : oddrn + "/fields/" + f.name(), + f.name(), + f.doc(), + false, + newRegisteredRecords, + sink + )); + } + + private static void extractUnion(Schema schema, + String parentOddr, + String oddrn, //null for root + String name, + String doc, + ImmutableSet registeredRecords, + List sink) { + boolean isRoot = oddrn == null; + boolean containsNull = schema.getTypes().stream().map(Schema::getType).anyMatch(t -> t == Schema.Type.NULL); + // if it is not root and there is only 2 values for union (null and smth else) + // we registering this field as optional without mentioning union + if (!isRoot && containsNull && schema.getTypes().size() == 2) { + var nonNullSchema = schema.getTypes().stream() + .filter(s -> s.getType() != Schema.Type.NULL) + .findFirst() + .orElseThrow(IllegalStateException::new); + extract( + nonNullSchema, + parentOddr, + oddrn, + name, + doc, + true, + registeredRecords, + sink + ); + return; + } + oddrn = isRoot ? parentOddr + "/union" : oddrn; + if (isRoot) { + sink.add(createDataSetField("Avro root union", doc, parentOddr, oddrn, schema, containsNull)); + } else { + sink.add(createDataSetField(name, doc, parentOddr, oddrn, schema, containsNull)); + } + for (Schema t : schema.getTypes()) { + if (t.getType() != Schema.Type.NULL) { + extract( + t, + oddrn, + oddrn + "/values/" + t.getName(), + t.getName(), + t.getDoc(), + containsNull, + registeredRecords, + sink + ); + } + } + } + + private static void extractArray(Schema schema, + String parentOddr, + String oddrn, //null for root + String name, + String doc, + Boolean nullable, + ImmutableSet registeredRecords, + List sink) { + boolean isRoot = oddrn == null; + oddrn = isRoot ? parentOddr + "/array" : oddrn; + if (isRoot) { + sink.add(createDataSetField("Avro root Array", doc, parentOddr, oddrn, schema, nullable)); + } else { + sink.add(createDataSetField(name, doc, parentOddr, oddrn, schema, nullable)); + } + extract( + schema.getElementType(), + oddrn, + oddrn + "/items/" + schema.getElementType().getName(), + schema.getElementType().getName(), + schema.getElementType().getDoc(), + false, + registeredRecords, + sink + ); + } + + private static void extractMap(Schema schema, + String parentOddr, + String oddrn, //null for root + String name, + String doc, + Boolean nullable, + ImmutableSet registeredRecords, + List sink) { + boolean isRoot = oddrn == null; + oddrn = isRoot ? parentOddr + "/map" : oddrn; + if (isRoot) { + sink.add(createDataSetField("Avro root map", doc, parentOddr, oddrn, schema, nullable)); + } else { + sink.add(createDataSetField(name, doc, parentOddr, oddrn, schema, nullable)); + } + extract( + new Schema.Parser().parse("\"string\""), + oddrn, + oddrn + "/key", + "key", + null, + nullable, + registeredRecords, + sink + ); + extract( + schema.getValueType(), + oddrn, + oddrn + "/value", + "value", + null, + nullable, + registeredRecords, + sink + ); + } + + + private static void extractPrimitive(Schema schema, + String parentOddr, + String oddrn, //null for root + String name, + String doc, + Boolean nullable, + List sink) { + boolean isRoot = oddrn == null; + String primOddrn = isRoot ? (parentOddr + "/" + schema.getType()) : oddrn; + if (isRoot) { + sink.add(createDataSetField("Root avro " + schema.getType(), + doc, parentOddr, primOddrn, schema, nullable)); + } else { + sink.add(createDataSetField(name, doc, parentOddr, primOddrn, schema, nullable)); + } + } + + private static DataSetFieldType.TypeEnum mapType(Schema.Type type) { + return switch (type) { + case INT, LONG -> DataSetFieldType.TypeEnum.INTEGER; + case FLOAT, DOUBLE, FIXED -> DataSetFieldType.TypeEnum.NUMBER; + case STRING, ENUM -> DataSetFieldType.TypeEnum.STRING; + case BOOLEAN -> DataSetFieldType.TypeEnum.BOOLEAN; + case BYTES -> DataSetFieldType.TypeEnum.BINARY; + case ARRAY -> DataSetFieldType.TypeEnum.LIST; + case RECORD -> DataSetFieldType.TypeEnum.STRUCT; + case MAP -> DataSetFieldType.TypeEnum.MAP; + case UNION -> DataSetFieldType.TypeEnum.UNION; + case NULL -> DataSetFieldType.TypeEnum.UNKNOWN; + }; + } + + private static DataSetFieldType mapSchema(Schema schema, Boolean nullable) { + return new DataSetFieldType() + .logicalType(logicalType(schema)) + .isNullable(nullable) + .type(mapType(schema.getType())); + } + + private static String logicalType(Schema schema) { + return schema.getType() == Schema.Type.RECORD + ? schema.getFullName() + : schema.getType().toString().toLowerCase(); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/DataSetFieldsExtractors.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/DataSetFieldsExtractors.java new file mode 100644 index 00000000000..b9093262bd6 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/DataSetFieldsExtractors.java @@ -0,0 +1,45 @@ +package com.provectus.kafka.ui.service.integration.odd.schema; + +import com.provectus.kafka.ui.sr.model.SchemaSubject; +import com.provectus.kafka.ui.sr.model.SchemaType; +import io.confluent.kafka.schemaregistry.avro.AvroSchema; +import io.confluent.kafka.schemaregistry.json.JsonSchema; +import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.opendatadiscovery.client.model.DataSetField; +import org.opendatadiscovery.client.model.DataSetFieldType; +import org.opendatadiscovery.oddrn.model.KafkaPath; + +public final class DataSetFieldsExtractors { + + public static List extract(SchemaSubject subject, + Map resolvedRefs, + KafkaPath topicOddrn, + boolean isKey) { + SchemaType schemaType = Optional.ofNullable(subject.getSchemaType()).orElse(SchemaType.AVRO); + return switch (schemaType) { + case AVRO -> AvroExtractor.extract( + new AvroSchema(subject.getSchema(), List.of(), resolvedRefs, null), topicOddrn, isKey); + case JSON -> JsonSchemaExtractor.extract( + new JsonSchema(subject.getSchema(), List.of(), resolvedRefs, null), topicOddrn, isKey); + case PROTOBUF -> ProtoExtractor.extract( + new ProtobufSchema(subject.getSchema(), List.of(), resolvedRefs, null, null), topicOddrn, isKey); + }; + } + + + static DataSetField rootField(KafkaPath topicOddrn, boolean isKey) { + var rootOddrn = topicOddrn.oddrn() + "/columns/" + (isKey ? "key" : "value"); + return new DataSetField() + .name(isKey ? "key" : "value") + .description("Topic's " + (isKey ? "key" : "value") + " schema") + .parentFieldOddrn(topicOddrn.oddrn()) + .oddrn(rootOddrn) + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRUCT) + .isNullable(true)); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/JsonSchemaExtractor.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/JsonSchemaExtractor.java new file mode 100644 index 00000000000..93adbdbe0cc --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/JsonSchemaExtractor.java @@ -0,0 +1,311 @@ +package com.provectus.kafka.ui.service.integration.odd.schema; + +import com.google.common.collect.ImmutableSet; +import com.provectus.kafka.ui.sr.model.SchemaSubject; +import io.confluent.kafka.schemaregistry.json.JsonSchema; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.annotation.Nullable; +import org.everit.json.schema.ArraySchema; +import org.everit.json.schema.BooleanSchema; +import org.everit.json.schema.CombinedSchema; +import org.everit.json.schema.FalseSchema; +import org.everit.json.schema.NullSchema; +import org.everit.json.schema.NumberSchema; +import org.everit.json.schema.ObjectSchema; +import org.everit.json.schema.ReferenceSchema; +import org.everit.json.schema.Schema; +import org.everit.json.schema.StringSchema; +import org.everit.json.schema.TrueSchema; +import org.opendatadiscovery.client.model.DataSetField; +import org.opendatadiscovery.client.model.DataSetFieldType; +import org.opendatadiscovery.client.model.MetadataExtension; +import org.opendatadiscovery.oddrn.model.KafkaPath; + +final class JsonSchemaExtractor { + + private JsonSchemaExtractor() { + } + + static List extract(JsonSchema jsonSchema, KafkaPath topicOddrn, boolean isKey) { + Schema schema = jsonSchema.rawSchema(); + List result = new ArrayList<>(); + result.add(DataSetFieldsExtractors.rootField(topicOddrn, isKey)); + extract( + schema, + topicOddrn.oddrn() + "/columns/" + (isKey ? "key" : "value"), + null, + null, + null, + ImmutableSet.of(), + result + ); + return result; + } + + private static void extract(Schema schema, + String parentOddr, + String oddrn, //null for root + String name, + Boolean nullable, + ImmutableSet registeredRecords, + List sink) { + if (schema instanceof ReferenceSchema s) { + Optional.ofNullable(s.getReferredSchema()) + .ifPresent(refSchema -> extract(refSchema, parentOddr, oddrn, name, nullable, registeredRecords, sink)); + } else if (schema instanceof ObjectSchema s) { + extractObject(s, parentOddr, oddrn, name, nullable, registeredRecords, sink); + } else if (schema instanceof ArraySchema s) { + extractArray(s, parentOddr, oddrn, name, nullable, registeredRecords, sink); + } else if (schema instanceof CombinedSchema cs) { + extractCombined(cs, parentOddr, oddrn, name, nullable, registeredRecords, sink); + } else if (schema instanceof BooleanSchema + || schema instanceof NumberSchema + || schema instanceof StringSchema + || schema instanceof NullSchema + ) { + extractPrimitive(schema, parentOddr, oddrn, name, nullable, sink); + } else { + extractUnknown(schema, parentOddr, oddrn, name, nullable, sink); + } + } + + private static void extractPrimitive(Schema schema, + String parentOddr, + String oddrn, //null for root + String name, + Boolean nullable, + List sink) { + boolean isRoot = oddrn == null; + sink.add( + createDataSetField( + schema, + isRoot ? "Root JSON primitive" : name, + parentOddr, + isRoot ? (parentOddr + "/" + logicalTypeName(schema)) : oddrn, + mapType(schema), + logicalTypeName(schema), + nullable + ) + ); + } + + private static void extractUnknown(Schema schema, + String parentOddr, + String oddrn, //null for root + String name, + Boolean nullable, + List sink) { + boolean isRoot = oddrn == null; + sink.add( + createDataSetField( + schema, + isRoot ? "Root type " + logicalTypeName(schema) : name, + parentOddr, + isRoot ? (parentOddr + "/" + logicalTypeName(schema)) : oddrn, + DataSetFieldType.TypeEnum.UNKNOWN, + logicalTypeName(schema), + nullable + ) + ); + } + + private static void extractObject(ObjectSchema schema, + String parentOddr, + String oddrn, //null for root + String name, + Boolean nullable, + ImmutableSet registeredRecords, + List sink) { + boolean isRoot = oddrn == null; + // schemaLocation can be null for empty object schemas (like if it used in anyOf) + @Nullable var schemaLocation = schema.getSchemaLocation(); + if (!isRoot) { + sink.add(createDataSetField( + schema, + name, + parentOddr, + oddrn, + DataSetFieldType.TypeEnum.STRUCT, + logicalTypeName(schema), + nullable + )); + if (schemaLocation != null && registeredRecords.contains(schemaLocation)) { + // avoiding recursion by checking if record already registered in parsing chain + return; + } + } + + var newRegisteredRecords = schemaLocation == null + ? registeredRecords + : ImmutableSet.builder() + .addAll(registeredRecords) + .add(schemaLocation) + .build(); + + schema.getPropertySchemas().forEach((propertyName, propertySchema) -> { + boolean required = schema.getRequiredProperties().contains(propertyName); + extract( + propertySchema, + isRoot ? parentOddr : oddrn, + isRoot + ? parentOddr + "/" + propertyName + : oddrn + "/fields/" + propertyName, + propertyName, + !required, + newRegisteredRecords, + sink + ); + }); + } + + private static void extractArray(ArraySchema schema, + String parentOddr, + String oddrn, //null for root + String name, + Boolean nullable, + ImmutableSet registeredRecords, + List sink) { + boolean isRoot = oddrn == null; + oddrn = isRoot ? parentOddr + "/array" : oddrn; + if (isRoot) { + sink.add( + createDataSetField( + schema, + "Json array root", + parentOddr, + oddrn, + DataSetFieldType.TypeEnum.LIST, + "array", + nullable + )); + } else { + sink.add( + createDataSetField( + schema, + name, + parentOddr, + oddrn, + DataSetFieldType.TypeEnum.LIST, + "array", + nullable + )); + } + @Nullable var itemsSchema = schema.getAllItemSchema(); + if (itemsSchema != null) { + extract( + itemsSchema, + oddrn, + oddrn + "/items/" + logicalTypeName(itemsSchema), + logicalTypeName(itemsSchema), + false, + registeredRecords, + sink + ); + } + } + + private static void extractCombined(CombinedSchema schema, + String parentOddr, + String oddrn, //null for root + String name, + Boolean nullable, + ImmutableSet registeredRecords, + List sink) { + String combineType = "unknown"; + if (schema.getCriterion() == CombinedSchema.ALL_CRITERION) { + combineType = "allOf"; + } + if (schema.getCriterion() == CombinedSchema.ANY_CRITERION) { + combineType = "anyOf"; + } + if (schema.getCriterion() == CombinedSchema.ONE_CRITERION) { + combineType = "oneOf"; + } + + boolean isRoot = oddrn == null; + oddrn = isRoot ? (parentOddr + "/" + combineType) : (oddrn + "/" + combineType); + sink.add( + createDataSetField( + schema, + isRoot ? "Root %s".formatted(combineType) : name, + parentOddr, + oddrn, + DataSetFieldType.TypeEnum.UNION, + combineType, + nullable + ).addMetadataItem(new MetadataExtension() + .schemaUrl(URI.create("wontbeused.oops")) + .metadata(Map.of("criterion", combineType))) + ); + + for (Schema subschema : schema.getSubschemas()) { + extract( + subschema, + oddrn, + oddrn + "/values/" + logicalTypeName(subschema), + logicalTypeName(subschema), + nullable, + registeredRecords, + sink + ); + } + } + + private static String getDescription(Schema schema) { + return Optional.ofNullable(schema.getTitle()) + .orElse(schema.getDescription()); + } + + private static String logicalTypeName(Schema schema) { + return schema.getClass() + .getSimpleName() + .replace("Schema", ""); + } + + private static DataSetField createDataSetField(Schema schema, + String name, + String parentOddrn, + String oddrn, + DataSetFieldType.TypeEnum type, + String logicalType, + Boolean nullable) { + return new DataSetField() + .name(name) + .parentFieldOddrn(parentOddrn) + .oddrn(oddrn) + .description(getDescription(schema)) + .type( + new DataSetFieldType() + .isNullable(nullable) + .logicalType(logicalType) + .type(type) + ); + } + + private static DataSetFieldType.TypeEnum mapType(Schema type) { + if (type instanceof NumberSchema) { + return DataSetFieldType.TypeEnum.NUMBER; + } + if (type instanceof StringSchema) { + return DataSetFieldType.TypeEnum.STRING; + } + if (type instanceof BooleanSchema || type instanceof TrueSchema || type instanceof FalseSchema) { + return DataSetFieldType.TypeEnum.BOOLEAN; + } + if (type instanceof ObjectSchema) { + return DataSetFieldType.TypeEnum.STRUCT; + } + if (type instanceof ReferenceSchema s) { + return mapType(s.getReferredSchema()); + } + if (type instanceof CombinedSchema) { + return DataSetFieldType.TypeEnum.UNION; + } + return DataSetFieldType.TypeEnum.UNKNOWN; + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/ProtoExtractor.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/ProtoExtractor.java new file mode 100644 index 00000000000..01b25ff48db --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/ProtoExtractor.java @@ -0,0 +1,229 @@ +package com.provectus.kafka.ui.service.integration.odd.schema; + +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.BoolValue; +import com.google.protobuf.BytesValue; +import com.google.protobuf.Descriptors; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.DoubleValue; +import com.google.protobuf.Duration; +import com.google.protobuf.FloatValue; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; +import com.google.protobuf.StringValue; +import com.google.protobuf.Timestamp; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.UInt64Value; +import com.google.protobuf.Value; +import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.opendatadiscovery.client.model.DataSetField; +import org.opendatadiscovery.client.model.DataSetFieldType; +import org.opendatadiscovery.client.model.DataSetFieldType.TypeEnum; +import org.opendatadiscovery.oddrn.model.KafkaPath; + +final class ProtoExtractor { + + private static final Set PRIMITIVES_WRAPPER_TYPE_NAMES = Set.of( + BoolValue.getDescriptor().getFullName(), + Int32Value.getDescriptor().getFullName(), + UInt32Value.getDescriptor().getFullName(), + Int64Value.getDescriptor().getFullName(), + UInt64Value.getDescriptor().getFullName(), + StringValue.getDescriptor().getFullName(), + BytesValue.getDescriptor().getFullName(), + FloatValue.getDescriptor().getFullName(), + DoubleValue.getDescriptor().getFullName() + ); + + private ProtoExtractor() { + } + + static List extract(ProtobufSchema protobufSchema, KafkaPath topicOddrn, boolean isKey) { + Descriptor schema = protobufSchema.toDescriptor(); + List result = new ArrayList<>(); + result.add(DataSetFieldsExtractors.rootField(topicOddrn, isKey)); + var rootOddrn = topicOddrn.oddrn() + "/columns/" + (isKey ? "key" : "value"); + schema.getFields().forEach(f -> + extract(f, + rootOddrn, + rootOddrn + "/" + f.getName(), + f.getName(), + !f.isRequired(), + f.isRepeated(), + ImmutableSet.of(schema.getFullName()), + result + )); + return result; + } + + private static void extract(Descriptors.FieldDescriptor field, + String parentOddr, + String oddrn, //null for root + String name, + boolean nullable, + boolean repeated, + ImmutableSet registeredRecords, + List sink) { + if (repeated) { + extractRepeated(field, parentOddr, oddrn, name, nullable, registeredRecords, sink); + } else if (field.getType() == Descriptors.FieldDescriptor.Type.MESSAGE) { + extractMessage(field, parentOddr, oddrn, name, nullable, registeredRecords, sink); + } else { + extractPrimitive(field, parentOddr, oddrn, name, nullable, sink); + } + } + + // converts some(!) Protobuf Well-known type (from google.protobuf.* packages) + // see JsonFormat::buildWellKnownTypePrinters for impl details + private static boolean extractProtoWellKnownType(Descriptors.FieldDescriptor field, + String parentOddr, + String oddrn, //null for root + String name, + boolean nullable, + List sink) { + // all well-known types are messages + if (field.getType() != Descriptors.FieldDescriptor.Type.MESSAGE) { + return false; + } + String typeName = field.getMessageType().getFullName(); + if (typeName.equals(Timestamp.getDescriptor().getFullName())) { + sink.add(createDataSetField(name, parentOddr, oddrn, TypeEnum.DATETIME, typeName, nullable)); + return true; + } + if (typeName.equals(Duration.getDescriptor().getFullName())) { + sink.add(createDataSetField(name, parentOddr, oddrn, TypeEnum.DURATION, typeName, nullable)); + return true; + } + if (typeName.equals(Value.getDescriptor().getFullName())) { + //TODO: use ANY type when it will appear in ODD + sink.add(createDataSetField(name, parentOddr, oddrn, TypeEnum.UNKNOWN, typeName, nullable)); + return true; + } + if (PRIMITIVES_WRAPPER_TYPE_NAMES.contains(typeName)) { + var wrapped = field.getMessageType().findFieldByName("value"); + sink.add(createDataSetField(name, parentOddr, oddrn, mapType(wrapped.getType()), typeName, true)); + return true; + } + return false; + } + + private static void extractRepeated(Descriptors.FieldDescriptor field, + String parentOddr, + String oddrn, //null for root + String name, + boolean nullable, + ImmutableSet registeredRecords, + List sink) { + sink.add(createDataSetField(name, parentOddr, oddrn, TypeEnum.LIST, "repeated", nullable)); + + String itemName = field.getType() == Descriptors.FieldDescriptor.Type.MESSAGE + ? field.getMessageType().getName() + : field.getType().name().toLowerCase(); + + extract( + field, + oddrn, + oddrn + "/items/" + itemName, + itemName, + nullable, + false, + registeredRecords, + sink + ); + } + + private static void extractMessage(Descriptors.FieldDescriptor field, + String parentOddr, + String oddrn, //null for root + String name, + boolean nullable, + ImmutableSet registeredRecords, + List sink) { + if (extractProtoWellKnownType(field, parentOddr, oddrn, name, nullable, sink)) { + return; + } + sink.add(createDataSetField(name, parentOddr, oddrn, TypeEnum.STRUCT, getLogicalTypeName(field), nullable)); + + String msgTypeName = field.getMessageType().getFullName(); + if (registeredRecords.contains(msgTypeName)) { + // avoiding recursion by checking if record already registered in parsing chain + return; + } + var newRegisteredRecords = ImmutableSet.builder() + .addAll(registeredRecords) + .add(msgTypeName) + .build(); + + field.getMessageType() + .getFields() + .forEach(f -> { + extract(f, + oddrn, + oddrn + "/fields/" + f.getName(), + f.getName(), + !f.isRequired(), + f.isRepeated(), + newRegisteredRecords, + sink + ); + }); + } + + private static void extractPrimitive(Descriptors.FieldDescriptor field, + String parentOddr, + String oddrn, + String name, + boolean nullable, + List sink) { + sink.add( + createDataSetField( + name, + parentOddr, + oddrn, + mapType(field.getType()), + getLogicalTypeName(field), + nullable + ) + ); + } + + private static String getLogicalTypeName(Descriptors.FieldDescriptor f) { + return f.getType() == Descriptors.FieldDescriptor.Type.MESSAGE + ? f.getMessageType().getFullName() + : f.getType().name().toLowerCase(); + } + + private static DataSetField createDataSetField(String name, + String parentOddrn, + String oddrn, + TypeEnum type, + String logicalType, + Boolean nullable) { + return new DataSetField() + .name(name) + .parentFieldOddrn(parentOddrn) + .oddrn(oddrn) + .type( + new DataSetFieldType() + .isNullable(nullable) + .logicalType(logicalType) + .type(type) + ); + } + + + private static TypeEnum mapType(Descriptors.FieldDescriptor.Type type) { + return switch (type) { + case INT32, INT64, SINT32, SFIXED32, SINT64, UINT32, UINT64, FIXED32, FIXED64, SFIXED64 -> TypeEnum.INTEGER; + case FLOAT, DOUBLE -> TypeEnum.NUMBER; + case STRING, ENUM -> TypeEnum.STRING; + case BOOL -> TypeEnum.BOOLEAN; + case BYTES -> TypeEnum.BINARY; + case MESSAGE, GROUP -> TypeEnum.STRUCT; + }; + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlApiClient.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlApiClient.java index 9341dd30d3b..e8f4954bf0a 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlApiClient.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlApiClient.java @@ -4,24 +4,29 @@ import static ksql.KsqlGrammarParser.PrintTopicContext; import static ksql.KsqlGrammarParser.SingleStatementContext; import static ksql.KsqlGrammarParser.UndefineVariableContext; +import static org.springframework.http.MediaType.APPLICATION_JSON; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.node.TextNode; -import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.config.ClustersProperties; import com.provectus.kafka.ui.service.ksql.response.ResponseParser; +import com.provectus.kafka.ui.util.WebClientConfigurator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import javax.annotation.Nullable; import lombok.Builder; import lombok.Value; import lombok.extern.slf4j.Slf4j; import org.springframework.core.codec.DecodingException; import org.springframework.http.MediaType; import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; -import org.springframework.web.reactive.function.client.ExchangeStrategies; +import org.springframework.util.unit.DataSize; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Flux; @@ -30,21 +35,27 @@ @Slf4j public class KsqlApiClient { + private static final MimeType KQL_API_MIME_TYPE = MimeTypeUtils.parseMimeType("application/vnd.ksql.v1+json"); + private static final Set> UNSUPPORTED_STMT_TYPES = Set.of( PrintTopicContext.class, DefineVariableContext.class, UndefineVariableContext.class ); - @Builder + @Builder(toBuilder = true) @Value public static class KsqlResponseTable { String header; List columnNames; List> values; + boolean error; public Optional getColumnValue(List row, String column) { - return Optional.ofNullable(row.get(columnNames.indexOf(column))); + int colIdx = columnNames.indexOf(column); + return colIdx >= 0 + ? Optional.ofNullable(row.get(colIdx)) + : Optional.empty(); } } @@ -56,31 +67,42 @@ private static class KsqlRequest { //-------------------------------------------------------------------------------------------- - private final KafkaCluster cluster; + private final String baseUrl; + private final WebClient webClient; - public KsqlApiClient(KafkaCluster cluster) { - this.cluster = cluster; + public KsqlApiClient(String baseUrl, + @Nullable ClustersProperties.KsqldbServerAuth ksqldbServerAuth, + @Nullable ClustersProperties.TruststoreConfig ksqldbServerSsl, + @Nullable ClustersProperties.KeystoreConfig keystoreConfig, + @Nullable DataSize maxBuffSize) { + this.baseUrl = baseUrl; + this.webClient = webClient(ksqldbServerAuth, ksqldbServerSsl, keystoreConfig, maxBuffSize); } - private WebClient webClient() { - var exchangeStrategies = ExchangeStrategies.builder() - .codecs(configurer -> { - configurer.customCodecs() - .register( - new Jackson2JsonDecoder( - new ObjectMapper(), - // some ksqldb versions do not set content-type header in response, - // but we still need to use JsonDecoder for it - MimeTypeUtils.APPLICATION_OCTET_STREAM)); + private static WebClient webClient(@Nullable ClustersProperties.KsqldbServerAuth ksqldbServerAuth, + @Nullable ClustersProperties.TruststoreConfig truststoreConfig, + @Nullable ClustersProperties.KeystoreConfig keystoreConfig, + @Nullable DataSize maxBuffSize) { + ksqldbServerAuth = Optional.ofNullable(ksqldbServerAuth).orElse(new ClustersProperties.KsqldbServerAuth()); + maxBuffSize = Optional.ofNullable(maxBuffSize).orElse(DataSize.ofMegabytes(20)); + + return new WebClientConfigurator() + .configureSsl(truststoreConfig, keystoreConfig) + .configureBasicAuth( + ksqldbServerAuth.getUsername(), + ksqldbServerAuth.getPassword() + ) + .configureBufferSize(maxBuffSize) + .configureCodecs(codecs -> { + var mapper = new JsonMapper(); + codecs.defaultCodecs() + .jackson2JsonEncoder(new Jackson2JsonEncoder(mapper, KQL_API_MIME_TYPE, APPLICATION_JSON)); + // some ksqldb versions do not set content-type header in response, + // but we still need to use JsonDecoder for it + codecs.defaultCodecs() + .jackson2JsonDecoder(new Jackson2JsonDecoder(mapper, MimeTypeUtils.ALL)); }) .build(); - return WebClient.builder() - .exchangeStrategies(exchangeStrategies) - .build(); - } - - private String baseKsqlDbUri() { - return cluster.getKsqldbServer(); } private KsqlRequest ksqlRequest(String ksql, Map streamProperties) { @@ -88,11 +110,11 @@ private KsqlRequest ksqlRequest(String ksql, Map streamPropertie } private Flux executeSelect(String ksql, Map streamProperties) { - return webClient() + return webClient .post() - .uri(baseKsqlDbUri() + "/query") - .accept(MediaType.parseMediaType("application/vnd.ksql.v1+json")) - .contentType(MediaType.parseMediaType("application/vnd.ksql.v1+json")) + .uri(baseUrl + "/query") + .accept(new MediaType(KQL_API_MIME_TYPE)) + .contentType(new MediaType(KQL_API_MIME_TYPE)) .bodyValue(ksqlRequest(ksql, streamProperties)) .retrieve() .bodyToFlux(JsonNode.class) @@ -118,11 +140,11 @@ private boolean isUnexpectedJsonArrayEndCharException(Throwable th) { private Flux executeStatement(String ksql, Map streamProperties) { - return webClient() + return webClient .post() - .uri(baseKsqlDbUri() + "/ksql") - .accept(MediaType.parseMediaType("application/vnd.ksql.v1+json")) - .contentType(MediaType.parseMediaType("application/json")) + .uri(baseUrl + "/ksql") + .accept(new MediaType(KQL_API_MIME_TYPE)) + .contentType(APPLICATION_JSON) .bodyValue(ksqlRequest(ksql, streamProperties)) .exchangeToFlux( resp -> { diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2.java index 12a2e7d0b24..e8c2a4c65a3 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2.java @@ -13,7 +13,6 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import lombok.Value; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; @@ -22,7 +21,7 @@ @Service public class KsqlServiceV2 { - @Value + @lombok.Value private static class KsqlExecuteCommand { KafkaCluster cluster; String ksql; @@ -48,13 +47,13 @@ public Flux execute(String commandId) { throw new ValidationException("No command registered with id " + commandId); } registeredCommands.invalidate(commandId); - return new KsqlApiClient(cmd.cluster) - .execute(cmd.ksql, cmd.streamProperties); + return cmd.cluster.getKsqlClient() + .flux(client -> client.execute(cmd.ksql, cmd.streamProperties)); } public Flux listTables(KafkaCluster cluster) { - return new KsqlApiClient(cluster) - .execute("LIST TABLES;", Map.of()) + return cluster.getKsqlClient() + .flux(client -> client.execute("LIST TABLES;", Map.of())) .flatMap(resp -> { if (!resp.getHeader().equals("Tables")) { log.error("Unexpected result header: {}", resp.getHeader()); @@ -75,8 +74,8 @@ public Flux listTables(KafkaCluster cluster) { } public Flux listStreams(KafkaCluster cluster) { - return new KsqlApiClient(cluster) - .execute("LIST STREAMS;", Map.of()) + return cluster.getKsqlClient() + .flux(client -> client.execute("LIST STREAMS;", Map.of())) .flatMap(resp -> { if (!resp.getHeader().equals("Streams")) { log.error("Unexpected result header: {}", resp.getHeader()); @@ -90,7 +89,14 @@ public Flux listStreams(KafkaCluster cluster) { .name(resp.getColumnValue(row, "name").map(JsonNode::asText).orElse(null)) .topic(resp.getColumnValue(row, "topic").map(JsonNode::asText).orElse(null)) .keyFormat(resp.getColumnValue(row, "keyFormat").map(JsonNode::asText).orElse(null)) - .valueFormat(resp.getColumnValue(row, "valueFormat").map(JsonNode::asText).orElse(null))) + .valueFormat( + // for old versions (<0.13) "format" column is filled, + // for new version "keyFormat" & "valueFormat" columns should be filled + resp.getColumnValue(row, "valueFormat") + .or(() -> resp.getColumnValue(row, "format")) + .map(JsonNode::asText) + .orElse(null)) + ) .collect(Collectors.toList())); }); } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/response/ResponseParser.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/response/ResponseParser.java index 4781d159e7b..cd91fa57dcc 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/response/ResponseParser.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/response/ResponseParser.java @@ -3,14 +3,13 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.node.TextNode; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; import com.provectus.kafka.ui.exception.KsqlApiException; import com.provectus.kafka.ui.service.ksql.KsqlApiClient; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; import org.springframework.web.reactive.function.client.WebClientResponseException; public class ResponseParser { @@ -24,11 +23,7 @@ public static Optional parseSelectResponse(Json return Optional.of( KsqlApiClient.KsqlResponseTable.builder() .header("Schema") - .columnNames( - Arrays.stream(jsonNode.get("header").get("schema").asText().split(",")) - .map(String::trim) - .collect(Collectors.toList()) - ) + .columnNames(parseSelectHeadersString(jsonNode.get("header").get("schema").asText())) .build()); } if (arrayFieldNonEmpty(jsonNode, "row")) { @@ -46,18 +41,50 @@ public static Optional parseSelectResponse(Json return Optional.empty(); } + @VisibleForTesting + static List parseSelectHeadersString(String str) { + List headers = new ArrayList<>(); + int structNesting = 0; + boolean quotes = false; + var headerBuilder = new StringBuilder(); + for (char ch : str.toCharArray()) { + if (ch == '<') { + structNesting++; + } else if (ch == '>') { + structNesting--; + } else if (ch == '`') { + quotes = !quotes; + } else if (ch == ' ' && headerBuilder.isEmpty()) { + continue; //skipping leading & training whitespaces + } else if (ch == ',' && structNesting == 0 && !quotes) { + headers.add(headerBuilder.toString()); + headerBuilder = new StringBuilder(); + continue; + } + headerBuilder.append(ch); + } + if (!headerBuilder.isEmpty()) { + headers.add(headerBuilder.toString()); + } + return headers; + } + public static KsqlApiClient.KsqlResponseTable errorTableWithTextMsg(String errorText) { return KsqlApiClient.KsqlResponseTable.builder() .header("Execution error") .columnNames(List.of("message")) .values(List.of(List.of(new TextNode(errorText)))) + .error(true) .build(); } public static KsqlApiClient.KsqlResponseTable parseErrorResponse(WebClientResponseException e) { try { var errBody = new JsonMapper().readTree(e.getResponseBodyAsString()); - return DynamicParser.parseObject("Execution error", errBody); + return DynamicParser.parseObject("Execution error", errBody) + .toBuilder() + .error(true) + .build(); } catch (Exception ex) { return errorTableWithTextMsg( String.format( diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/DataMasking.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/DataMasking.java new file mode 100644 index 00000000000..a2e9c88f86b --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/DataMasking.java @@ -0,0 +1,101 @@ +package com.provectus.kafka.ui.service.masking; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.node.ContainerNode; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.provectus.kafka.ui.config.ClustersProperties; +import com.provectus.kafka.ui.model.TopicMessageDTO; +import com.provectus.kafka.ui.serde.api.Serde; +import com.provectus.kafka.ui.service.masking.policies.MaskingPolicy; +import java.util.List; +import java.util.Optional; +import java.util.function.UnaryOperator; +import java.util.regex.Pattern; +import javax.annotation.Nullable; +import lombok.Value; +import org.apache.commons.lang3.StringUtils; + +public class DataMasking { + + private static final JsonMapper JSON_MAPPER = new JsonMapper(); + + @Value + static class Mask { + @Nullable + Pattern topicKeysPattern; + @Nullable + Pattern topicValuesPattern; + + MaskingPolicy policy; + + boolean shouldBeApplied(String topic, Serde.Target target) { + return target == Serde.Target.KEY + ? topicKeysPattern != null && topicKeysPattern.matcher(topic).matches() + : topicValuesPattern != null && topicValuesPattern.matcher(topic).matches(); + } + } + + private final List masks; + + public static DataMasking create(@Nullable List config) { + return new DataMasking( + Optional.ofNullable(config).orElse(List.of()).stream().map(property -> { + Preconditions.checkNotNull(property.getType(), "masking type not specified"); + Preconditions.checkArgument( + StringUtils.isNotEmpty(property.getTopicKeysPattern()) + || StringUtils.isNotEmpty(property.getTopicValuesPattern()), + "topicKeysPattern or topicValuesPattern (or both) should be set for masking policy"); + return new Mask( + Optional.ofNullable(property.getTopicKeysPattern()).map(Pattern::compile).orElse(null), + Optional.ofNullable(property.getTopicValuesPattern()).map(Pattern::compile).orElse(null), + MaskingPolicy.create(property) + ); + }).toList() + ); + } + + @VisibleForTesting + DataMasking(List masks) { + this.masks = masks; + } + + public UnaryOperator getMaskerForTopic(String topic) { + var keyMasker = getMaskingFunction(topic, Serde.Target.KEY); + var valMasker = getMaskingFunction(topic, Serde.Target.VALUE); + return msg -> msg + .key(keyMasker.apply(msg.getKey())) + .content(valMasker.apply(msg.getContent())); + } + + @VisibleForTesting + UnaryOperator getMaskingFunction(String topic, Serde.Target target) { + var targetMasks = masks.stream().filter(m -> m.shouldBeApplied(topic, target)).toList(); + if (targetMasks.isEmpty()) { + return UnaryOperator.identity(); + } + return inputStr -> { + if (inputStr == null) { + return null; + } + try { + JsonNode json = JSON_MAPPER.readTree(inputStr); + if (json.isContainerNode()) { + for (Mask targetMask : targetMasks) { + json = targetMask.policy.applyToJsonContainer((ContainerNode) json); + } + return json.toString(); + } + } catch (JsonProcessingException jsonException) { + //just ignore + } + // if we can't parse input as json or parsed json is not object/array + // we just apply first found policy + // (there is no need to apply all of them, because they will just override each other) + return targetMasks.get(0).policy.applyToString(inputStr); + }; + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/FieldsSelector.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/FieldsSelector.java new file mode 100644 index 00000000000..99563943984 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/FieldsSelector.java @@ -0,0 +1,28 @@ +package com.provectus.kafka.ui.service.masking.policies; + +import com.provectus.kafka.ui.config.ClustersProperties; +import com.provectus.kafka.ui.exception.ValidationException; +import java.util.regex.Pattern; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +interface FieldsSelector { + + static FieldsSelector create(ClustersProperties.Masking property) { + if (StringUtils.hasText(property.getFieldsNamePattern()) && !CollectionUtils.isEmpty(property.getFields())) { + throw new ValidationException("You can't provide both fieldNames & fieldsNamePattern for masking"); + } + if (StringUtils.hasText(property.getFieldsNamePattern())) { + Pattern pattern = Pattern.compile(property.getFieldsNamePattern()); + return f -> pattern.matcher(f).matches(); + } + if (!CollectionUtils.isEmpty(property.getFields())) { + return f -> property.getFields().contains(f); + } + //no pattern, no field names - mean all fields should be masked + return fieldName -> true; + } + + boolean shouldBeMasked(String fieldName); + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Mask.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Mask.java new file mode 100644 index 00000000000..e6a469f2c03 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Mask.java @@ -0,0 +1,87 @@ +package com.provectus.kafka.ui.service.masking.policies; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ContainerNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.google.common.base.Preconditions; +import java.util.List; +import java.util.function.UnaryOperator; + +class Mask extends MaskingPolicy { + + static final List DEFAULT_PATTERN = List.of("X", "x", "n", "-"); + + private final UnaryOperator masker; + + Mask(FieldsSelector fieldsSelector, List maskingChars) { + super(fieldsSelector); + this.masker = createMasker(maskingChars); + } + + @Override + public ContainerNode applyToJsonContainer(ContainerNode node) { + return (ContainerNode) maskWithFieldsCheck(node); + } + + @Override + public String applyToString(String str) { + return masker.apply(str); + } + + private static UnaryOperator createMasker(List maskingChars) { + Preconditions.checkNotNull(maskingChars); + Preconditions.checkArgument(maskingChars.size() == 4, "mask pattern should contain 4 elements"); + return input -> { + StringBuilder sb = new StringBuilder(input.length()); + for (int i = 0; i < input.length(); i++) { + int cp = input.codePointAt(i); + switch (Character.getType(cp)) { + case Character.SPACE_SEPARATOR, + Character.LINE_SEPARATOR, + Character.PARAGRAPH_SEPARATOR -> sb.appendCodePoint(cp); // keeping separators as-is + case Character.UPPERCASE_LETTER -> sb.append(maskingChars.get(0)); + case Character.LOWERCASE_LETTER -> sb.append(maskingChars.get(1)); + case Character.DECIMAL_DIGIT_NUMBER -> sb.append(maskingChars.get(2)); + default -> sb.append(maskingChars.get(3)); + } + } + return sb.toString(); + }; + } + + private JsonNode maskWithFieldsCheck(JsonNode node) { + if (node.isObject()) { + ObjectNode obj = ((ObjectNode) node).objectNode(); + node.fields().forEachRemaining(f -> { + String fieldName = f.getKey(); + JsonNode fieldVal = f.getValue(); + if (fieldShouldBeMasked(fieldName)) { + obj.set(fieldName, maskNodeRecursively(fieldVal)); + } else { + obj.set(fieldName, maskWithFieldsCheck(fieldVal)); + } + }); + return obj; + } else if (node.isArray()) { + ArrayNode arr = ((ArrayNode) node).arrayNode(node.size()); + node.elements().forEachRemaining(e -> arr.add(maskWithFieldsCheck(e))); + return arr; + } + return node; + } + + private JsonNode maskNodeRecursively(JsonNode node) { + if (node.isObject()) { + ObjectNode obj = ((ObjectNode) node).objectNode(); + node.fields().forEachRemaining(f -> obj.set(f.getKey(), maskNodeRecursively(f.getValue()))); + return obj; + } else if (node.isArray()) { + ArrayNode arr = ((ArrayNode) node).arrayNode(node.size()); + node.elements().forEachRemaining(e -> arr.add(maskNodeRecursively(e))); + return arr; + } + return new TextNode(masker.apply(node.asText())); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/MaskingPolicy.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/MaskingPolicy.java new file mode 100644 index 00000000000..9b80da0cb18 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/MaskingPolicy.java @@ -0,0 +1,41 @@ +package com.provectus.kafka.ui.service.masking.policies; + +import com.fasterxml.jackson.databind.node.ContainerNode; +import com.provectus.kafka.ui.config.ClustersProperties; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public abstract class MaskingPolicy { + + public static MaskingPolicy create(ClustersProperties.Masking property) { + FieldsSelector fieldsSelector = FieldsSelector.create(property); + return switch (property.getType()) { + case REMOVE -> new Remove(fieldsSelector); + case REPLACE -> new Replace( + fieldsSelector, + property.getReplacement() == null + ? Replace.DEFAULT_REPLACEMENT + : property.getReplacement() + ); + case MASK -> new Mask( + fieldsSelector, + property.getMaskingCharsReplacement() == null + ? Mask.DEFAULT_PATTERN + : property.getMaskingCharsReplacement() + ); + }; + } + + //---------------------------------------------------------------- + + private final FieldsSelector fieldsSelector; + + protected boolean fieldShouldBeMasked(String fieldName) { + return fieldsSelector.shouldBeMasked(fieldName); + } + + public abstract ContainerNode applyToJsonContainer(ContainerNode node); + + public abstract String applyToString(String str); + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Remove.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Remove.java new file mode 100644 index 00000000000..cc5cdd14159 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Remove.java @@ -0,0 +1,43 @@ +package com.provectus.kafka.ui.service.masking.policies; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ContainerNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + + +class Remove extends MaskingPolicy { + + Remove(FieldsSelector fieldsSelector) { + super(fieldsSelector); + } + + @Override + public String applyToString(String str) { + return "null"; + } + + @Override + public ContainerNode applyToJsonContainer(ContainerNode node) { + return (ContainerNode) removeFields(node); + } + + private JsonNode removeFields(JsonNode node) { + if (node.isObject()) { + ObjectNode obj = ((ObjectNode) node).objectNode(); + node.fields().forEachRemaining(f -> { + String fieldName = f.getKey(); + JsonNode fieldVal = f.getValue(); + if (!fieldShouldBeMasked(fieldName)) { + obj.set(fieldName, removeFields(fieldVal)); + } + }); + return obj; + } else if (node.isArray()) { + var arr = ((ArrayNode) node).arrayNode(node.size()); + node.elements().forEachRemaining(e -> arr.add(removeFields(e))); + return arr; + } + return node; + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Replace.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Replace.java new file mode 100644 index 00000000000..1cf91793d22 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Replace.java @@ -0,0 +1,65 @@ +package com.provectus.kafka.ui.service.masking.policies; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ContainerNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.google.common.base.Preconditions; + +class Replace extends MaskingPolicy { + + static final String DEFAULT_REPLACEMENT = "***DATA_MASKED***"; + + private final String replacement; + + Replace(FieldsSelector fieldsSelector, String replacementString) { + super(fieldsSelector); + this.replacement = Preconditions.checkNotNull(replacementString); + } + + @Override + public String applyToString(String str) { + return replacement; + } + + @Override + public ContainerNode applyToJsonContainer(ContainerNode node) { + return (ContainerNode) replaceWithFieldsCheck(node); + } + + private JsonNode replaceWithFieldsCheck(JsonNode node) { + if (node.isObject()) { + ObjectNode obj = ((ObjectNode) node).objectNode(); + node.fields().forEachRemaining(f -> { + String fieldName = f.getKey(); + JsonNode fieldVal = f.getValue(); + if (fieldShouldBeMasked(fieldName)) { + obj.set(fieldName, replaceRecursive(fieldVal)); + } else { + obj.set(fieldName, replaceWithFieldsCheck(fieldVal)); + } + }); + return obj; + } else if (node.isArray()) { + ArrayNode arr = ((ArrayNode) node).arrayNode(node.size()); + node.elements().forEachRemaining(e -> arr.add(replaceWithFieldsCheck(e))); + return arr; + } + // if it is not an object or array - we have nothing to replace here + return node; + } + + private JsonNode replaceRecursive(JsonNode node) { + if (node.isObject()) { + ObjectNode obj = ((ObjectNode) node).objectNode(); + node.fields().forEachRemaining(f -> obj.set(f.getKey(), replaceRecursive(f.getValue()))); + return obj; + } else if (node.isArray()) { + ArrayNode arr = ((ArrayNode) node).arrayNode(node.size()); + node.elements().forEachRemaining(e -> arr.add(replaceRecursive(e))); + return arr; + } + return new TextNode(replacement); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxMetricsFormatter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxMetricsFormatter.java new file mode 100644 index 00000000000..4d3d31f50f4 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxMetricsFormatter.java @@ -0,0 +1,85 @@ +package com.provectus.kafka.ui.service.metrics; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.management.MBeanAttributeInfo; +import javax.management.ObjectName; + +/** + * Converts JMX metrics into JmxExporter prometheus format: format. + */ +class JmxMetricsFormatter { + + // copied from https://github.com/prometheus/jmx_exporter/blob/b6b811b4aae994e812e902b26dd41f29364c0e2b/collector/src/main/java/io/prometheus/jmx/JmxMBeanPropertyCache.java#L15 + private static final Pattern PROPERTY_PATTERN = Pattern.compile( + "([^,=:\\*\\?]+)=(\"(?:[^\\\\\"]*(?:\\\\.)?)*\"|[^,=:\"]*)"); + + static List constructMetricsList(ObjectName jmxMetric, + MBeanAttributeInfo[] attributes, + Object[] attrValues) { + String domain = fixIllegalChars(jmxMetric.getDomain()); + LinkedHashMap labels = getLabelsMap(jmxMetric); + String firstLabel = labels.keySet().iterator().next(); + String firstLabelValue = fixIllegalChars(labels.get(firstLabel)); + labels.remove(firstLabel); //removing first label since it's value will be in name + + List result = new ArrayList<>(attributes.length); + for (int i = 0; i < attributes.length; i++) { + String attrName = fixIllegalChars(attributes[i].getName()); + convertNumericValue(attrValues[i]).ifPresent(convertedValue -> { + String name = String.format("%s_%s_%s", domain, firstLabelValue, attrName); + var metric = RawMetric.create(name, labels, convertedValue); + result.add(metric); + }); + } + return result; + } + + private static String fixIllegalChars(String str) { + return str + .replace('.', '_') + .replace('-', '_'); + } + + private static Optional convertNumericValue(Object value) { + if (!(value instanceof Number)) { + return Optional.empty(); + } + try { + if (value instanceof Long) { + return Optional.of(new BigDecimal((Long) value)); + } else if (value instanceof Integer) { + return Optional.of(new BigDecimal((Integer) value)); + } + return Optional.of(new BigDecimal(value.toString())); + } catch (NumberFormatException nfe) { + return Optional.empty(); + } + } + + /** + * Converts Mbean properties to map keeping order (copied from jmx_exporter repo). + */ + private static LinkedHashMap getLabelsMap(ObjectName mbeanName) { + LinkedHashMap keyProperties = new LinkedHashMap<>(); + String properties = mbeanName.getKeyPropertyListString(); + Matcher match = PROPERTY_PATTERN.matcher(properties); + while (match.lookingAt()) { + String labelName = fixIllegalChars(match.group(1)); // label names should be fixed + String labelValue = match.group(2); + keyProperties.put(labelName, labelValue); + properties = properties.substring(match.end()); + if (properties.startsWith(",")) { + properties = properties.substring(1); + } + match.reset(properties); + } + return keyProperties; + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxMetricsRetriever.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxMetricsRetriever.java new file mode 100644 index 00000000000..e7a58cbae27 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxMetricsRetriever.java @@ -0,0 +1,134 @@ +package com.provectus.kafka.ui.service.metrics; + +import com.provectus.kafka.ui.model.KafkaCluster; +import java.io.Closeable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.kafka.common.Node; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + + +@Service +@Slf4j +class JmxMetricsRetriever implements MetricsRetriever, Closeable { + + private static final boolean SSL_JMX_SUPPORTED; + + static { + // see JmxSslSocketFactory doc for details + SSL_JMX_SUPPORTED = JmxSslSocketFactory.initialized(); + } + + private static final String JMX_URL = "service:jmx:rmi:///jndi/rmi://"; + private static final String JMX_SERVICE_TYPE = "jmxrmi"; + private static final String CANONICAL_NAME_PATTERN = "kafka.server*:*"; + + @Override + public void close() { + JmxSslSocketFactory.clearFactoriesCache(); + } + + @Override + public Flux retrieve(KafkaCluster c, Node node) { + if (isSslJmxEndpoint(c) && !SSL_JMX_SUPPORTED) { + log.warn("Cluster {} has jmx ssl configured, but it is not supported", c.getName()); + return Flux.empty(); + } + return Mono.fromSupplier(() -> retrieveSync(c, node)) + .subscribeOn(Schedulers.boundedElastic()) + .flatMapMany(Flux::fromIterable); + } + + private boolean isSslJmxEndpoint(KafkaCluster cluster) { + return cluster.getMetricsConfig().getKeystoreLocation() != null; + } + + @SneakyThrows + private List retrieveSync(KafkaCluster c, Node node) { + String jmxUrl = JMX_URL + node.host() + ":" + c.getMetricsConfig().getPort() + "/" + JMX_SERVICE_TYPE; + log.debug("Collection JMX metrics for {}", jmxUrl); + List result = new ArrayList<>(); + withJmxConnector(jmxUrl, c, jmxConnector -> getMetricsFromJmx(jmxConnector, result)); + log.debug("{} metrics collected for {}", result.size(), jmxUrl); + return result; + } + + private void withJmxConnector(String jmxUrl, + KafkaCluster c, + Consumer consumer) { + var env = prepareJmxEnvAndSetThreadLocal(c); + try (JMXConnector connector = JMXConnectorFactory.newJMXConnector(new JMXServiceURL(jmxUrl), env)) { + try { + connector.connect(env); + } catch (Exception exception) { + log.error("Error connecting to {}", jmxUrl, exception); + return; + } + consumer.accept(connector); + } catch (Exception e) { + log.error("Error getting jmx metrics from {}", jmxUrl, e); + } finally { + JmxSslSocketFactory.clearThreadLocalContext(); + } + } + + private Map prepareJmxEnvAndSetThreadLocal(KafkaCluster cluster) { + var metricsConfig = cluster.getMetricsConfig(); + Map env = new HashMap<>(); + if (isSslJmxEndpoint(cluster)) { + var clusterSsl = cluster.getOriginalProperties().getSsl(); + JmxSslSocketFactory.setSslContextThreadLocal( + clusterSsl != null ? clusterSsl.getTruststoreLocation() : null, + clusterSsl != null ? clusterSsl.getTruststorePassword() : null, + metricsConfig.getKeystoreLocation(), + metricsConfig.getKeystorePassword() + ); + JmxSslSocketFactory.editJmxConnectorEnv(env); + } + + if (StringUtils.isNotEmpty(metricsConfig.getUsername()) + && StringUtils.isNotEmpty(metricsConfig.getPassword())) { + env.put( + JMXConnector.CREDENTIALS, + new String[] {metricsConfig.getUsername(), metricsConfig.getPassword()} + ); + } + return env; + } + + @SneakyThrows + private void getMetricsFromJmx(JMXConnector jmxConnector, List sink) { + MBeanServerConnection msc = jmxConnector.getMBeanServerConnection(); + var jmxMetrics = msc.queryNames(new ObjectName(CANONICAL_NAME_PATTERN), null); + for (ObjectName jmxMetric : jmxMetrics) { + sink.addAll(extractObjectMetrics(jmxMetric, msc)); + } + } + + @SneakyThrows + private List extractObjectMetrics(ObjectName objectName, MBeanServerConnection msc) { + MBeanAttributeInfo[] attrNames = msc.getMBeanInfo(objectName).getAttributes(); + Object[] attrValues = new Object[attrNames.length]; + for (int i = 0; i < attrNames.length; i++) { + attrValues[i] = msc.getAttribute(objectName, attrNames[i].getName()); + } + return JmxMetricsFormatter.constructMetricsList(objectName, attrNames, attrValues); + } + +} + diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxSslSocketFactory.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxSslSocketFactory.java new file mode 100644 index 00000000000..fa84fc361c4 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxSslSocketFactory.java @@ -0,0 +1,220 @@ +package com.provectus.kafka.ui.service.metrics; + +import com.google.common.base.Preconditions; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyStore; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nullable; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import javax.rmi.ssl.SslRMIClientSocketFactory; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.ResourceUtils; + +/* + * Purpose of this class to provide an ability to connect to different JMX endpoints using different keystores. + * + * Usually, when you want to establish SSL JMX connection you set "com.sun.jndi.rmi.factory.socket" env + * property to SslRMIClientSocketFactory instance. SslRMIClientSocketFactory itself uses SSLSocketFactory.getDefault() + * as a socket factory implementation. Problem here is that when ones SslRMIClientSocketFactory instance is created, + * the same cached SSLSocketFactory instance will be used to establish connection with *all* JMX endpoints. + * Moreover, even if we submit custom SslRMIClientSocketFactory implementation which takes specific ssl context + * into account, SslRMIClientSocketFactory is + * internally created during RMI calls. + * + * So, the only way we found to deal with it is to change internal field ('defaultSocketFactory') of + * SslRMIClientSocketFactory to our custom impl, and left all internal RMI code work as is. + * Since RMI code is synchronous, we can pass parameters (which are truststore/keystore) to our custom factory + * that we want to use when creating ssl socket via ThreadLocal variables. + * + * NOTE 1: Theoretically we could avoid using reflection to set internal field set by + * setting "ssl.SocketFactory.provider" security property (see code in SSLSocketFactory.getDefault()), + * but that code uses systemClassloader which is not working right when we're creating executable spring boot jar + * (https://docs.spring.io/spring-boot/docs/current/reference/html/executable-jar.html#appendix.executable-jar.restrictions). + * We can use this if we swith to other jar-packing solutions in the future. + * + * NOTE 2: There are two paths from which socket factory is called - when jmx connection if established (we manage this + * by passing ThreadLocal vars) and from DGCClient in background thread - we deal with that we cache created factories + * for specific host+port. + * + */ +@Slf4j +class JmxSslSocketFactory extends javax.net.ssl.SSLSocketFactory { + + private static final boolean SSL_JMX_SUPPORTED; + + static { + boolean sslJmxSupported = false; + try { + Field defaultSocketFactoryField = SslRMIClientSocketFactory.class.getDeclaredField("defaultSocketFactory"); + defaultSocketFactoryField.setAccessible(true); + defaultSocketFactoryField.set(null, new JmxSslSocketFactory()); + sslJmxSupported = true; + } catch (Exception e) { + log.error("----------------------------------"); + log.error("SSL can't be enabled for JMX retrieval. " + + "Make sure your java app run with '--add-opens java.rmi/javax.rmi.ssl=ALL-UNNAMED' arg. Err: {}", + e.getMessage()); + log.trace("SSL can't be enabled for JMX retrieval", e); + log.error("----------------------------------"); + } + SSL_JMX_SUPPORTED = sslJmxSupported; + } + + public static boolean initialized() { + return SSL_JMX_SUPPORTED; + } + + private static final ThreadLocal SSL_CONTEXT_THREAD_LOCAL = new ThreadLocal<>(); + + private static final Map CACHED_FACTORIES = new ConcurrentHashMap<>(); + + private record HostAndPort(String host, int port) { + } + + private record Ssl(@Nullable String truststoreLocation, + @Nullable String truststorePassword, + @Nullable String keystoreLocation, + @Nullable String keystorePassword) { + } + + public static void setSslContextThreadLocal(@Nullable String truststoreLocation, + @Nullable String truststorePassword, + @Nullable String keystoreLocation, + @Nullable String keystorePassword) { + SSL_CONTEXT_THREAD_LOCAL.set( + new Ssl(truststoreLocation, truststorePassword, keystoreLocation, keystorePassword)); + } + + // should be called when (host:port) -> factory cache should be invalidated (ex. on app config reload) + public static void clearFactoriesCache() { + CACHED_FACTORIES.clear(); + } + + public static void clearThreadLocalContext() { + SSL_CONTEXT_THREAD_LOCAL.set(null); + } + + public static void editJmxConnectorEnv(Map env) { + env.put("com.sun.jndi.rmi.factory.socket", new SslRMIClientSocketFactory()); + } + + //----------------------------------------------------------------------------------------------- + + private final javax.net.ssl.SSLSocketFactory defaultSocketFactory; + + @SneakyThrows + public JmxSslSocketFactory() { + this.defaultSocketFactory = SSLContext.getDefault().getSocketFactory(); + } + + @SneakyThrows + private javax.net.ssl.SSLSocketFactory createFactoryFromThreadLocalCtx() { + Ssl ssl = Preconditions.checkNotNull(SSL_CONTEXT_THREAD_LOCAL.get()); + + var trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + if (ssl.truststoreLocation() != null && ssl.truststorePassword() != null) { + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load( + new FileInputStream((ResourceUtils.getFile(ssl.truststoreLocation()))), + ssl.truststorePassword().toCharArray() + ); + trustManagerFactory.init(trustStore); + } + + var keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + if (ssl.keystoreLocation() != null && ssl.keystorePassword() != null) { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load( + new FileInputStream(ResourceUtils.getFile(ssl.keystoreLocation())), + ssl.keystorePassword().toCharArray() + ); + keyManagerFactory.init(keyStore, ssl.keystorePassword().toCharArray()); + } + + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init( + keyManagerFactory.getKeyManagers(), + trustManagerFactory.getTrustManagers(), + null + ); + return ctx.getSocketFactory(); + } + + private boolean threadLocalContextSet() { + return SSL_CONTEXT_THREAD_LOCAL.get() != null; + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + var hostAndPort = new HostAndPort(host, port); + if (CACHED_FACTORIES.containsKey(hostAndPort)) { + return CACHED_FACTORIES.get(hostAndPort).createSocket(host, port); + } else if (threadLocalContextSet()) { + var factory = createFactoryFromThreadLocalCtx(); + CACHED_FACTORIES.put(hostAndPort, factory); + return factory.createSocket(host, port); + } + return defaultSocketFactory.createSocket(host, port); + } + + /// FOLLOWING METHODS WON'T BE USED DURING JMX INTERACTION, IMPLEMENTING THEM JUST FOR CONSISTENCY ->>>>> + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { + if (threadLocalContextSet()) { + return createFactoryFromThreadLocalCtx().createSocket(s, host, port, autoClose); + } + return defaultSocketFactory.createSocket(s, host, port, autoClose); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) + throws IOException, UnknownHostException { + if (threadLocalContextSet()) { + return createFactoryFromThreadLocalCtx().createSocket(host, port, localHost, localPort); + } + return defaultSocketFactory.createSocket(host, port, localHost, localPort); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + if (threadLocalContextSet()) { + return createFactoryFromThreadLocalCtx().createSocket(host, port); + } + return defaultSocketFactory.createSocket(host, port); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) + throws IOException { + if (threadLocalContextSet()) { + return createFactoryFromThreadLocalCtx().createSocket(address, port, localAddress, localPort); + } + return defaultSocketFactory.createSocket(address, port, localAddress, localPort); + } + + @Override + public String[] getDefaultCipherSuites() { + if (threadLocalContextSet()) { + return createFactoryFromThreadLocalCtx().getDefaultCipherSuites(); + } + return defaultSocketFactory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + if (threadLocalContextSet()) { + return createFactoryFromThreadLocalCtx().getSupportedCipherSuites(); + } + return defaultSocketFactory.getSupportedCipherSuites(); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/MetricsCollector.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/MetricsCollector.java new file mode 100644 index 00000000000..fca7ab1fea0 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/MetricsCollector.java @@ -0,0 +1,69 @@ +package com.provectus.kafka.ui.service.metrics; + +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.Metrics; +import com.provectus.kafka.ui.model.MetricsConfig; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.common.Node; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +@Component +@Slf4j +@RequiredArgsConstructor +public class MetricsCollector { + + private final JmxMetricsRetriever jmxMetricsRetriever; + private final PrometheusMetricsRetriever prometheusMetricsRetriever; + + public Mono getBrokerMetrics(KafkaCluster cluster, Collection nodes) { + return Flux.fromIterable(nodes) + .flatMap(n -> getMetrics(cluster, n).map(lst -> Tuples.of(n, lst))) + .collectMap(Tuple2::getT1, Tuple2::getT2) + .map(nodeMetrics -> collectMetrics(cluster, nodeMetrics)) + .defaultIfEmpty(Metrics.empty()); + } + + private Mono> getMetrics(KafkaCluster kafkaCluster, Node node) { + Flux metricFlux = Flux.empty(); + if (kafkaCluster.getMetricsConfig() != null) { + String type = kafkaCluster.getMetricsConfig().getType(); + if (type == null || type.equalsIgnoreCase(MetricsConfig.JMX_METRICS_TYPE)) { + metricFlux = jmxMetricsRetriever.retrieve(kafkaCluster, node); + } else if (type.equalsIgnoreCase(MetricsConfig.PROMETHEUS_METRICS_TYPE)) { + metricFlux = prometheusMetricsRetriever.retrieve(kafkaCluster, node); + } + } + return metricFlux.collectList(); + } + + public Metrics collectMetrics(KafkaCluster cluster, Map> perBrokerMetrics) { + Metrics.MetricsBuilder builder = Metrics.builder() + .perBrokerMetrics( + perBrokerMetrics.entrySet() + .stream() + .collect(Collectors.toMap(e -> e.getKey().id(), Map.Entry::getValue))); + + populateWellknowMetrics(cluster, perBrokerMetrics) + .apply(builder); + + return builder.build(); + } + + private WellKnownMetrics populateWellknowMetrics(KafkaCluster cluster, Map> perBrokerMetrics) { + WellKnownMetrics wellKnownMetrics = new WellKnownMetrics(); + perBrokerMetrics.forEach((node, metrics) -> + metrics.forEach(metric -> + wellKnownMetrics.populate(node, metric))); + return wellKnownMetrics; + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/MetricsRetriever.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/MetricsRetriever.java new file mode 100644 index 00000000000..7e1e126fa0d --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/MetricsRetriever.java @@ -0,0 +1,9 @@ +package com.provectus.kafka.ui.service.metrics; + +import com.provectus.kafka.ui.model.KafkaCluster; +import org.apache.kafka.common.Node; +import reactor.core.publisher.Flux; + +interface MetricsRetriever { + Flux retrieve(KafkaCluster c, Node node); +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/PrometheusEndpointMetricsParser.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/PrometheusEndpointMetricsParser.java new file mode 100644 index 00000000000..1a51ca0afae --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/PrometheusEndpointMetricsParser.java @@ -0,0 +1,46 @@ +package com.provectus.kafka.ui.service.metrics; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.math.NumberUtils; + +@Slf4j +class PrometheusEndpointMetricsParser { + + /** + * Matches openmetrics format. For example, string: + * kafka_server_BrokerTopicMetrics_FiveMinuteRate{name="BytesInPerSec",topic="__consumer_offsets",} 16.94886650744339 + * will produce: + * name=kafka_server_BrokerTopicMetrics_FiveMinuteRate + * value=16.94886650744339 + * labels={name="BytesInPerSec", topic="__consumer_offsets"}", + */ + private static final Pattern PATTERN = Pattern.compile( + "(?^\\w+)([ \t]*\\{*(?.*)}*)[ \\t]+(?[\\d]+\\.?[\\d]+)?"); + + static Optional parse(String s) { + Matcher matcher = PATTERN.matcher(s); + if (matcher.matches()) { + String value = matcher.group("value"); + String metricName = matcher.group("metricName"); + if (metricName == null || !NumberUtils.isCreatable(value)) { + return Optional.empty(); + } + var labels = Arrays.stream(matcher.group("properties").split(",")) + .filter(str -> !"".equals(str)) + .map(str -> str.split("=")) + .filter(spit -> spit.length == 2) + .collect(Collectors.toUnmodifiableMap( + str -> str[0].trim(), + str -> str[1].trim().replace("\"", ""))); + + return Optional.of(RawMetric.create(metricName, labels, new BigDecimal(value))); + } + return Optional.empty(); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/PrometheusMetricsRetriever.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/PrometheusMetricsRetriever.java new file mode 100644 index 00000000000..33ef1b80723 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/PrometheusMetricsRetriever.java @@ -0,0 +1,70 @@ +package com.provectus.kafka.ui.service.metrics; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.provectus.kafka.ui.config.ClustersProperties; +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.MetricsConfig; +import com.provectus.kafka.ui.util.WebClientConfigurator; +import java.util.Arrays; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.common.Node; +import org.springframework.stereotype.Service; +import org.springframework.util.unit.DataSize; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriComponentsBuilder; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Service +@Slf4j +class PrometheusMetricsRetriever implements MetricsRetriever { + + private static final String METRICS_ENDPOINT_PATH = "/metrics"; + private static final int DEFAULT_EXPORTER_PORT = 11001; + + @Override + public Flux retrieve(KafkaCluster c, Node node) { + log.debug("Retrieving metrics from prometheus exporter: {}:{}", node.host(), c.getMetricsConfig().getPort()); + + MetricsConfig metricsConfig = c.getMetricsConfig(); + var webClient = new WebClientConfigurator() + .configureBufferSize(DataSize.ofMegabytes(20)) + .configureBasicAuth(metricsConfig.getUsername(), metricsConfig.getPassword()) + .configureSsl( + c.getOriginalProperties().getSsl(), + new ClustersProperties.KeystoreConfig( + metricsConfig.getKeystoreLocation(), + metricsConfig.getKeystorePassword())) + .build(); + + return retrieve(webClient, node.host(), c.getMetricsConfig()); + } + + @VisibleForTesting + Flux retrieve(WebClient webClient, String host, MetricsConfig metricsConfig) { + int port = Optional.ofNullable(metricsConfig.getPort()).orElse(DEFAULT_EXPORTER_PORT); + boolean sslEnabled = metricsConfig.isSsl() || metricsConfig.getKeystoreLocation() != null; + var request = webClient.get() + .uri(UriComponentsBuilder.newInstance() + .scheme(sslEnabled ? "https" : "http") + .host(host) + .port(port) + .path(METRICS_ENDPOINT_PATH).build().toUri()); + + WebClient.ResponseSpec responseSpec = request.retrieve(); + return responseSpec.bodyToMono(String.class) + .doOnError(e -> log.error("Error while getting metrics from {}", host, e)) + .onErrorResume(th -> Mono.empty()) + .flatMapMany(body -> + Flux.fromStream( + Arrays.stream(body.split("\\n")) + .filter(str -> !Strings.isNullOrEmpty(str) && !str.startsWith("#")) // skipping comments strings + .map(PrometheusEndpointMetricsParser::parse) + .filter(Optional::isPresent) + .map(Optional::get) + ) + ); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/RawMetric.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/RawMetric.java new file mode 100644 index 00000000000..659212f23ff --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/RawMetric.java @@ -0,0 +1,60 @@ +package com.provectus.kafka.ui.service.metrics; + +import java.math.BigDecimal; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +public interface RawMetric { + + String name(); + + Map labels(); + + BigDecimal value(); + + // Key, that can be used for metrics reductions + default Object identityKey() { + return name() + "_" + labels(); + } + + RawMetric copyWithValue(BigDecimal newValue); + + //-------------------------------------------------- + + static RawMetric create(String name, Map labels, BigDecimal value) { + return new SimpleMetric(name, labels, value); + } + + @AllArgsConstructor + @EqualsAndHashCode + @ToString + class SimpleMetric implements RawMetric { + + private final String name; + private final Map labels; + private final BigDecimal value; + + @Override + public String name() { + return name; + } + + @Override + public Map labels() { + return labels; + } + + @Override + public BigDecimal value() { + return value; + } + + @Override + public RawMetric copyWithValue(BigDecimal newValue) { + return new SimpleMetric(name, labels, newValue); + } + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/WellKnownMetrics.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/WellKnownMetrics.java new file mode 100644 index 00000000000..8dd4609b601 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/WellKnownMetrics.java @@ -0,0 +1,70 @@ +package com.provectus.kafka.ui.service.metrics; + +import static org.apache.commons.lang3.StringUtils.containsIgnoreCase; +import static org.apache.commons.lang3.StringUtils.endsWithIgnoreCase; + +import com.provectus.kafka.ui.model.Metrics; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; +import org.apache.kafka.common.Node; + +class WellKnownMetrics { + + private static final String BROKER_TOPIC_METRICS = "BrokerTopicMetrics"; + private static final String FIFTEEN_MINUTE_RATE = "FifteenMinuteRate"; + + // per broker + final Map brokerBytesInFifteenMinuteRate = new HashMap<>(); + final Map brokerBytesOutFifteenMinuteRate = new HashMap<>(); + + // per topic + final Map bytesInFifteenMinuteRate = new HashMap<>(); + final Map bytesOutFifteenMinuteRate = new HashMap<>(); + + void populate(Node node, RawMetric rawMetric) { + updateBrokerIOrates(node, rawMetric); + updateTopicsIOrates(rawMetric); + } + + void apply(Metrics.MetricsBuilder metricsBuilder) { + metricsBuilder.topicBytesInPerSec(bytesInFifteenMinuteRate); + metricsBuilder.topicBytesOutPerSec(bytesOutFifteenMinuteRate); + metricsBuilder.brokerBytesInPerSec(brokerBytesInFifteenMinuteRate); + metricsBuilder.brokerBytesOutPerSec(brokerBytesOutFifteenMinuteRate); + } + + private void updateBrokerIOrates(Node node, RawMetric rawMetric) { + String name = rawMetric.name(); + if (!brokerBytesInFifteenMinuteRate.containsKey(node.id()) + && rawMetric.labels().size() == 1 + && "BytesInPerSec".equalsIgnoreCase(rawMetric.labels().get("name")) + && containsIgnoreCase(name, BROKER_TOPIC_METRICS) + && endsWithIgnoreCase(name, FIFTEEN_MINUTE_RATE)) { + brokerBytesInFifteenMinuteRate.put(node.id(), rawMetric.value()); + } + if (!brokerBytesOutFifteenMinuteRate.containsKey(node.id()) + && rawMetric.labels().size() == 1 + && "BytesOutPerSec".equalsIgnoreCase(rawMetric.labels().get("name")) + && containsIgnoreCase(name, BROKER_TOPIC_METRICS) + && endsWithIgnoreCase(name, FIFTEEN_MINUTE_RATE)) { + brokerBytesOutFifteenMinuteRate.put(node.id(), rawMetric.value()); + } + } + + private void updateTopicsIOrates(RawMetric rawMetric) { + String name = rawMetric.name(); + String topic = rawMetric.labels().get("topic"); + if (topic != null + && containsIgnoreCase(name, BROKER_TOPIC_METRICS) + && endsWithIgnoreCase(name, FIFTEEN_MINUTE_RATE)) { + String nameProperty = rawMetric.labels().get("name"); + if ("BytesInPerSec".equalsIgnoreCase(nameProperty)) { + bytesInFifteenMinuteRate.compute(topic, (k, v) -> v == null ? rawMetric.value() : v.add(rawMetric.value())); + } else if ("BytesOutPerSec".equalsIgnoreCase(nameProperty)) { + bytesOutFifteenMinuteRate.compute(topic, (k, v) -> v == null ? rawMetric.value() : v.add(rawMetric.value())); + } + } + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AbstractProviderCondition.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AbstractProviderCondition.java new file mode 100644 index 00000000000..4382ad8646f --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AbstractProviderCondition.java @@ -0,0 +1,31 @@ +package com.provectus.kafka.ui.service.rbac; + +import com.provectus.kafka.ui.config.auth.OAuthProperties; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.core.env.Environment; + +public abstract class AbstractProviderCondition { + private static final Bindable> OAUTH2_PROPERTIES = Bindable + .mapOf(String.class, OAuthProperties.OAuth2Provider.class); + + protected Set getRegisteredProvidersTypes(final Environment env) { + final Map properties = Binder.get(env) + .bind("auth.oauth2.client", OAUTH2_PROPERTIES) + .orElse(Map.of()); + return properties.values().stream() + .map(OAuthProperties.OAuth2Provider::getCustomParams) + .filter(Objects::nonNull) + .filter(Predicate.not(Map::isEmpty)) + .map(params -> params.get("type")) + .filter(Objects::nonNull) + .filter(StringUtils::isNotEmpty) + .collect(Collectors.toSet()); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java new file mode 100644 index 00000000000..62a2962bed9 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java @@ -0,0 +1,486 @@ +package com.provectus.kafka.ui.service.rbac; + +import static com.provectus.kafka.ui.model.rbac.Resource.APPLICATIONCONFIG; + +import com.provectus.kafka.ui.config.auth.AuthenticatedUser; +import com.provectus.kafka.ui.config.auth.RbacUser; +import com.provectus.kafka.ui.config.auth.RoleBasedAccessControlProperties; +import com.provectus.kafka.ui.model.ClusterDTO; +import com.provectus.kafka.ui.model.ConnectDTO; +import com.provectus.kafka.ui.model.InternalTopic; +import com.provectus.kafka.ui.model.rbac.AccessContext; +import com.provectus.kafka.ui.model.rbac.Permission; +import com.provectus.kafka.ui.model.rbac.Resource; +import com.provectus.kafka.ui.model.rbac.Role; +import com.provectus.kafka.ui.model.rbac.Subject; +import com.provectus.kafka.ui.model.rbac.permission.ConnectAction; +import com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction; +import com.provectus.kafka.ui.model.rbac.permission.SchemaAction; +import com.provectus.kafka.ui.model.rbac.permission.TopicAction; +import com.provectus.kafka.ui.service.rbac.extractor.CognitoAuthorityExtractor; +import com.provectus.kafka.ui.service.rbac.extractor.GithubAuthorityExtractor; +import com.provectus.kafka.ui.service.rbac.extractor.GoogleAuthorityExtractor; +import com.provectus.kafka.ui.service.rbac.extractor.OauthAuthorityExtractor; +import com.provectus.kafka.ui.service.rbac.extractor.ProviderAuthorityExtractor; +import jakarta.annotation.PostConstruct; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.core.env.Environment; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import reactor.core.publisher.Mono; + +@Service +@RequiredArgsConstructor +@EnableConfigurationProperties(RoleBasedAccessControlProperties.class) +@Slf4j +public class AccessControlService { + + private static final String ACCESS_DENIED = "Access denied"; + private static final String ACTIONS_ARE_EMPTY = "actions are empty"; + + @Nullable + private final InMemoryReactiveClientRegistrationRepository clientRegistrationRepository; + private final RoleBasedAccessControlProperties properties; + private final Environment environment; + + private boolean rbacEnabled = false; + private Set oauthExtractors = Collections.emptySet(); + + @PostConstruct + public void init() { + if (CollectionUtils.isEmpty(properties.getRoles())) { + log.trace("No roles provided, disabling RBAC"); + return; + } + rbacEnabled = true; + + this.oauthExtractors = properties.getRoles() + .stream() + .map(role -> role.getSubjects() + .stream() + .map(Subject::getProvider) + .distinct() + .map(provider -> switch (provider) { + case OAUTH_COGNITO -> new CognitoAuthorityExtractor(); + case OAUTH_GOOGLE -> new GoogleAuthorityExtractor(); + case OAUTH_GITHUB -> new GithubAuthorityExtractor(); + case OAUTH -> new OauthAuthorityExtractor(); + default -> null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toSet())) + .flatMap(Set::stream) + .collect(Collectors.toSet()); + + if (!properties.getRoles().isEmpty() + && "oauth2".equalsIgnoreCase(environment.getProperty("auth.type")) + && (clientRegistrationRepository == null || !clientRegistrationRepository.iterator().hasNext())) { + log.error("Roles are configured but no authentication methods are present. Authentication might fail."); + } + } + + public Mono validateAccess(AccessContext context) { + if (!rbacEnabled) { + return Mono.empty(); + } + + if (CollectionUtils.isNotEmpty(context.getApplicationConfigActions())) { + return getUser() + .doOnNext(user -> { + boolean accessGranted = isApplicationConfigAccessible(context, user); + + if (!accessGranted) { + throw new AccessDeniedException(ACCESS_DENIED); + } + }).then(); + } + + return getUser() + .doOnNext(user -> { + boolean accessGranted = + isApplicationConfigAccessible(context, user) + && isClusterAccessible(context, user) + && isClusterConfigAccessible(context, user) + && isTopicAccessible(context, user) + && isConsumerGroupAccessible(context, user) + && isConnectAccessible(context, user) + && isConnectorAccessible(context, user) // TODO connector selectors + && isSchemaAccessible(context, user) + && isKsqlAccessible(context, user) + && isAclAccessible(context, user) + && isAuditAccessible(context, user); + + if (!accessGranted) { + throw new AccessDeniedException(ACCESS_DENIED); + } + }) + .then(); + } + + public Mono getUser() { + return ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .filter(authentication -> authentication.getPrincipal() instanceof RbacUser) + .map(authentication -> ((RbacUser) authentication.getPrincipal())) + .map(user -> new AuthenticatedUser(user.name(), user.groups())); + } + + public boolean isApplicationConfigAccessible(AccessContext context, AuthenticatedUser user) { + if (!rbacEnabled) { + return true; + } + if (CollectionUtils.isEmpty(context.getApplicationConfigActions())) { + return true; + } + Set requiredActions = context.getApplicationConfigActions() + .stream() + .map(a -> a.toString().toUpperCase()) + .collect(Collectors.toSet()); + return isAccessible(APPLICATIONCONFIG, null, user, context, requiredActions); + } + + private boolean isClusterAccessible(AccessContext context, AuthenticatedUser user) { + if (!rbacEnabled) { + return true; + } + + Assert.isTrue(StringUtils.isNotEmpty(context.getCluster()), "cluster value is empty"); + + return properties.getRoles() + .stream() + .filter(filterRole(user)) + .anyMatch(filterCluster(context.getCluster())); + } + + public Mono isClusterAccessible(ClusterDTO cluster) { + if (!rbacEnabled) { + return Mono.just(true); + } + + AccessContext accessContext = AccessContext + .builder() + .cluster(cluster.getName()) + .build(); + + return getUser().map(u -> isClusterAccessible(accessContext, u)); + } + + public boolean isClusterConfigAccessible(AccessContext context, AuthenticatedUser user) { + if (!rbacEnabled) { + return true; + } + + if (CollectionUtils.isEmpty(context.getClusterConfigActions())) { + return true; + } + Assert.isTrue(StringUtils.isNotEmpty(context.getCluster()), "cluster value is empty"); + + Set requiredActions = context.getClusterConfigActions() + .stream() + .map(a -> a.toString().toUpperCase()) + .collect(Collectors.toSet()); + + return isAccessible(Resource.CLUSTERCONFIG, context.getCluster(), user, context, requiredActions); + } + + public boolean isTopicAccessible(AccessContext context, AuthenticatedUser user) { + if (!rbacEnabled) { + return true; + } + + if (context.getTopic() == null && context.getTopicActions().isEmpty()) { + return true; + } + Assert.isTrue(!context.getTopicActions().isEmpty(), ACTIONS_ARE_EMPTY); + + Set requiredActions = context.getTopicActions() + .stream() + .map(a -> a.toString().toUpperCase()) + .collect(Collectors.toSet()); + + return isAccessible(Resource.TOPIC, context.getTopic(), user, context, requiredActions); + } + + public Mono> filterViewableTopics(List topics, String clusterName) { + if (!rbacEnabled) { + return Mono.just(topics); + } + + return getUser() + .map(user -> topics.stream() + .filter(topic -> { + var accessContext = AccessContext + .builder() + .cluster(clusterName) + .topic(topic.getName()) + .topicActions(TopicAction.VIEW) + .build(); + return isTopicAccessible(accessContext, user); + } + ).toList()); + } + + private boolean isConsumerGroupAccessible(AccessContext context, AuthenticatedUser user) { + if (!rbacEnabled) { + return true; + } + + if (context.getConsumerGroup() == null && context.getConsumerGroupActions().isEmpty()) { + return true; + } + Assert.isTrue(!context.getConsumerGroupActions().isEmpty(), ACTIONS_ARE_EMPTY); + + Set requiredActions = context.getConsumerGroupActions() + .stream() + .map(a -> a.toString().toUpperCase()) + .collect(Collectors.toSet()); + + return isAccessible(Resource.CONSUMER, context.getConsumerGroup(), user, context, requiredActions); + } + + public Mono isConsumerGroupAccessible(String groupId, String clusterName) { + if (!rbacEnabled) { + return Mono.just(true); + } + + AccessContext accessContext = AccessContext + .builder() + .cluster(clusterName) + .consumerGroup(groupId) + .consumerGroupActions(ConsumerGroupAction.VIEW) + .build(); + + return getUser().map(u -> isConsumerGroupAccessible(accessContext, u)); + } + + public boolean isSchemaAccessible(AccessContext context, AuthenticatedUser user) { + if (!rbacEnabled) { + return true; + } + + if (context.getSchema() == null && context.getSchemaActions().isEmpty()) { + return true; + } + Assert.isTrue(!context.getSchemaActions().isEmpty(), ACTIONS_ARE_EMPTY); + + Set requiredActions = context.getSchemaActions() + .stream() + .map(a -> a.toString().toUpperCase()) + .collect(Collectors.toSet()); + + return isAccessible(Resource.SCHEMA, context.getSchema(), user, context, requiredActions); + } + + public Mono isSchemaAccessible(String schema, String clusterName) { + if (!rbacEnabled) { + return Mono.just(true); + } + + AccessContext accessContext = AccessContext + .builder() + .cluster(clusterName) + .schema(schema) + .schemaActions(SchemaAction.VIEW) + .build(); + + return getUser().map(u -> isSchemaAccessible(accessContext, u)); + } + + public boolean isConnectAccessible(AccessContext context, AuthenticatedUser user) { + if (!rbacEnabled) { + return true; + } + + if (context.getConnect() == null && context.getConnectActions().isEmpty()) { + return true; + } + Assert.isTrue(!context.getConnectActions().isEmpty(), ACTIONS_ARE_EMPTY); + + Set requiredActions = context.getConnectActions() + .stream() + .map(a -> a.toString().toUpperCase()) + .collect(Collectors.toSet()); + + return isAccessible(Resource.CONNECT, context.getConnect(), user, context, requiredActions); + } + + public Mono isConnectAccessible(ConnectDTO dto, String clusterName) { + if (!rbacEnabled) { + return Mono.just(true); + } + + return isConnectAccessible(dto.getName(), clusterName); + } + + public Mono isConnectAccessible(String connectName, String clusterName) { + if (!rbacEnabled) { + return Mono.just(true); + } + + AccessContext accessContext = AccessContext + .builder() + .cluster(clusterName) + .connect(connectName) + .connectActions(ConnectAction.VIEW) + .build(); + + return getUser().map(u -> isConnectAccessible(accessContext, u)); + } + + public boolean isConnectorAccessible(AccessContext context, AuthenticatedUser user) { + if (!rbacEnabled) { + return true; + } + + return isConnectAccessible(context, user); + } + + public Mono isConnectorAccessible(String connectName, String connectorName, String clusterName) { + if (!rbacEnabled) { + return Mono.just(true); + } + + AccessContext accessContext = AccessContext + .builder() + .cluster(clusterName) + .connect(connectName) + .connectActions(ConnectAction.VIEW) + .connector(connectorName) + .build(); + + return getUser().map(u -> isConnectorAccessible(accessContext, u)); + } + + private boolean isKsqlAccessible(AccessContext context, AuthenticatedUser user) { + if (!rbacEnabled) { + return true; + } + + if (context.getKsqlActions().isEmpty()) { + return true; + } + + Set requiredActions = context.getKsqlActions() + .stream() + .map(a -> a.toString().toUpperCase()) + .collect(Collectors.toSet()); + + return isAccessible(Resource.KSQL, null, user, context, requiredActions); + } + + private boolean isAclAccessible(AccessContext context, AuthenticatedUser user) { + if (!rbacEnabled) { + return true; + } + + if (context.getAclActions().isEmpty()) { + return true; + } + + Set requiredActions = context.getAclActions() + .stream() + .map(a -> a.toString().toUpperCase()) + .collect(Collectors.toSet()); + + return isAccessible(Resource.ACL, null, user, context, requiredActions); + } + + private boolean isAuditAccessible(AccessContext context, AuthenticatedUser user) { + if (!rbacEnabled) { + return true; + } + + if (context.getAuditAction().isEmpty()) { + return true; + } + + Set requiredActions = context.getAuditAction() + .stream() + .map(a -> a.toString().toUpperCase()) + .collect(Collectors.toSet()); + + return isAccessible(Resource.AUDIT, null, user, context, requiredActions); + } + + public Set getOauthExtractors() { + return oauthExtractors; + } + + public List getRoles() { + if (!rbacEnabled) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(properties.getRoles()); + } + + private boolean isAccessible(Resource resource, @Nullable String resourceValue, + AuthenticatedUser user, AccessContext context, Set requiredActions) { + Set grantedActions = properties.getRoles() + .stream() + .filter(filterRole(user)) + .filter(filterCluster(resource, context.getCluster())) + .flatMap(grantedRole -> grantedRole.getPermissions().stream()) + .filter(filterResource(resource)) + .filter(filterResourceValue(resourceValue)) + .flatMap(grantedPermission -> grantedPermission.getActions().stream()) + .map(String::toUpperCase) + .collect(Collectors.toSet()); + + return grantedActions.containsAll(requiredActions); + } + + private Predicate filterRole(AuthenticatedUser user) { + return role -> user.groups().contains(role.getName()); + } + + private Predicate filterCluster(String cluster) { + return grantedRole -> grantedRole.getClusters() + .stream() + .anyMatch(cluster::equalsIgnoreCase); + } + + private Predicate filterCluster(Resource resource, String cluster) { + if (resource == APPLICATIONCONFIG) { + return role -> true; + } + return filterCluster(cluster); + } + + private Predicate filterResource(Resource resource) { + return grantedPermission -> resource == grantedPermission.getResource(); + } + + private Predicate filterResourceValue(@Nullable String resourceValue) { + + if (resourceValue == null) { + return grantedPermission -> true; + } + return grantedPermission -> { + Pattern valuePattern = grantedPermission.getCompiledValuePattern(); + if (valuePattern == null) { + return true; + } + return valuePattern.matcher(resourceValue).matches(); + }; + } + + public boolean isRbacEnabled() { + return rbacEnabled; + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/CognitoAuthorityExtractor.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/CognitoAuthorityExtractor.java new file mode 100644 index 00000000000..58ac063bab3 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/CognitoAuthorityExtractor.java @@ -0,0 +1,72 @@ +package com.provectus.kafka.ui.service.rbac.extractor; + +import static com.provectus.kafka.ui.model.rbac.provider.Provider.Name.COGNITO; + +import com.google.common.collect.Sets; +import com.provectus.kafka.ui.model.rbac.Role; +import com.provectus.kafka.ui.model.rbac.provider.Provider; +import com.provectus.kafka.ui.service.rbac.AccessControlService; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import reactor.core.publisher.Mono; + +@Slf4j +public class CognitoAuthorityExtractor implements ProviderAuthorityExtractor { + + private static final String COGNITO_GROUPS_ATTRIBUTE_NAME = "cognito:groups"; + + @Override + public boolean isApplicable(String provider, Map customParams) { + return COGNITO.equalsIgnoreCase(provider) || COGNITO.equalsIgnoreCase(customParams.get(TYPE)); + } + + @Override + public Mono> extract(AccessControlService acs, Object value, Map additionalParams) { + log.debug("Extracting cognito user authorities"); + + DefaultOAuth2User principal; + try { + principal = (DefaultOAuth2User) value; + } catch (ClassCastException e) { + log.error("Can't cast value to DefaultOAuth2User", e); + throw new RuntimeException(); + } + + Set groupsByUsername = acs.getRoles() + .stream() + .filter(r -> r.getSubjects() + .stream() + .filter(s -> s.getProvider().equals(Provider.OAUTH_COGNITO)) + .filter(s -> s.getType().equals("user")) + .anyMatch(s -> s.getValue().equals(principal.getName()))) + .map(Role::getName) + .collect(Collectors.toSet()); + + List groups = principal.getAttribute(COGNITO_GROUPS_ATTRIBUTE_NAME); + if (groups == null) { + log.debug("Cognito groups param is not present"); + return Mono.just(groupsByUsername); + } + + Set groupsByGroups = acs.getRoles() + .stream() + .filter(role -> role.getSubjects() + .stream() + .filter(s -> s.getProvider().equals(Provider.OAUTH_COGNITO)) + .filter(s -> s.getType().equals("group")) + .anyMatch(subject -> groups + .stream() + .anyMatch(cognitoGroup -> cognitoGroup.equals(subject.getValue())) + )) + .map(Role::getName) + .collect(Collectors.toSet()); + + return Mono.just(Sets.union(groupsByUsername, groupsByGroups)); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GithubAuthorityExtractor.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GithubAuthorityExtractor.java new file mode 100644 index 00000000000..90c4ceebc60 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GithubAuthorityExtractor.java @@ -0,0 +1,188 @@ +package com.provectus.kafka.ui.service.rbac.extractor; + +import static com.provectus.kafka.ui.model.rbac.provider.Provider.Name.GITHUB; + +import com.provectus.kafka.ui.model.rbac.Role; +import com.provectus.kafka.ui.model.rbac.provider.Provider; +import com.provectus.kafka.ui.service.rbac.AccessControlService; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.security.config.oauth2.client.CommonOAuth2Provider; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@Slf4j +public class GithubAuthorityExtractor implements ProviderAuthorityExtractor { + + private static final String ORGANIZATION_ATTRIBUTE_NAME = "organizations_url"; + private static final String USERNAME_ATTRIBUTE_NAME = "login"; + private static final String ORGANIZATION_NAME = "login"; + private static final String ORGANIZATION = "organization"; + private static final String TEAM_NAME = "slug"; + private static final String GITHUB_ACCEPT_HEADER = "application/vnd.github+json"; + private static final String DUMMY = "dummy"; + // The number of results (max 100) per page of list organizations for authenticated user. + private static final Integer ORGANIZATIONS_PER_PAGE = 100; + + @Override + public boolean isApplicable(String provider, Map customParams) { + return GITHUB.equalsIgnoreCase(provider) || GITHUB.equalsIgnoreCase(customParams.get(TYPE)); + } + + @Override + public Mono> extract(AccessControlService acs, Object value, Map additionalParams) { + DefaultOAuth2User principal; + try { + principal = (DefaultOAuth2User) value; + } catch (ClassCastException e) { + log.error("Can't cast value to DefaultOAuth2User", e); + throw new RuntimeException(); + } + + Set rolesByUsername = new HashSet<>(); + String username = principal.getAttribute(USERNAME_ATTRIBUTE_NAME); + if (username == null) { + log.debug("Github username param is not present"); + } else { + acs.getRoles() + .stream() + .filter(r -> r.getSubjects() + .stream() + .filter(s -> s.getProvider().equals(Provider.OAUTH_GITHUB)) + .filter(s -> s.getType().equals("user")) + .anyMatch(s -> s.getValue().equals(username))) + .map(Role::getName) + .forEach(rolesByUsername::add); + } + + OAuth2UserRequest req = (OAuth2UserRequest) additionalParams.get("request"); + String infoEndpoint = req.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri(); + + if (infoEndpoint == null) { + infoEndpoint = CommonOAuth2Provider.GITHUB + .getBuilder(DUMMY) + .clientId(DUMMY) + .build() + .getProviderDetails() + .getUserInfoEndpoint() + .getUri(); + } + var webClient = WebClient.create(infoEndpoint); + + Mono> rolesByOrganization = getOrganizationRoles(principal, additionalParams, acs, webClient); + Mono> rolesByTeams = getTeamRoles(webClient, additionalParams, acs); + + return Mono.zip(rolesByOrganization, rolesByTeams) + .map((t) -> Stream.of(t.getT1(), t.getT2(), rolesByUsername) + .flatMap(Collection::stream) + .collect(Collectors.toSet())); + } + + private Mono> getOrganizationRoles(DefaultOAuth2User principal, Map additionalParams, + AccessControlService acs, WebClient webClient) { + String organization = principal.getAttribute(ORGANIZATION_ATTRIBUTE_NAME); + if (organization == null) { + log.debug("Github organization param is not present"); + return Mono.just(Collections.emptySet()); + } + + final Mono>> userOrganizations = webClient + .get() + .uri(uriBuilder -> uriBuilder.path("/orgs") + .queryParam("per_page", ORGANIZATIONS_PER_PAGE) + .build()) + .headers(headers -> { + headers.set(HttpHeaders.ACCEPT, GITHUB_ACCEPT_HEADER); + OAuth2UserRequest request = (OAuth2UserRequest) additionalParams.get("request"); + headers.setBearerAuth(request.getAccessToken().getTokenValue()); + }) + .retrieve() + //@formatter:off + .bodyToMono(new ParameterizedTypeReference<>() {}); + //@formatter:on + + return userOrganizations + .map(orgsMap -> acs.getRoles() + .stream() + .filter(role -> role.getSubjects() + .stream() + .filter(s -> s.getProvider().equals(Provider.OAUTH_GITHUB)) + .filter(s -> s.getType().equals(ORGANIZATION)) + .anyMatch(subject -> orgsMap.stream() + .map(org -> org.get(ORGANIZATION_NAME).toString()) + .anyMatch(orgName -> orgName.equalsIgnoreCase(subject.getValue())) + )) + .map(Role::getName) + .collect(Collectors.toSet())); + } + + @SuppressWarnings("unchecked") + private Mono> getTeamRoles(WebClient webClient, Map additionalParams, + AccessControlService acs) { + + var requestedTeams = acs.getRoles() + .stream() + .filter(r -> r.getSubjects() + .stream() + .filter(s -> s.getProvider().equals(Provider.OAUTH_GITHUB)) + .anyMatch(s -> s.getType().equals("team"))) + .collect(Collectors.toSet()); + + if (requestedTeams.isEmpty()) { + log.debug("No roles with github teams found, skipping"); + return Mono.just(Collections.emptySet()); + } + + final Mono>> rawTeams = webClient + .get() + .uri(uriBuilder -> uriBuilder.path("/teams") + .queryParam("per_page", ORGANIZATIONS_PER_PAGE) + .build()) + .headers(headers -> { + headers.set(HttpHeaders.ACCEPT, GITHUB_ACCEPT_HEADER); + OAuth2UserRequest request = (OAuth2UserRequest) additionalParams.get("request"); + headers.setBearerAuth(request.getAccessToken().getTokenValue()); + }) + .retrieve() + //@formatter:off + .bodyToMono(new ParameterizedTypeReference<>() {}); + //@formatter:on + + final Mono> mappedTeams = rawTeams + .map(teams -> teams.stream() + .map(teamInfo -> { + var name = teamInfo.get(TEAM_NAME); + var orgInfo = (Map) teamInfo.get(ORGANIZATION); + var orgName = orgInfo.get(ORGANIZATION_NAME); + return orgName + "/" + name; + }) + .map(Object::toString) + .collect(Collectors.toList()) + ); + + return mappedTeams + .map(teams -> acs.getRoles() + .stream() + .filter(role -> role.getSubjects() + .stream() + .filter(s -> s.getProvider().equals(Provider.OAUTH_GITHUB)) + .filter(s -> s.getType().equals("team")) + .anyMatch(subject -> teams.stream() + .anyMatch(teamName -> teamName.equalsIgnoreCase(subject.getValue())) + )) + .map(Role::getName) + .collect(Collectors.toSet())); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GoogleAuthorityExtractor.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GoogleAuthorityExtractor.java new file mode 100644 index 00000000000..fcc372d5a3e --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GoogleAuthorityExtractor.java @@ -0,0 +1,69 @@ +package com.provectus.kafka.ui.service.rbac.extractor; + +import static com.provectus.kafka.ui.model.rbac.provider.Provider.Name.GOOGLE; + +import com.google.common.collect.Sets; +import com.provectus.kafka.ui.model.rbac.Role; +import com.provectus.kafka.ui.model.rbac.provider.Provider; +import com.provectus.kafka.ui.service.rbac.AccessControlService; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import reactor.core.publisher.Mono; + +@Slf4j +public class GoogleAuthorityExtractor implements ProviderAuthorityExtractor { + + private static final String GOOGLE_DOMAIN_ATTRIBUTE_NAME = "hd"; + public static final String EMAIL_ATTRIBUTE_NAME = "email"; + + @Override + public boolean isApplicable(String provider, Map customParams) { + return GOOGLE.equalsIgnoreCase(provider) || GOOGLE.equalsIgnoreCase(customParams.get(TYPE)); + } + + @Override + public Mono> extract(AccessControlService acs, Object value, Map additionalParams) { + log.debug("Extracting google user authorities"); + + DefaultOAuth2User principal; + try { + principal = (DefaultOAuth2User) value; + } catch (ClassCastException e) { + log.error("Can't cast value to DefaultOAuth2User", e); + throw new RuntimeException(); + } + + Set groupsByUsername = acs.getRoles() + .stream() + .filter(r -> r.getSubjects() + .stream() + .filter(s -> s.getProvider().equals(Provider.OAUTH_GOOGLE)) + .filter(s -> s.getType().equals("user")) + .anyMatch(s -> s.getValue().equals(principal.getAttribute(EMAIL_ATTRIBUTE_NAME)))) + .map(Role::getName) + .collect(Collectors.toSet()); + + + String domain = principal.getAttribute(GOOGLE_DOMAIN_ATTRIBUTE_NAME); + if (domain == null) { + log.debug("Google domain param is not present"); + return Mono.just(groupsByUsername); + } + + Set groupsByDomain = acs.getRoles() + .stream() + .filter(r -> r.getSubjects() + .stream() + .filter(s -> s.getProvider().equals(Provider.OAUTH_GOOGLE)) + .filter(s -> s.getType().equals("domain")) + .anyMatch(s -> s.getValue().equals(domain))) + .map(Role::getName) + .collect(Collectors.toSet()); + + return Mono.just(Sets.union(groupsByUsername, groupsByDomain)); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/OauthAuthorityExtractor.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/OauthAuthorityExtractor.java new file mode 100644 index 00000000000..c935235d519 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/OauthAuthorityExtractor.java @@ -0,0 +1,113 @@ +package com.provectus.kafka.ui.service.rbac.extractor; + +import static com.provectus.kafka.ui.model.rbac.provider.Provider.Name.OAUTH; + +import com.google.common.collect.Sets; +import com.provectus.kafka.ui.config.auth.OAuthProperties; +import com.provectus.kafka.ui.model.rbac.Role; +import com.provectus.kafka.ui.model.rbac.provider.Provider; +import com.provectus.kafka.ui.service.rbac.AccessControlService; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.util.Assert; +import reactor.core.publisher.Mono; + +@Slf4j +public class OauthAuthorityExtractor implements ProviderAuthorityExtractor { + + public static final String ROLES_FIELD_PARAM_NAME = "roles-field"; + + @Override + public boolean isApplicable(String provider, Map customParams) { + var containsRolesFieldNameParam = customParams.containsKey(ROLES_FIELD_PARAM_NAME); + if (!containsRolesFieldNameParam) { + log.debug("Provider [{}] doesn't contain a roles field param name, mapping won't be performed", provider); + return false; + } + + return OAUTH.equalsIgnoreCase(provider) || OAUTH.equalsIgnoreCase(customParams.get(TYPE)); + } + + @Override + public Mono> extract(AccessControlService acs, Object value, Map additionalParams) { + log.trace("Extracting OAuth2 user authorities"); + + DefaultOAuth2User principal; + try { + principal = (DefaultOAuth2User) value; + } catch (ClassCastException e) { + log.error("Can't cast value to DefaultOAuth2User", e); + throw new RuntimeException(); + } + + var provider = (OAuthProperties.OAuth2Provider) additionalParams.get("provider"); + Assert.notNull(provider, "provider is null"); + var rolesFieldName = provider.getCustomParams().get(ROLES_FIELD_PARAM_NAME); + + Set rolesByUsername = acs.getRoles() + .stream() + .filter(r -> r.getSubjects() + .stream() + .filter(s -> s.getProvider().equals(Provider.OAUTH)) + .filter(s -> s.getType().equals("user")) + .anyMatch(s -> s.getValue().equals(principal.getName()))) + .map(Role::getName) + .collect(Collectors.toSet()); + + Set rolesByRolesField = acs.getRoles() + .stream() + .filter(role -> role.getSubjects() + .stream() + .filter(s -> s.getProvider().equals(Provider.OAUTH)) + .filter(s -> s.getType().equals("role")) + .anyMatch(subject -> { + var roleName = subject.getValue(); + var principalRoles = convertRoles(principal.getAttribute(rolesFieldName)); + var roleMatched = principalRoles.contains(roleName); + + if (roleMatched) { + log.debug("Assigning role [{}] to user [{}]", roleName, principal.getName()); + } else { + log.trace("Role [{}] not found in user [{}] roles", roleName, principal.getName()); + } + + return roleMatched; + }) + ) + .map(Role::getName) + .collect(Collectors.toSet()); + + return Mono.just(Sets.union(rolesByUsername, rolesByRolesField)); + } + + @SuppressWarnings("unchecked") + private Collection convertRoles(Object roles) { + if (roles == null) { + log.debug("Param missing from attributes, skipping"); + return Collections.emptySet(); + } + + if ((roles instanceof List) || (roles instanceof Set)) { + log.trace("The field is either a set or a list, returning as is"); + return (Collection) roles; + } + + if (!(roles instanceof String)) { + log.debug("The field is not a string, skipping"); + return Collections.emptySet(); + } + + log.trace("Trying to deserialize the field value [{}] as a string", roles); + + return Arrays.stream(((String) roles).split(",")) + .collect(Collectors.toSet()); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/ProviderAuthorityExtractor.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/ProviderAuthorityExtractor.java new file mode 100644 index 00000000000..02c6d3017fe --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/ProviderAuthorityExtractor.java @@ -0,0 +1,16 @@ +package com.provectus.kafka.ui.service.rbac.extractor; + +import com.provectus.kafka.ui.service.rbac.AccessControlService; +import java.util.Map; +import java.util.Set; +import reactor.core.publisher.Mono; + +public interface ProviderAuthorityExtractor { + + String TYPE = "type"; + + boolean isApplicable(String provider, Map customParams); + + Mono> extract(AccessControlService acs, Object value, Map additionalParams); + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java new file mode 100644 index 00000000000..ba30eb5cc38 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java @@ -0,0 +1,78 @@ +package com.provectus.kafka.ui.service.rbac.extractor; + +import com.provectus.kafka.ui.config.auth.LdapProperties; +import com.provectus.kafka.ui.model.rbac.Role; +import com.provectus.kafka.ui.model.rbac.provider.Provider; +import com.provectus.kafka.ui.service.rbac.AccessControlService; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator; +import org.springframework.util.Assert; + +@Slf4j +public class RbacLdapAuthoritiesExtractor extends DefaultLdapAuthoritiesPopulator { + + private final AccessControlService acs; + private final LdapProperties props; + + public RbacLdapAuthoritiesExtractor(ApplicationContext context, + BaseLdapPathContextSource contextSource, String groupFilterSearchBase) { + super(contextSource, groupFilterSearchBase); + this.acs = context.getBean(AccessControlService.class); + this.props = context.getBean(LdapProperties.class); + } + + @Override + protected Set getAdditionalRoles(DirContextOperations user, String username) { + var ldapGroups = getRoles(user.getNameInNamespace(), username); + + return acs.getRoles() + .stream() + .filter(r -> r.getSubjects() + .stream() + .filter(subject -> subject.getProvider().equals(Provider.LDAP)) + .filter(subject -> subject.getType().equals("group")) + .anyMatch(subject -> ldapGroups.contains(subject.getValue())) + ) + .map(Role::getName) + .peek(role -> log.trace("Mapped role [{}] for user [{}]", role, username)) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + } + + private Set getRoles(String userDn, String username) { + var groupSearchBase = props.getGroupFilterSearchBase(); + Assert.notNull(groupSearchBase, "groupSearchBase is empty"); + + var groupRoleAttribute = props.getGroupRoleAttribute(); + if (groupRoleAttribute == null) { + + groupRoleAttribute = "cn"; + } + + log.trace( + "Searching for roles for user [{}] with DN [{}], groupRoleAttribute [{}] and filter [{}] in search base [{}]", + username, userDn, groupRoleAttribute, getGroupSearchFilter(), groupSearchBase); + + var ldapTemplate = getLdapTemplate(); + ldapTemplate.setIgnoreNameNotFoundException(true); + + Set>> userRoles = ldapTemplate.searchForMultipleAttributeValues( + groupSearchBase, getGroupSearchFilter(), new String[] {userDn, username}, + new String[] {groupRoleAttribute}); + + return userRoles.stream() + .map(record -> record.get(getGroupRoleAttribute()).get(0)) + .peek(group -> log.trace("Found LDAP group [{}] for user [{}]", group, username)) + .collect(Collectors.toSet()); + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/BaseStrategy.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/BaseStrategy.java deleted file mode 100644 index fa057116adb..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/BaseStrategy.java +++ /dev/null @@ -1,166 +0,0 @@ -package com.provectus.kafka.ui.strategy.ksql.statement; - -import com.fasterxml.jackson.databind.JsonNode; -import com.provectus.kafka.ui.exception.UnprocessableEntityException; -import com.provectus.kafka.ui.model.KsqlCommandDTO; -import com.provectus.kafka.ui.model.KsqlCommandResponseDTO; -import com.provectus.kafka.ui.model.TableDTO; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Spliterator; -import java.util.Spliterators; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -public abstract class BaseStrategy { - protected static final String KSQL_REQUEST_PATH = "/ksql"; - protected static final String QUERY_REQUEST_PATH = "/query"; - private static final String MAPPING_EXCEPTION_ERROR = "KSQL DB response mapping error"; - protected String host = null; - protected KsqlCommandDTO ksqlCommand = null; - - public String getUri() { - if (this.host != null) { - return this.host + this.getRequestPath(); - } - throw new UnprocessableEntityException("Strategy doesn't have host"); - } - - public boolean test(String sql) { - return sql.trim().toLowerCase().matches(getTestRegExp()); - } - - public BaseStrategy host(String host) { - this.host = host; - return this; - } - - public KsqlCommandDTO getKsqlCommand() { - return ksqlCommand; - } - - public BaseStrategy ksqlCommand(KsqlCommandDTO ksqlCommand) { - this.ksqlCommand = ksqlCommand; - return this; - } - - protected String getRequestPath() { - return BaseStrategy.KSQL_REQUEST_PATH; - } - - protected KsqlCommandResponseDTO serializeTableResponse(JsonNode response, String key) { - JsonNode item = getResponseFirstItemValue(response, key); - TableDTO table = item.isArray() ? getTableFromArray(item) : getTableFromObject(item); - return (new KsqlCommandResponseDTO()).data(table); - } - - protected KsqlCommandResponseDTO serializeMessageResponse(JsonNode response, String key) { - JsonNode item = getResponseFirstItemValue(response, key); - return (new KsqlCommandResponseDTO()).message(getMessageFromObject(item)); - } - - protected KsqlCommandResponseDTO serializeQueryResponse(JsonNode response) { - if (response.isArray() && response.size() > 0) { - TableDTO table = (new TableDTO()) - .headers(getQueryResponseHeader(response)) - .rows(getQueryResponseRows(response)); - return (new KsqlCommandResponseDTO()).data(table); - } - throw new UnprocessableEntityException(MAPPING_EXCEPTION_ERROR); - } - - private JsonNode getResponseFirstItemValue(JsonNode response, String key) { - if (response.isArray() && response.size() > 0) { - JsonNode first = response.get(0); - if (first.has(key)) { - return first.path(key); - } - } - throw new UnprocessableEntityException(MAPPING_EXCEPTION_ERROR); - } - - private List getQueryResponseHeader(JsonNode response) { - JsonNode headerRow = response.get(0); - if (headerRow.isObject() && headerRow.has("header")) { - String schema = headerRow.get("header").get("schema").asText(); - return Arrays.stream(schema.split(",")).map(String::trim).collect(Collectors.toList()); - } - return new ArrayList<>(); - } - - private List> getQueryResponseRows(JsonNode node) { - return getStreamForJsonArray(node) - .filter(row -> row.has("row") && row.get("row").has("columns")) - .map(row -> row.get("row").get("columns")) - .map(cellNode -> getStreamForJsonArray(cellNode) - .map(JsonNode::asText) - .collect(Collectors.toList()) - ) - .collect(Collectors.toList()); - } - - private TableDTO getTableFromArray(JsonNode node) { - TableDTO table = new TableDTO(); - table.headers(new ArrayList<>()).rows(new ArrayList<>()); - if (node.size() > 0) { - List keys = getJsonObjectKeys(node.get(0)); - List> rows = getTableRows(node, keys); - table.headers(keys).rows(rows); - } - return table; - } - - private TableDTO getTableFromObject(JsonNode node) { - List keys = getJsonObjectKeys(node); - List values = getJsonObjectValues(node); - List> rows = IntStream - .range(0, keys.size()) - .mapToObj(i -> List.of(keys.get(i), values.get(i))) - .collect(Collectors.toList()); - return (new TableDTO()).headers(List.of("key", "value")).rows(rows); - } - - private String getMessageFromObject(JsonNode node) { - if (node.isObject() && node.has("message")) { - return node.get("message").asText(); - } - throw new UnprocessableEntityException(MAPPING_EXCEPTION_ERROR); - } - - private List> getTableRows(JsonNode node, List keys) { - return getStreamForJsonArray(node) - .map(row -> keys.stream() - .map(header -> row.get(header).asText()) - .collect(Collectors.toList()) - ) - .collect(Collectors.toList()); - } - - private Stream getStreamForJsonArray(JsonNode node) { - if (node.isArray() && node.size() > 0) { - return StreamSupport.stream(node.spliterator(), false); - } - throw new UnprocessableEntityException(MAPPING_EXCEPTION_ERROR); - } - - private List getJsonObjectKeys(JsonNode node) { - if (node.isObject()) { - return StreamSupport.stream( - Spliterators.spliteratorUnknownSize(node.fieldNames(), Spliterator.ORDERED), false - ).collect(Collectors.toList()); - } - throw new UnprocessableEntityException(MAPPING_EXCEPTION_ERROR); - } - - private List getJsonObjectValues(JsonNode node) { - return getJsonObjectKeys(node).stream().map(key -> node.get(key).asText()) - .collect(Collectors.toList()); - } - - public abstract KsqlCommandResponseDTO serializeResponse(JsonNode response); - - protected abstract String getTestRegExp(); -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/CreateStrategy.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/CreateStrategy.java deleted file mode 100644 index d26046a0fdd..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/CreateStrategy.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.provectus.kafka.ui.strategy.ksql.statement; - -import com.fasterxml.jackson.databind.JsonNode; -import com.provectus.kafka.ui.model.KsqlCommandResponseDTO; -import org.springframework.stereotype.Component; - -@Component -public class CreateStrategy extends BaseStrategy { - private static final String RESPONSE_VALUE_KEY = "commandStatus"; - - @Override - public KsqlCommandResponseDTO serializeResponse(JsonNode response) { - return serializeMessageResponse(response, RESPONSE_VALUE_KEY); - } - - @Override - protected String getTestRegExp() { - return "create (table|stream)(.*)(with|as select(.*)from)(.*);"; - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/DescribeStrategy.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/DescribeStrategy.java deleted file mode 100644 index b8d7435bad7..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/DescribeStrategy.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.provectus.kafka.ui.strategy.ksql.statement; - -import com.fasterxml.jackson.databind.JsonNode; -import com.provectus.kafka.ui.model.KsqlCommandResponseDTO; -import org.springframework.stereotype.Component; - -@Component -public class DescribeStrategy extends BaseStrategy { - private static final String RESPONSE_VALUE_KEY = "sourceDescription"; - - @Override - public KsqlCommandResponseDTO serializeResponse(JsonNode response) { - return serializeTableResponse(response, RESPONSE_VALUE_KEY); - } - - @Override - protected String getTestRegExp() { - return "describe (.*);"; - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/DropStrategy.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/DropStrategy.java deleted file mode 100644 index 95b6884dc10..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/DropStrategy.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.provectus.kafka.ui.strategy.ksql.statement; - -import com.fasterxml.jackson.databind.JsonNode; -import com.provectus.kafka.ui.model.KsqlCommandResponseDTO; -import org.springframework.stereotype.Component; - -@Component -public class DropStrategy extends BaseStrategy { - private static final String RESPONSE_VALUE_KEY = "commandStatus"; - - @Override - public KsqlCommandResponseDTO serializeResponse(JsonNode response) { - return serializeMessageResponse(response, RESPONSE_VALUE_KEY); - } - - @Override - protected String getTestRegExp() { - return "drop (table|stream) (.*);"; - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/ExplainStrategy.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/ExplainStrategy.java deleted file mode 100644 index 221113b7e8a..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/ExplainStrategy.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.provectus.kafka.ui.strategy.ksql.statement; - -import com.fasterxml.jackson.databind.JsonNode; -import com.provectus.kafka.ui.model.KsqlCommandResponseDTO; -import org.springframework.stereotype.Component; - -@Component -public class ExplainStrategy extends BaseStrategy { - private static final String RESPONSE_VALUE_KEY = "queryDescription"; - - @Override - public KsqlCommandResponseDTO serializeResponse(JsonNode response) { - return serializeTableResponse(response, RESPONSE_VALUE_KEY); - } - - @Override - protected String getTestRegExp() { - return "explain (.*);"; - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/SelectStrategy.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/SelectStrategy.java deleted file mode 100644 index e535c8107df..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/SelectStrategy.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.provectus.kafka.ui.strategy.ksql.statement; - -import com.fasterxml.jackson.databind.JsonNode; -import com.provectus.kafka.ui.model.KsqlCommandResponseDTO; -import org.springframework.stereotype.Component; - -@Component -public class SelectStrategy extends BaseStrategy { - - @Override - public KsqlCommandResponseDTO serializeResponse(JsonNode response) { - return serializeQueryResponse(response); - } - - @Override - protected String getRequestPath() { - return BaseStrategy.QUERY_REQUEST_PATH; - } - - @Override - protected String getTestRegExp() { - return "select (.*) from (.*);"; - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/ShowStrategy.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/ShowStrategy.java deleted file mode 100644 index 93c635b0446..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/ShowStrategy.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.provectus.kafka.ui.strategy.ksql.statement; - -import com.fasterxml.jackson.databind.JsonNode; -import com.provectus.kafka.ui.model.KsqlCommandDTO; -import com.provectus.kafka.ui.model.KsqlCommandResponseDTO; -import java.util.List; -import java.util.Optional; -import org.springframework.stereotype.Component; - -@Component -public class ShowStrategy extends BaseStrategy { - private static final List SHOW_STATEMENTS = - List.of("functions", "topics", "streams", "tables", "queries", "properties"); - private static final List LIST_STATEMENTS = - List.of("functions", "topics", "streams", "tables"); - private String responseValueKey = ""; - - @Override - public KsqlCommandResponseDTO serializeResponse(JsonNode response) { - return serializeTableResponse(response, responseValueKey); - } - - @Override - public boolean test(String sql) { - Optional statement = SHOW_STATEMENTS.stream() - .filter(s -> testSql(sql, getShowRegExp(s)) || testSql(sql, getListRegExp(s))) - .findFirst(); - if (statement.isPresent()) { - setResponseValueKey(statement.get()); - return true; - } - return false; - } - - @Override - protected String getTestRegExp() { - return ""; - } - - @Override - public BaseStrategy ksqlCommand(KsqlCommandDTO ksqlCommand) { - // return new instance to avoid conflicts for parallel requests - ShowStrategy clone = new ShowStrategy(); - clone.setResponseValueKey(responseValueKey); - clone.ksqlCommand = ksqlCommand; - return clone; - } - - protected String getShowRegExp(String key) { - return "show " + key + ";"; - } - - protected String getListRegExp(String key) { - if (LIST_STATEMENTS.contains(key)) { - return "list " + key + ";"; - } - return ""; - } - - private void setResponseValueKey(String path) { - responseValueKey = path; - } - - private boolean testSql(String sql, String pattern) { - return sql.trim().toLowerCase().matches(pattern); - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/TerminateStrategy.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/TerminateStrategy.java deleted file mode 100644 index b043b8c6c94..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/strategy/ksql/statement/TerminateStrategy.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.provectus.kafka.ui.strategy.ksql.statement; - -import com.fasterxml.jackson.databind.JsonNode; -import com.provectus.kafka.ui.model.KsqlCommandResponseDTO; -import org.springframework.stereotype.Component; - -@Component -public class TerminateStrategy extends BaseStrategy { - private static final String RESPONSE_VALUE_KEY = "commandStatus"; - - @Override - public KsqlCommandResponseDTO serializeResponse(JsonNode response) { - return serializeMessageResponse(response, RESPONSE_VALUE_KEY); - } - - @Override - protected String getTestRegExp() { - return "terminate (.*);"; - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ApplicationMetrics.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ApplicationMetrics.java new file mode 100644 index 00000000000..811e6491804 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ApplicationMetrics.java @@ -0,0 +1,86 @@ +package com.provectus.kafka.ui.util; + +import static lombok.AccessLevel.PRIVATE; + +import com.google.common.annotations.VisibleForTesting; +import com.provectus.kafka.ui.emitter.PolledRecords; +import com.provectus.kafka.ui.model.KafkaCluster; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import java.util.concurrent.atomic.AtomicInteger; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor(access = PRIVATE) +public class ApplicationMetrics { + + // kafka-ui specific metrics prefix. Added to make it easier to distinguish kui metrics from + // other metrics, exposed by spring boot (like http stats, jvm, etc.) + private static final String COMMON_PREFIX = "kui_"; + + private final String clusterName; + private final MeterRegistry registry; + + public static ApplicationMetrics forCluster(KafkaCluster cluster) { + return new ApplicationMetrics(cluster.getName(), Metrics.globalRegistry); + } + + @VisibleForTesting + public static ApplicationMetrics noop() { + return new ApplicationMetrics("noop", new SimpleMeterRegistry()); + } + + public void meterPolledRecords(String topic, PolledRecords polled, boolean throttled) { + pollTimer(topic).record(polled.elapsed()); + polledRecords(topic).increment(polled.count()); + polledBytes(topic).record(polled.bytes()); + if (throttled) { + pollThrottlingActivations().increment(); + } + } + + private Counter polledRecords(String topic) { + return Counter.builder(COMMON_PREFIX + "topic_records_polled") + .description("Number of records polled from topic") + .tag("cluster", clusterName) + .tag("topic", topic) + .register(registry); + } + + private DistributionSummary polledBytes(String topic) { + return DistributionSummary.builder(COMMON_PREFIX + "topic_polled_bytes") + .description("Bytes polled from kafka topic") + .tag("cluster", clusterName) + .tag("topic", topic) + .register(registry); + } + + private Timer pollTimer(String topic) { + return Timer.builder(COMMON_PREFIX + "topic_poll_time") + .description("Time spend in polling for topic") + .tag("cluster", clusterName) + .tag("topic", topic) + .register(registry); + } + + private Counter pollThrottlingActivations() { + return Counter.builder(COMMON_PREFIX + "poll_throttling_activations") + .description("Number of poll throttling activations") + .tag("cluster", clusterName) + .register(registry); + } + + public AtomicInteger activeConsumers() { + var count = new AtomicInteger(); + Gauge.builder(COMMON_PREFIX + "active_consumers", () -> count) + .description("Number of active consumers") + .tag("cluster", clusterName) + .register(registry); + return count; + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ApplicationRestarter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ApplicationRestarter.java new file mode 100644 index 00000000000..42c8136f929 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ApplicationRestarter.java @@ -0,0 +1,46 @@ +package com.provectus.kafka.ui.util; + +import com.provectus.kafka.ui.KafkaUiApplication; +import java.io.Closeable; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class ApplicationRestarter implements ApplicationListener { + + private String[] applicationArgs; + private ApplicationContext applicationContext; + + @Override + public void onApplicationEvent(ApplicationStartedEvent event) { + this.applicationArgs = event.getArgs(); + this.applicationContext = event.getApplicationContext(); + } + + public void requestRestart() { + log.info("Restarting application"); + Thread thread = new Thread(() -> { + closeApplicationContext(applicationContext); + KafkaUiApplication.startApplication(applicationArgs); + }); + thread.setName("restartedMain-" + System.currentTimeMillis()); + thread.setDaemon(false); + thread.start(); + } + + private void closeApplicationContext(ApplicationContext context) { + while (context instanceof Closeable) { + try { + ((Closeable) context).close(); + } catch (Exception e) { + log.warn("Error stopping application before restart", e); + throw new RuntimeException(e); + } + context = context.getParent(); + } + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ClusterUtil.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ClusterUtil.java deleted file mode 100644 index 2e77366de14..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ClusterUtil.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.provectus.kafka.ui.util; - -import com.provectus.kafka.ui.model.MessageFormatDTO; -import com.provectus.kafka.ui.model.ServerStatusDTO; -import com.provectus.kafka.ui.model.TopicMessageDTO; -import com.provectus.kafka.ui.serde.RecordSerDe; -import java.time.Instant; -import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.util.HashMap; -import java.util.Map; -import lombok.extern.slf4j.Slf4j; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.common.record.TimestampType; -import org.apache.kafka.common.utils.Bytes; - - -@Slf4j -public class ClusterUtil { - - private ClusterUtil() { - } - - private static final ZoneId UTC_ZONE_ID = ZoneId.of("UTC"); - - public static TopicMessageDTO mapToTopicMessage(ConsumerRecord consumerRecord, - RecordSerDe recordDeserializer) { - - Map headers = new HashMap<>(); - consumerRecord.headers().iterator() - .forEachRemaining(header -> - headers.put( - header.key(), - header.value() != null ? new String(header.value()) : null - ) - ); - - TopicMessageDTO topicMessage = new TopicMessageDTO(); - - OffsetDateTime timestamp = - OffsetDateTime.ofInstant(Instant.ofEpochMilli(consumerRecord.timestamp()), UTC_ZONE_ID); - TopicMessageDTO.TimestampTypeEnum timestampType = - mapToTimestampType(consumerRecord.timestampType()); - topicMessage.setPartition(consumerRecord.partition()); - topicMessage.setOffset(consumerRecord.offset()); - topicMessage.setTimestamp(timestamp); - topicMessage.setTimestampType(timestampType); - - topicMessage.setHeaders(headers); - var parsed = recordDeserializer.deserialize(consumerRecord); - topicMessage.setKey(parsed.getKey()); - topicMessage.setContent(parsed.getValue()); - topicMessage.setKeyFormat(parsed.getKeyFormat() != null - ? MessageFormatDTO.valueOf(parsed.getKeyFormat().name()) - : null); - topicMessage.setValueFormat(parsed.getValueFormat() != null - ? MessageFormatDTO.valueOf(parsed.getValueFormat().name()) - : null); - topicMessage.setKeySize(ConsumerRecordUtil.getKeySize(consumerRecord)); - topicMessage.setValueSize(ConsumerRecordUtil.getValueSize(consumerRecord)); - topicMessage.setKeySchemaId(parsed.getKeySchemaId()); - topicMessage.setValueSchemaId(parsed.getValueSchemaId()); - topicMessage.setHeadersSize(ConsumerRecordUtil.getHeadersSize(consumerRecord)); - - return topicMessage; - } - - private static TopicMessageDTO.TimestampTypeEnum mapToTimestampType(TimestampType timestampType) { - switch (timestampType) { - case CREATE_TIME: - return TopicMessageDTO.TimestampTypeEnum.CREATE_TIME; - case LOG_APPEND_TIME: - return TopicMessageDTO.TimestampTypeEnum.LOG_APPEND_TIME; - case NO_TIMESTAMP_TYPE: - return TopicMessageDTO.TimestampTypeEnum.NO_TIMESTAMP_TYPE; - default: - throw new IllegalArgumentException("Unknown timestampType: " + timestampType); - } - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ConsumerRecordUtil.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ConsumerRecordUtil.java deleted file mode 100644 index a6716599891..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ConsumerRecordUtil.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.provectus.kafka.ui.util; - -import java.util.Arrays; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.common.header.Header; -import org.apache.kafka.common.header.Headers; -import org.apache.kafka.common.utils.Bytes; - -public class ConsumerRecordUtil { - - private ConsumerRecordUtil() { - } - - public static Long getHeadersSize(ConsumerRecord consumerRecord) { - Headers headers = consumerRecord.headers(); - if (headers != null) { - return Arrays.stream(consumerRecord.headers().toArray()) - .mapToLong(ConsumerRecordUtil::headerSize) - .sum(); - } - return 0L; - } - - public static Long getKeySize(ConsumerRecord consumerRecord) { - return consumerRecord.key() != null ? (long) consumerRecord.key().get().length : null; - } - - public static Long getValueSize(ConsumerRecord consumerRecord) { - return consumerRecord.value() != null ? (long) consumerRecord.value().get().length : null; - } - - private static int headerSize(Header header) { - int key = header.key() != null ? header.key().getBytes().length : 0; - int val = header.value() != null ? header.value().length : 0; - return key + val; - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/DynamicConfigOperations.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/DynamicConfigOperations.java new file mode 100644 index 00000000000..2a5532ca76a --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/DynamicConfigOperations.java @@ -0,0 +1,253 @@ +package com.provectus.kafka.ui.util; + + +import com.provectus.kafka.ui.config.ClustersProperties; +import com.provectus.kafka.ui.config.WebclientProperties; +import com.provectus.kafka.ui.config.auth.OAuthProperties; +import com.provectus.kafka.ui.config.auth.RoleBasedAccessControlProperties; +import com.provectus.kafka.ui.exception.FileUploadException; +import com.provectus.kafka.ui.exception.ValidationException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.Instant; +import java.util.Optional; +import javax.annotation.Nullable; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.env.YamlPropertySourceLoader; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.codec.multipart.FilePart; +import org.springframework.stereotype.Component; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.introspector.BeanAccess; +import org.yaml.snakeyaml.introspector.Property; +import org.yaml.snakeyaml.introspector.PropertyUtils; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.representer.Representer; +import reactor.core.publisher.Mono; + +@Slf4j +@RequiredArgsConstructor +@Component +public class DynamicConfigOperations { + + static final String DYNAMIC_CONFIG_ENABLED_ENV_PROPERTY = "dynamic.config.enabled"; + static final String FILTERING_GROOVY_ENABLED_PROPERTY = "filtering.groovy.enabled"; + static final String DYNAMIC_CONFIG_PATH_ENV_PROPERTY = "dynamic.config.path"; + static final String DYNAMIC_CONFIG_PATH_ENV_PROPERTY_DEFAULT = "/etc/kafkaui/dynamic_config.yaml"; + + static final String CONFIG_RELATED_UPLOADS_DIR_PROPERTY = "config.related.uploads.dir"; + static final String CONFIG_RELATED_UPLOADS_DIR_DEFAULT = "/etc/kafkaui/uploads"; + + public static ApplicationContextInitializer dynamicConfigPropertiesInitializer() { + return appCtx -> + new DynamicConfigOperations(appCtx) + .loadDynamicPropertySource() + .ifPresent(source -> appCtx.getEnvironment().getPropertySources().addFirst(source)); + } + + private final ConfigurableApplicationContext ctx; + + public boolean dynamicConfigEnabled() { + return "true".equalsIgnoreCase(ctx.getEnvironment().getProperty(DYNAMIC_CONFIG_ENABLED_ENV_PROPERTY)); + } + + public boolean filteringGroovyEnabled() { + return "true".equalsIgnoreCase(ctx.getEnvironment().getProperty(FILTERING_GROOVY_ENABLED_PROPERTY)); + } + + private Path dynamicConfigFilePath() { + return Paths.get( + Optional.ofNullable(ctx.getEnvironment().getProperty(DYNAMIC_CONFIG_PATH_ENV_PROPERTY)) + .orElse(DYNAMIC_CONFIG_PATH_ENV_PROPERTY_DEFAULT) + ); + } + + @SneakyThrows + public Optional> loadDynamicPropertySource() { + if (dynamicConfigEnabled()) { + Path configPath = dynamicConfigFilePath(); + if (!Files.exists(configPath) || !Files.isReadable(configPath)) { + log.warn("Dynamic config file {} doesnt exist or not readable", configPath); + return Optional.empty(); + } + var propertySource = new CompositePropertySource("dynamicProperties"); + new YamlPropertySourceLoader() + .load("dynamicProperties", new FileSystemResource(configPath)) + .forEach(propertySource::addPropertySource); + log.info("Dynamic config loaded from {}", configPath); + return Optional.of(propertySource); + } + return Optional.empty(); + } + + public PropertiesStructure getCurrentProperties() { + checkIfDynamicConfigEnabled(); + return PropertiesStructure.builder() + .kafka(getNullableBean(ClustersProperties.class)) + .rbac(getNullableBean(RoleBasedAccessControlProperties.class)) + .auth( + PropertiesStructure.Auth.builder() + .type(ctx.getEnvironment().getProperty("auth.type")) + .oauth2(getNullableBean(OAuthProperties.class)) + .build()) + .webclient(getNullableBean(WebclientProperties.class)) + .build(); + } + + @Nullable + private T getNullableBean(Class clazz) { + try { + return ctx.getBean(clazz); + } catch (NoSuchBeanDefinitionException nsbde) { + return null; + } + } + + public void persist(PropertiesStructure properties) { + checkIfDynamicConfigEnabled(); + properties.initAndValidate(); + + String yaml = serializeToYaml(properties); + writeYamlToFile(yaml, dynamicConfigFilePath()); + } + + public Mono uploadConfigRelatedFile(FilePart file) { + checkIfDynamicConfigEnabled(); + String targetDirStr = ctx.getEnvironment() + .getProperty(CONFIG_RELATED_UPLOADS_DIR_PROPERTY, CONFIG_RELATED_UPLOADS_DIR_DEFAULT); + + Path targetDir = Path.of(targetDirStr); + if (!Files.exists(targetDir)) { + try { + Files.createDirectories(targetDir); + } catch (IOException e) { + return Mono.error( + new FileUploadException("Error creating directory for uploads %s".formatted(targetDir), e)); + } + } + + Path targetFilePath = targetDir.resolve(file.filename() + "-" + Instant.now().getEpochSecond()); + log.info("Uploading config-related file {}", targetFilePath); + if (Files.exists(targetFilePath)) { + log.info("File {} already exists, it will be overwritten", targetFilePath); + } + + return file.transferTo(targetFilePath) + .thenReturn(targetFilePath) + .doOnError(th -> log.error("Error uploading file {}", targetFilePath, th)) + .onErrorMap(th -> new FileUploadException(targetFilePath, th)); + } + + public void checkIfFilteringGroovyEnabled() { + if (!filteringGroovyEnabled()) { + throw new ValidationException( + "Groovy filters is not allowed. " + + "Set filtering.groovy.enabled property to 'true' to enabled it."); + } + } + + private void checkIfDynamicConfigEnabled() { + if (!dynamicConfigEnabled()) { + throw new ValidationException( + "Dynamic config change is not allowed. " + + "Set dynamic.config.enabled property to 'true' to enabled it."); + } + } + + @SneakyThrows + private void writeYamlToFile(String yaml, Path path) { + if (Files.isDirectory(path)) { + throw new ValidationException("Dynamic file path is a directory, but should be a file path"); + } + if (!Files.exists(path.getParent())) { + Files.createDirectories(path.getParent()); + } + if (Files.exists(path) && !Files.isWritable(path)) { + throw new ValidationException("File already exists and is not writable"); + } + try { + Files.writeString( + path, + yaml, + StandardOpenOption.CREATE, + StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING // to override existing file + ); + } catch (IOException e) { + throw new ValidationException("Error writing to " + path, e); + } + } + + private String serializeToYaml(PropertiesStructure props) { + //representer, that skips fields with null values + Representer representer = new Representer(new DumperOptions()) { + @Override + protected NodeTuple representJavaBeanProperty(Object javaBean, + Property property, + Object propertyValue, + Tag customTag) { + if (propertyValue == null) { + return null; // if value of property is null, ignore it. + } else { + return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag); + } + } + }; + var propertyUtils = new PropertyUtils(); + propertyUtils.setBeanAccess(BeanAccess.FIELD); + representer.setPropertyUtils(propertyUtils); + representer.addClassTag(PropertiesStructure.class, Tag.MAP); //to avoid adding class tag + representer.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); //use indent instead of {} + return new Yaml(representer).dump(props); + } + + ///--------------------------------------------------------------------- + + @Data + @Builder + // field name should be in sync with @ConfigurationProperties annotation + public static class PropertiesStructure { + + private ClustersProperties kafka; + private RoleBasedAccessControlProperties rbac; + private Auth auth; + private WebclientProperties webclient; + + @Data + @Builder + public static class Auth { + String type; + OAuthProperties oauth2; + } + + public void initAndValidate() { + Optional.ofNullable(kafka) + .ifPresent(ClustersProperties::validateAndSetDefaults); + + Optional.ofNullable(rbac) + .ifPresent(RoleBasedAccessControlProperties::init); + + Optional.ofNullable(auth) + .flatMap(a -> Optional.ofNullable(a.oauth2)) + .ifPresent(OAuthProperties::init); + + Optional.ofNullable(webclient) + .ifPresent(WebclientProperties::validate); + } + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/GithubReleaseInfo.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/GithubReleaseInfo.java new file mode 100644 index 00000000000..26afaa82a2e --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/GithubReleaseInfo.java @@ -0,0 +1,52 @@ +package com.provectus.kafka.ui.util; + +import com.google.common.annotations.VisibleForTesting; +import java.time.Duration; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Mono; + +@Slf4j +public class GithubReleaseInfo { + + private static final String GITHUB_LATEST_RELEASE_RETRIEVAL_URL = + "https://api.github.com/repos/provectus/kafka-ui/releases/latest"; + + private static final Duration GITHUB_API_MAX_WAIT_TIME = Duration.ofSeconds(2); + + public record GithubReleaseDto(String html_url, String tag_name, String published_at) { + + static GithubReleaseDto empty() { + return new GithubReleaseDto(null, null, null); + } + } + + private volatile GithubReleaseDto release = GithubReleaseDto.empty(); + + private final Mono refreshMono; + + public GithubReleaseInfo() { + this(GITHUB_LATEST_RELEASE_RETRIEVAL_URL); + } + + @VisibleForTesting + GithubReleaseInfo(String url) { + this.refreshMono = new WebClientConfigurator().build() + .get() + .uri(url) + .exchangeToMono(resp -> resp.bodyToMono(GithubReleaseDto.class)) + .timeout(GITHUB_API_MAX_WAIT_TIME) + .doOnError(th -> log.trace("Error getting latest github release info", th)) + .onErrorResume(th -> true, th -> Mono.just(GithubReleaseDto.empty())) + .doOnNext(release -> this.release = release) + .then(); + } + + public GithubReleaseDto get() { + return release; + } + + public Mono refresh() { + return refreshMono; + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/JmxClusterUtil.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/JmxClusterUtil.java deleted file mode 100644 index 163ebb5e9ed..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/JmxClusterUtil.java +++ /dev/null @@ -1,217 +0,0 @@ -package com.provectus.kafka.ui.util; - -import static java.util.stream.Collectors.groupingBy; -import static java.util.stream.Collectors.reducing; -import static java.util.stream.Collectors.toList; - -import com.provectus.kafka.ui.model.JmxBrokerMetrics; -import com.provectus.kafka.ui.model.JmxConnectionInfo; -import com.provectus.kafka.ui.model.KafkaCluster; -import com.provectus.kafka.ui.model.MetricDTO; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.management.MBeanAttributeInfo; -import javax.management.MBeanServerConnection; -import javax.management.ObjectName; -import javax.management.remote.JMXConnector; -import lombok.Builder; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import lombok.Value; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.pool2.KeyedObjectPool; -import org.apache.kafka.common.Node; -import org.jetbrains.annotations.Nullable; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -@Component -@Slf4j -@RequiredArgsConstructor -public class JmxClusterUtil { - - private static final String JMX_URL = "service:jmx:rmi:///jndi/rmi://"; - private static final String JMX_SERVICE_TYPE = "jmxrmi"; - private static final String KAFKA_SERVER_PARAM = "kafka.server"; - private static final String NAME_METRIC_FIELD = "name"; - private final KeyedObjectPool pool; - - @Builder - @Value - public static class JmxMetrics { - Map bytesInPerSec; - Map bytesOutPerSec; - Map internalBrokerMetrics; - List metrics; - - public static JmxMetrics empty() { - return JmxClusterUtil.JmxMetrics.builder() - .bytesInPerSec(Map.of()) - .bytesOutPerSec(Map.of()) - .internalBrokerMetrics(Map.of()) - .metrics(List.of()) - .build(); - } - } - - public Mono getBrokerMetrics(KafkaCluster cluster, Collection nodes) { - return Flux.fromIterable(nodes) - // jmx is a blocking api, so we trying to parallelize its execution on boundedElastic scheduler - .parallel() - .runOn(Schedulers.boundedElastic()) - .map(n -> Map.entry(n.id(), - JmxBrokerMetrics.builder().metrics(getJmxMetric(cluster, n)).build())) - .sequential() - .collectMap(Map.Entry::getKey, Map.Entry::getValue) - .map(this::collectMetrics); - } - - private List getJmxMetric(KafkaCluster cluster, Node node) { - return Optional.of(cluster) - .filter(c -> c.getJmxPort() != null) - .filter(c -> c.getJmxPort() > 0) - .map(c -> getJmxMetrics(node.host(), c.getJmxPort(), c.isJmxSsl(), - c.getJmxUsername(), c.getJmxPassword())) - .orElse(Collections.emptyList()); - } - - @SneakyThrows - private List getJmxMetrics(String host, int port, boolean jmxSsl, - @Nullable String username, @Nullable String password) { - String jmxUrl = JMX_URL + host + ":" + port + "/" + JMX_SERVICE_TYPE; - final var connectionInfo = JmxConnectionInfo.builder() - .url(jmxUrl) - .ssl(jmxSsl) - .username(username) - .password(password) - .build(); - JMXConnector srv; - try { - srv = pool.borrowObject(connectionInfo); - } catch (Exception e) { - log.error("Cannot get JMX connector for the pool due to: ", e); - return Collections.emptyList(); - } - - List result = new ArrayList<>(); - try { - MBeanServerConnection msc = srv.getMBeanServerConnection(); - var jmxMetrics = msc.queryNames(null, null).stream() - .filter(q -> q.getCanonicalName().startsWith(KAFKA_SERVER_PARAM)) - .collect(Collectors.toList()); - for (ObjectName jmxMetric : jmxMetrics) { - final Hashtable params = jmxMetric.getKeyPropertyList(); - MetricDTO metric = new MetricDTO(); - metric.setName(params.get(NAME_METRIC_FIELD)); - metric.setCanonicalName(jmxMetric.getCanonicalName()); - metric.setParams(params); - metric.setValue(getJmxMetrics(jmxMetric.getCanonicalName(), msc)); - result.add(metric); - } - pool.returnObject(connectionInfo, srv); - } catch (Exception e) { - log.error("Cannot get jmxMetricsNames, {}", jmxUrl, e); - closeConnectionExceptionally(jmxUrl, srv); - } - return result; - } - - @SneakyThrows - private Map getJmxMetrics(String canonicalName, MBeanServerConnection msc) { - Map resultAttr = new HashMap<>(); - ObjectName name = new ObjectName(canonicalName); - var attrNames = msc.getMBeanInfo(name).getAttributes(); - for (MBeanAttributeInfo attrName : attrNames) { - var value = msc.getAttribute(name, attrName.getName()); - if (NumberUtil.isNumeric(value)) { - resultAttr.put(attrName.getName(), new BigDecimal(value.toString())); - } - } - return resultAttr; - } - - private JmxMetrics collectMetrics(Map perBrokerJmxMetrics) { - final List metrics = perBrokerJmxMetrics.values() - .stream() - .flatMap(b -> b.getMetrics().stream()) - .collect( - groupingBy( - MetricDTO::getCanonicalName, - reducing(this::reduceJmxMetrics) - ) - ).values().stream() - .filter(Optional::isPresent) - .map(Optional::get) - .collect(toList()); - return JmxMetrics.builder() - .metrics(metrics) - .internalBrokerMetrics(perBrokerJmxMetrics) - .bytesInPerSec(findTopicMetrics( - metrics, JmxMetricsName.BYTES_IN_PER_SEC, JmxMetricsValueName.FIFTEEN_MINUTE_RATE)) - .bytesOutPerSec(findTopicMetrics( - metrics, JmxMetricsName.BYTES_OUT_PER_SEC, JmxMetricsValueName.FIFTEEN_MINUTE_RATE)) - .build(); - } - - private Map findTopicMetrics(List metrics, - JmxMetricsName metricsName, - JmxMetricsValueName valueName) { - return metrics.stream().filter(m -> metricsName.getValue().equals(m.getName())) - .filter(m -> m.getParams().containsKey("topic")) - .filter(m -> m.getValue().containsKey(valueName.getValue())) - .map(m -> Tuples.of( - m.getParams().get("topic"), - m.getValue().get(valueName.getValue()) - )).collect(groupingBy( - Tuple2::getT1, - reducing(BigDecimal.ZERO, Tuple2::getT2, BigDecimal::add) - )); - } - - private void closeConnectionExceptionally(String url, JMXConnector srv) { - try { - pool.invalidateObject(new JmxConnectionInfo(url), srv); - } catch (Exception e) { - log.error("Cannot invalidate object in pool, {}", url); - } - } - - public MetricDTO reduceJmxMetrics(MetricDTO metric1, MetricDTO metric2) { - var result = new MetricDTO(); - Map value = Stream.concat( - metric1.getValue().entrySet().stream(), - metric2.getValue().entrySet().stream() - ).collect(Collectors.groupingBy( - Map.Entry::getKey, - Collectors.reducing(BigDecimal.ZERO, Map.Entry::getValue, BigDecimal::add) - )); - result.setName(metric1.getName()); - result.setCanonicalName(metric1.getCanonicalName()); - result.setParams(metric1.getParams()); - result.setValue(value); - return result; - } - - private boolean isWellKnownMetric(MetricDTO metric) { - final Optional param = - Optional.ofNullable(metric.getParams().get(NAME_METRIC_FIELD)).filter(p -> - Arrays.stream(JmxMetricsName.values()).map(JmxMetricsName::getValue) - .anyMatch(n -> n.equals(p)) - ); - return metric.getCanonicalName().contains(KAFKA_SERVER_PARAM) && param.isPresent(); - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/JmxMetricsName.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/JmxMetricsName.java deleted file mode 100644 index 2384fc85367..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/JmxMetricsName.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.provectus.kafka.ui.util; - -public enum JmxMetricsName { - MESSAGES_IN_PER_SEC("MessagesInPerSec"), - BYTES_IN_PER_SEC("BytesInPerSec"), - REPLICATION_BYTES_IN_PER_SEC("ReplicationBytesInPerSec"), - REQUESTS_PER_SEC("RequestsPerSec"), - ERRORS_PER_SEC("ErrorsPerSec"), - MESSAGE_CONVERSIONS_PER_SEC("MessageConversionsPerSec"), - BYTES_OUT_PER_SEC("BytesOutPerSec"), - REPLICATION_BYTES_OUT_PER_SEC("ReplicationBytesOutPerSec"), - NO_KEY_COMPACTED_TOPIC_RECORDS_PER_SEC("NoKeyCompactedTopicRecordsPerSec"), - INVALID_MAGIC_NUMBER_RECORDS_PER_SEC("InvalidMagicNumberRecordsPerSec"), - INVALID_MESSAGE_CRC_RECORDS_PER_SEC("InvalidMessageCrcRecordsPerSec"), - INVALID_OFFSET_OR_SEQUENCE_RECORDS_PER_SEC("InvalidOffsetOrSequenceRecordsPerSec"), - UNCLEAN_LEADER_ELECTIONS_PER_SEC("UncleanLeaderElectionsPerSec"), - ISR_SHRINKS_PER_SEC("IsrShrinksPerSec"), - ISR_EXPANDS_PER_SEC("IsrExpandsPerSec"), - REASSIGNMENT_BYTES_OUT_PER_SEC("ReassignmentBytesOutPerSec"), - REASSIGNMENT_BYTES_IN_PER_SEC("ReassignmentBytesInPerSec"), - PRODUCE_MESSAGE_CONVERSIONS_PER_SEC("ProduceMessageConversionsPerSec"), - FAILED_FETCH_REQUESTS_PER_SEC("FailedFetchRequestsPerSec"), - ZOOKEEPER_SYNC_CONNECTS_PER_SEC("ZooKeeperSyncConnectsPerSec"), - BYTES_REJECTED_PER_SEC("BytesRejectedPerSec"), - ZOO_KEEPER_AUTH_FAILURES_PER_SEC("ZooKeeperAuthFailuresPerSec"), - TOTAL_FETCH_REQUESTS_PER_SEC("TotalFetchRequestsPerSec"), - FAILED_ISR_UPDATES_PER_SEC("FailedIsrUpdatesPerSec"), - INCREMENTAL_FETCH_SESSION_EVICTIONS_PER_SEC("IncrementalFetchSessionEvictionsPerSec"), - FETCH_MESSAGE_CONVERSIONS_PER_SEC("FetchMessageConversionsPerSec"), - FAILED_PRODUCE_REQUESTS_PER_SEC("FailedProduceRequestsPerSe"); - - private final String value; - - JmxMetricsName(String value) { - this.value = value; - } - - public String getValue() { - return value; - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/JmxMetricsValueName.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/JmxMetricsValueName.java deleted file mode 100644 index cbcc6cee078..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/JmxMetricsValueName.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.provectus.kafka.ui.util; - -public enum JmxMetricsValueName { - COUNT("Count"), - ONE_MINUTE_RATE("OneMinuteRate"), - FIFTEEN_MINUTE_RATE("FifteenMinuteRate"), - FIVE_MINUTE_RATE("FiveMinuteRate"), - MEAN_RATE("MeanRate"); - - private final String value; - - JmxMetricsValueName(String value) { - this.value = value; - } - - public String getValue() { - return value; - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/JmxPoolFactory.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/JmxPoolFactory.java deleted file mode 100644 index 49e73a58f28..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/JmxPoolFactory.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.provectus.kafka.ui.util; - -import com.provectus.kafka.ui.model.JmxConnectionInfo; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import javax.management.remote.JMXConnector; -import javax.management.remote.JMXConnectorFactory; -import javax.management.remote.JMXServiceURL; -import javax.rmi.ssl.SslRMIClientSocketFactory; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.pool2.BaseKeyedPooledObjectFactory; -import org.apache.commons.pool2.PooledObject; -import org.apache.commons.pool2.impl.DefaultPooledObject; - -@Slf4j -public class JmxPoolFactory extends BaseKeyedPooledObjectFactory { - - @Override - public JMXConnector create(JmxConnectionInfo info) throws Exception { - Map env = new HashMap<>(); - if (StringUtils.isNotEmpty(info.getUsername()) && StringUtils.isNotEmpty(info.getPassword())) { - env.put("jmx.remote.credentials", new String[] {info.getUsername(), info.getPassword()}); - } - - if (info.isSsl()) { - env.put("com.sun.jndi.rmi.factory.socket", new SslRMIClientSocketFactory()); - } - - return JMXConnectorFactory.connect(new JMXServiceURL(info.getUrl()), env); - } - - @Override - public PooledObject wrap(JMXConnector jmxConnector) { - return new DefaultPooledObject<>(jmxConnector); - } - - @Override - public void destroyObject(JmxConnectionInfo key, PooledObject p) { - try { - p.getObject().close(); - } catch (IOException e) { - log.error("Cannot close connection with {}", key); - } - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/KafkaConstants.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/KafkaConstants.java deleted file mode 100644 index aa482c57b59..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/KafkaConstants.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.provectus.kafka.ui.util; - -import static org.apache.kafka.common.config.TopicConfig.CLEANUP_POLICY_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.CLEANUP_POLICY_DELETE; -import static org.apache.kafka.common.config.TopicConfig.COMPRESSION_TYPE_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.DELETE_RETENTION_MS_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.FILE_DELETE_DELAY_MS_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.FLUSH_MESSAGES_INTERVAL_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.FLUSH_MS_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.INDEX_INTERVAL_BYTES_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.MAX_COMPACTION_LAG_MS_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.MAX_MESSAGE_BYTES_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.MESSAGE_DOWNCONVERSION_ENABLE_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.MESSAGE_TIMESTAMP_DIFFERENCE_MAX_MS_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.MESSAGE_TIMESTAMP_TYPE_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.MIN_CLEANABLE_DIRTY_RATIO_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.MIN_COMPACTION_LAG_MS_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.MIN_IN_SYNC_REPLICAS_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.PREALLOCATE_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.RETENTION_BYTES_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.RETENTION_MS_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.SEGMENT_BYTES_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.SEGMENT_INDEX_BYTES_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.SEGMENT_JITTER_MS_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.SEGMENT_MS_CONFIG; -import static org.apache.kafka.common.config.TopicConfig.UNCLEAN_LEADER_ELECTION_ENABLE_CONFIG; - -import java.util.AbstractMap; -import java.util.Map; - -public final class KafkaConstants { - - private static final String LONG_MAX_STRING = Long.toString(Long.MAX_VALUE); - - public static final Map TOPIC_DEFAULT_CONFIGS = Map.ofEntries( - new AbstractMap.SimpleEntry<>(CLEANUP_POLICY_CONFIG, CLEANUP_POLICY_DELETE), - new AbstractMap.SimpleEntry<>(COMPRESSION_TYPE_CONFIG, "producer"), - new AbstractMap.SimpleEntry<>(DELETE_RETENTION_MS_CONFIG, "86400000"), - new AbstractMap.SimpleEntry<>(FILE_DELETE_DELAY_MS_CONFIG, "60000"), - new AbstractMap.SimpleEntry<>(FLUSH_MESSAGES_INTERVAL_CONFIG, LONG_MAX_STRING), - new AbstractMap.SimpleEntry<>(FLUSH_MS_CONFIG, LONG_MAX_STRING), - new AbstractMap.SimpleEntry<>("follower.replication.throttled.replicas", ""), - new AbstractMap.SimpleEntry<>(INDEX_INTERVAL_BYTES_CONFIG, "4096"), - new AbstractMap.SimpleEntry<>("leader.replication.throttled.replicas", ""), - new AbstractMap.SimpleEntry<>(MAX_COMPACTION_LAG_MS_CONFIG, LONG_MAX_STRING), - new AbstractMap.SimpleEntry<>(MAX_MESSAGE_BYTES_CONFIG, "1000012"), - new AbstractMap.SimpleEntry<>(MESSAGE_TIMESTAMP_DIFFERENCE_MAX_MS_CONFIG, LONG_MAX_STRING), - new AbstractMap.SimpleEntry<>(MESSAGE_TIMESTAMP_TYPE_CONFIG, "CreateTime"), - new AbstractMap.SimpleEntry<>(MIN_CLEANABLE_DIRTY_RATIO_CONFIG, "0.5"), - new AbstractMap.SimpleEntry<>(MIN_COMPACTION_LAG_MS_CONFIG, "0"), - new AbstractMap.SimpleEntry<>(MIN_IN_SYNC_REPLICAS_CONFIG, "1"), - new AbstractMap.SimpleEntry<>(PREALLOCATE_CONFIG, "false"), - new AbstractMap.SimpleEntry<>(RETENTION_BYTES_CONFIG, "-1"), - new AbstractMap.SimpleEntry<>(RETENTION_MS_CONFIG, "604800000"), - new AbstractMap.SimpleEntry<>(SEGMENT_BYTES_CONFIG, "1073741824"), - new AbstractMap.SimpleEntry<>(SEGMENT_INDEX_BYTES_CONFIG, "10485760"), - new AbstractMap.SimpleEntry<>(SEGMENT_JITTER_MS_CONFIG, "0"), - new AbstractMap.SimpleEntry<>(SEGMENT_MS_CONFIG, "604800000"), - new AbstractMap.SimpleEntry<>(UNCLEAN_LEADER_ELECTION_ENABLE_CONFIG, "false"), - new AbstractMap.SimpleEntry<>(MESSAGE_DOWNCONVERSION_ENABLE_CONFIG, "true") - ); - - private KafkaConstants() { - } -} \ No newline at end of file diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/KafkaServicesValidation.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/KafkaServicesValidation.java new file mode 100644 index 00000000000..4b8af81f851 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/KafkaServicesValidation.java @@ -0,0 +1,145 @@ +package com.provectus.kafka.ui.util; + +import static com.provectus.kafka.ui.config.ClustersProperties.TruststoreConfig; + +import com.provectus.kafka.ui.connect.api.KafkaConnectClientApi; +import com.provectus.kafka.ui.model.ApplicationPropertyValidationDTO; +import com.provectus.kafka.ui.service.ReactiveAdminClient; +import com.provectus.kafka.ui.service.ksql.KsqlApiClient; +import com.provectus.kafka.ui.sr.api.KafkaSrClientApi; +import java.io.FileInputStream; +import java.security.KeyStore; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.function.Supplier; +import javax.annotation.Nullable; +import javax.net.ssl.TrustManagerFactory; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.clients.admin.AdminClientConfig; +import org.springframework.util.ResourceUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Slf4j +public final class KafkaServicesValidation { + + private KafkaServicesValidation() { + } + + private static Mono valid() { + return Mono.just(new ApplicationPropertyValidationDTO().error(false)); + } + + private static Mono invalid(String errorMsg) { + return Mono.just(new ApplicationPropertyValidationDTO().error(true).errorMessage(errorMsg)); + } + + private static Mono invalid(Throwable th) { + return Mono.just(new ApplicationPropertyValidationDTO().error(true).errorMessage(th.getMessage())); + } + + /** + * Returns error msg, if any. + */ + public static Optional validateTruststore(TruststoreConfig truststoreConfig) { + if (truststoreConfig.getTruststoreLocation() != null && truststoreConfig.getTruststorePassword() != null) { + try (FileInputStream fileInputStream = new FileInputStream( + (ResourceUtils.getFile(truststoreConfig.getTruststoreLocation())))) { + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(fileInputStream, truststoreConfig.getTruststorePassword().toCharArray()); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm() + ); + trustManagerFactory.init(trustStore); + } catch (Exception e) { + return Optional.of(e.getMessage()); + } + } + return Optional.empty(); + } + + public static Mono validateClusterConnection(String bootstrapServers, + Properties clusterProps, + @Nullable + TruststoreConfig ssl) { + Properties properties = new Properties(); + SslPropertiesUtil.addKafkaSslProperties(ssl, properties); + properties.putAll(clusterProps); + properties.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + // editing properties to make validation faster + properties.put(AdminClientConfig.RETRIES_CONFIG, 1); + properties.put(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, 5_000); + properties.put(AdminClientConfig.DEFAULT_API_TIMEOUT_MS_CONFIG, 5_000); + properties.put(AdminClientConfig.CLIENT_ID_CONFIG, "kui-admin-client-validation-" + System.currentTimeMillis()); + AdminClient adminClient = null; + try { + adminClient = AdminClient.create(properties); + } catch (Exception e) { + log.error("Error creating admin client during validation", e); + return invalid("Error while creating AdminClient. See logs for details."); + } + return Mono.just(adminClient) + .then(ReactiveAdminClient.toMono(adminClient.listTopics().names())) + .then(valid()) + .doOnTerminate(adminClient::close) + .onErrorResume(th -> { + log.error("Error connecting to cluster", th); + return KafkaServicesValidation.invalid("Error connecting to cluster. See logs for details."); + }); + } + + public static Mono validateSchemaRegistry( + Supplier> clientSupplier) { + ReactiveFailover client; + try { + client = clientSupplier.get(); + } catch (Exception e) { + log.error("Error creating Schema Registry client", e); + return invalid("Error creating Schema Registry client: " + e.getMessage()); + } + return client + .mono(KafkaSrClientApi::getGlobalCompatibilityLevel) + .then(valid()) + .onErrorResume(KafkaServicesValidation::invalid); + } + + public static Mono validateConnect( + Supplier> clientSupplier) { + ReactiveFailover client; + try { + client = clientSupplier.get(); + } catch (Exception e) { + log.error("Error creating Connect client", e); + return invalid("Error creating Connect client: " + e.getMessage()); + } + return client.flux(KafkaConnectClientApi::getConnectorPlugins) + .collectList() + .then(valid()) + .onErrorResume(KafkaServicesValidation::invalid); + } + + public static Mono validateKsql( + Supplier> clientSupplier) { + ReactiveFailover client; + try { + client = clientSupplier.get(); + } catch (Exception e) { + log.error("Error creating Ksql client", e); + return invalid("Error creating Ksql client: " + e.getMessage()); + } + return client.flux(c -> c.execute("SHOW VARIABLES;", Map.of())) + .collectList() + .flatMap(ksqlResults -> + Flux.fromIterable(ksqlResults) + .filter(KsqlApiClient.KsqlResponseTable::isError) + .flatMap(err -> invalid("Error response from ksql: " + err)) + .next() + .switchIfEmpty(valid()) + ) + .onErrorResume(KafkaServicesValidation::invalid); + } + + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/KafkaVersion.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/KafkaVersion.java new file mode 100644 index 00000000000..3d6b2ca40ee --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/KafkaVersion.java @@ -0,0 +1,21 @@ +package com.provectus.kafka.ui.util; + +import java.util.Optional; + +public final class KafkaVersion { + + private KafkaVersion() { + } + + public static Optional parse(String version) throws NumberFormatException { + try { + final String[] parts = version.split("\\."); + if (parts.length > 2) { + version = parts[0] + "." + parts[1]; + } + return Optional.of(Float.parseFloat(version.split("-")[0])); + } catch (Exception e) { + return Optional.empty(); + } + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/MapUtil.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/MapUtil.java deleted file mode 100644 index d1a5c035ee6..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/MapUtil.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.provectus.kafka.ui.util; - -import java.util.Map; -import java.util.stream.Collectors; - -public class MapUtil { - - private MapUtil() { - } - - public static Map removeNullValues(Map map) { - return map.entrySet().stream() - .filter(e -> e.getValue() != null) - .collect( - Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue - ) - ); - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/NumberUtil.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/NumberUtil.java deleted file mode 100644 index cb1f08b3ab1..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/NumberUtil.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.provectus.kafka.ui.util; - -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.math.NumberUtils; - -@Slf4j -public class NumberUtil { - - private NumberUtil() { - } - - public static boolean isNumeric(Object value) { - return value != null && NumberUtils.isCreatable(value.toString()); - } - - public static float parserClusterVersion(String version) { - try { - final String[] parts = version.split("\\."); - if (parts.length > 2) { - version = parts[0] + "." + parts[1]; - } - return Float.parseFloat(version.split("-")[0]); - } catch (Exception e) { - log.error("Conversion clusterVersion {} to float value failed", version); - throw e; - } - } -} \ No newline at end of file diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/OffsetsSeek.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/OffsetsSeek.java deleted file mode 100644 index 0fd830b323d..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/OffsetsSeek.java +++ /dev/null @@ -1,145 +0,0 @@ -package com.provectus.kafka.ui.util; - -import com.provectus.kafka.ui.model.ConsumerPosition; -import com.provectus.kafka.ui.model.SeekTypeDTO; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import lombok.extern.slf4j.Slf4j; -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.common.TopicPartition; -import org.apache.kafka.common.utils.Bytes; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -@Slf4j -public abstract class OffsetsSeek { - protected final String topic; - protected final ConsumerPosition consumerPosition; - - protected OffsetsSeek(String topic, ConsumerPosition consumerPosition) { - this.topic = topic; - this.consumerPosition = consumerPosition; - } - - public ConsumerPosition getConsumerPosition() { - return consumerPosition; - } - - public Map getPartitionsOffsets(Consumer consumer) { - SeekTypeDTO seekType = consumerPosition.getSeekType(); - List partitions = getRequestedPartitions(consumer); - log.info("Positioning consumer for topic {} with {}", topic, consumerPosition); - Map offsets; - switch (seekType) { - case OFFSET: - offsets = offsetsFromPositions(consumer, partitions); - break; - case TIMESTAMP: - offsets = offsetsForTimestamp(consumer); - break; - case BEGINNING: - offsets = offsetsFromBeginning(consumer, partitions); - break; - case LATEST: - offsets = endOffsets(consumer, partitions); - break; - default: - throw new IllegalArgumentException("Unknown seekType: " + seekType); - } - return offsets; - } - - public WaitingOffsets waitingOffsets(Consumer consumer, - Collection partitions) { - return new WaitingOffsets(topic, consumer, partitions); - } - - public WaitingOffsets assignAndSeek(Consumer consumer) { - final Map partitionsOffsets = getPartitionsOffsets(consumer); - consumer.assign(partitionsOffsets.keySet()); - partitionsOffsets.forEach(consumer::seek); - log.info("Assignment: {}", consumer.assignment()); - return waitingOffsets(consumer, partitionsOffsets.keySet()); - } - - - public List getRequestedPartitions(Consumer consumer) { - Map partitionPositions = consumerPosition.getSeekTo(); - return consumer.partitionsFor(topic).stream() - .filter( - p -> partitionPositions.isEmpty() - || partitionPositions.containsKey(new TopicPartition(p.topic(), p.partition())) - ).map(p -> new TopicPartition(p.topic(), p.partition())) - .collect(Collectors.toList()); - } - - protected Map endOffsets( - Consumer consumer, List partitions) { - return consumer.endOffsets(partitions); - } - - protected abstract Map offsetsFromBeginning( - Consumer consumer, List partitions); - - protected abstract Map offsetsForTimestamp( - Consumer consumer); - - protected abstract Map offsetsFromPositions( - Consumer consumer, List partitions); - - public static class WaitingOffsets { - private final Map endOffsets; // partition number -> offset - private final Map beginOffsets; // partition number -> offset - private final String topic; - - public WaitingOffsets(String topic, Consumer consumer, - Collection partitions) { - this.topic = topic; - var allBeginningOffsets = consumer.beginningOffsets(partitions); - var allEndOffsets = consumer.endOffsets(partitions); - - this.endOffsets = allEndOffsets.entrySet().stream() - .filter(entry -> !allBeginningOffsets.get(entry.getKey()).equals(entry.getValue())) - .map(e -> Tuples.of(e.getKey().partition(), e.getValue() - 1)) - .collect(Collectors.toMap(Tuple2::getT1, Tuple2::getT2)); - - this.beginOffsets = this.endOffsets.keySet().stream() - .map(p -> Tuples.of(p, allBeginningOffsets.get(new TopicPartition(topic, p)))) - .collect(Collectors.toMap(Tuple2::getT1, Tuple2::getT2)); - } - - public void markPolled(ConsumerRecord rec) { - markPolled(rec.partition(), rec.offset()); - } - - public void markPolled(int partition, long offset) { - Long endWaiting = endOffsets.get(partition); - if (endWaiting != null && endWaiting <= offset) { - endOffsets.remove(partition); - } - Long beginWaiting = beginOffsets.get(partition); - if (beginWaiting != null && beginWaiting >= offset) { - beginOffsets.remove(partition); - } - } - - public boolean endReached() { - return endOffsets.isEmpty(); - } - - public boolean beginReached() { - return beginOffsets.isEmpty(); - } - - public Map getEndOffsets() { - return endOffsets; - } - - public Map getBeginOffsets() { - return beginOffsets; - } - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/OffsetsSeekBackward.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/OffsetsSeekBackward.java deleted file mode 100644 index e3d8f1b5b84..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/OffsetsSeekBackward.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.provectus.kafka.ui.util; - -import com.provectus.kafka.ui.model.ConsumerPosition; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import lombok.extern.slf4j.Slf4j; -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.common.TopicPartition; -import org.apache.kafka.common.utils.Bytes; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -@Slf4j -public class OffsetsSeekBackward extends OffsetsSeek { - - private final int maxMessages; - - public OffsetsSeekBackward(String topic, - ConsumerPosition consumerPosition, int maxMessages) { - super(topic, consumerPosition); - this.maxMessages = maxMessages; - } - - public int msgsPerPartition(int partitionsSize) { - return msgsPerPartition(maxMessages, partitionsSize); - } - - public int msgsPerPartition(long awaitingMessages, int partitionsSize) { - return (int) Math.ceil((double) awaitingMessages / partitionsSize); - } - - - protected Map offsetsFromPositions(Consumer consumer, - List partitions) { - - return findOffsetsInt(consumer, consumerPosition.getSeekTo(), partitions); - } - - protected Map offsetsFromBeginning(Consumer consumer, - List partitions) { - return findOffsets(consumer, Map.of(), partitions); - } - - protected Map offsetsForTimestamp(Consumer consumer) { - Map timestampsToSearch = - consumerPosition.getSeekTo().entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue - )); - Map offsetsForTimestamps = consumer.offsetsForTimes(timestampsToSearch) - .entrySet().stream() - .filter(e -> e.getValue() != null) - .map(v -> Tuples.of(v.getKey(), v.getValue().offset())) - .collect(Collectors.toMap(Tuple2::getT1, Tuple2::getT2)); - - if (offsetsForTimestamps.isEmpty()) { - throw new IllegalArgumentException("No offsets were found for requested timestamps"); - } - - log.info("Timestamps: {} to offsets: {}", timestampsToSearch, offsetsForTimestamps); - - return findOffsets(consumer, offsetsForTimestamps, offsetsForTimestamps.keySet()); - } - - protected Map findOffsetsInt( - Consumer consumer, Map seekTo, - List partitions) { - return findOffsets(consumer, seekTo, partitions); - } - - protected Map findOffsets( - Consumer consumer, Map seekTo, - Collection partitions) { - - final Map beginningOffsets = consumer.beginningOffsets(partitions); - final Map endOffsets = consumer.endOffsets(partitions); - - final Map seekMap = new HashMap<>(); - final Set emptyPartitions = new HashSet<>(); - - for (Map.Entry entry : seekTo.entrySet()) { - final Long endOffset = endOffsets.get(entry.getKey()); - final Long beginningOffset = beginningOffsets.get(entry.getKey()); - if (beginningOffset != null - && endOffset != null - && beginningOffset < endOffset - && entry.getValue() > beginningOffset - ) { - final Long value; - if (entry.getValue() > endOffset) { - value = endOffset; - } else { - value = entry.getValue(); - } - - seekMap.put(entry.getKey(), value); - } else { - emptyPartitions.add(entry.getKey()); - } - } - - Set waiting = new HashSet<>(partitions); - waiting.removeAll(emptyPartitions); - waiting.removeAll(seekMap.keySet()); - - for (TopicPartition topicPartition : waiting) { - seekMap.put(topicPartition, endOffsets.get(topicPartition)); - } - - return seekMap; - } - - -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/OffsetsSeekForward.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/OffsetsSeekForward.java deleted file mode 100644 index 6b6ea735fc3..00000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/OffsetsSeekForward.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.provectus.kafka.ui.util; - -import com.provectus.kafka.ui.model.ConsumerPosition; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import lombok.extern.slf4j.Slf4j; -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.common.TopicPartition; -import org.apache.kafka.common.utils.Bytes; - -@Slf4j -public class OffsetsSeekForward extends OffsetsSeek { - - public OffsetsSeekForward(String topic, ConsumerPosition consumerPosition) { - super(topic, consumerPosition); - } - - protected Map offsetsFromPositions(Consumer consumer, - List partitions) { - final Map offsets = - offsetsFromBeginning(consumer, partitions); - - final Map endOffsets = consumer.endOffsets(offsets.keySet()); - final Set set = new HashSet<>(consumerPosition.getSeekTo().keySet()); - final Map collect = consumerPosition.getSeekTo().entrySet().stream() - .filter(e -> e.getValue() < endOffsets.get(e.getKey())) - .filter(e -> endOffsets.get(e.getKey()) > offsets.get(e.getKey())) - .collect(Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue - )); - offsets.putAll(collect); - set.removeAll(collect.keySet()); - set.forEach(offsets::remove); - - return offsets; - } - - protected Map offsetsForTimestamp(Consumer consumer) { - Map offsetsForTimestamps = - consumer.offsetsForTimes(consumerPosition.getSeekTo()) - .entrySet().stream() - .filter(e -> e.getValue() != null) - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().offset())); - - if (offsetsForTimestamps.isEmpty()) { - throw new IllegalArgumentException("No offsets were found for requested timestamps"); - } - - return offsetsForTimestamps; - } - - protected Map offsetsFromBeginning(Consumer consumer, - List partitions) { - return consumer.beginningOffsets(partitions); - } - -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ReactiveFailover.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ReactiveFailover.java new file mode 100644 index 00000000000..0293e4f9250 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ReactiveFailover.java @@ -0,0 +1,169 @@ +package com.provectus.kafka.ui.util; + +import com.google.common.base.Preconditions; +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class ReactiveFailover { + + public static final Duration DEFAULT_RETRY_GRACE_PERIOD_MS = Duration.ofSeconds(5); + public static final Predicate CONNECTION_REFUSED_EXCEPTION_FILTER = + error -> error.getCause() instanceof IOException && error.getCause().getMessage().contains("Connection refused"); + + private final List> publishers; + private int currentIndex = 0; + + private final Predicate failoverExceptionsPredicate; + private final String noAvailablePublishersMsg; + + // creates single-publisher failover (basically for tests usage) + public static ReactiveFailover createNoop(T publisher) { + return create( + List.of(publisher), + th -> true, + "publisher is not available", + DEFAULT_RETRY_GRACE_PERIOD_MS + ); + } + + public static ReactiveFailover create(List publishers, + Predicate failoverExeptionsPredicate, + String noAvailablePublishersMsg, + Duration retryGracePeriodMs) { + return new ReactiveFailover<>( + publishers.stream().map(p -> new PublisherHolder<>(() -> p, retryGracePeriodMs.toMillis())).toList(), + failoverExeptionsPredicate, + noAvailablePublishersMsg + ); + } + + public static ReactiveFailover create(List args, + Function factory, + Predicate failoverExeptionsPredicate, + String noAvailablePublishersMsg, + Duration retryGracePeriodMs) { + return new ReactiveFailover<>( + args.stream().map(arg -> + new PublisherHolder<>(() -> factory.apply(arg), retryGracePeriodMs.toMillis())).toList(), + failoverExeptionsPredicate, + noAvailablePublishersMsg + ); + } + + private ReactiveFailover(List> publishers, + Predicate failoverExceptionsPredicate, + String noAvailablePublishersMsg) { + Preconditions.checkArgument(!publishers.isEmpty()); + this.publishers = publishers; + this.failoverExceptionsPredicate = failoverExceptionsPredicate; + this.noAvailablePublishersMsg = noAvailablePublishersMsg; + } + + public Mono mono(Function> f) { + List> candidates = getActivePublishers(); + if (candidates.isEmpty()) { + return Mono.error(() -> new IllegalStateException(noAvailablePublishersMsg)); + } + return mono(f, candidates); + } + + private Mono mono(Function> f, List> candidates) { + var publisher = candidates.get(0); + return publisher.get() + .flatMap(f) + .onErrorResume(failoverExceptionsPredicate, th -> { + publisher.markFailed(); + if (candidates.size() == 1) { + return Mono.error(th); + } + var newCandidates = candidates.stream().skip(1).filter(PublisherHolder::isActive).toList(); + if (newCandidates.isEmpty()) { + return Mono.error(th); + } + return mono(f, newCandidates); + }); + } + + public Flux flux(Function> f) { + List> candidates = getActivePublishers(); + if (candidates.isEmpty()) { + return Flux.error(() -> new IllegalStateException(noAvailablePublishersMsg)); + } + return flux(f, candidates); + } + + private Flux flux(Function> f, List> candidates) { + var publisher = candidates.get(0); + return publisher.get() + .flatMapMany(f) + .onErrorResume(failoverExceptionsPredicate, th -> { + publisher.markFailed(); + if (candidates.size() == 1) { + return Flux.error(th); + } + var newCandidates = candidates.stream().skip(1).filter(PublisherHolder::isActive).toList(); + if (newCandidates.isEmpty()) { + return Flux.error(th); + } + return flux(f, newCandidates); + }); + } + + /** + * Returns list of active publishers, starting with latest active. + */ + private synchronized List> getActivePublishers() { + var result = new ArrayList>(); + for (int i = 0, j = currentIndex; i < publishers.size(); i++) { + var publisher = publishers.get(j); + if (publisher.isActive()) { + result.add(publisher); + } else if (currentIndex == j) { + currentIndex = ++currentIndex == publishers.size() ? 0 : currentIndex; + } + j = ++j == publishers.size() ? 0 : j; + } + return result; + } + + static class PublisherHolder { + + private final long retryGracePeriodMs; + private final Supplier supplier; + private final AtomicLong lastErrorTs = new AtomicLong(); + private T publisherInstance; + + PublisherHolder(Supplier supplier, long retryGracePeriodMs) { + this.supplier = supplier; + this.retryGracePeriodMs = retryGracePeriodMs; + } + + synchronized Mono get() { + if (publisherInstance == null) { + try { + publisherInstance = supplier.get(); + } catch (Throwable th) { + return Mono.error(th); + } + } + return Mono.just(publisherInstance); + } + + void markFailed() { + lastErrorTs.set(System.currentTimeMillis()); + } + + boolean isActive() { + return System.currentTimeMillis() - lastErrorTs.get() > retryGracePeriodMs; + } + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/SslPropertiesUtil.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/SslPropertiesUtil.java new file mode 100644 index 00000000000..4d157fbcb5f --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/SslPropertiesUtil.java @@ -0,0 +1,23 @@ +package com.provectus.kafka.ui.util; + +import com.provectus.kafka.ui.config.ClustersProperties; +import java.util.Properties; +import javax.annotation.Nullable; +import org.apache.kafka.common.config.SslConfigs; + +public final class SslPropertiesUtil { + + private SslPropertiesUtil() { + } + + public static void addKafkaSslProperties(@Nullable ClustersProperties.TruststoreConfig truststoreConfig, + Properties sink) { + if (truststoreConfig != null && truststoreConfig.getTruststoreLocation() != null) { + sink.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, truststoreConfig.getTruststoreLocation()); + if (truststoreConfig.getTruststorePassword() != null) { + sink.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, truststoreConfig.getTruststorePassword()); + } + } + } + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/WebClientConfigurator.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/WebClientConfigurator.java new file mode 100644 index 00000000000..c5aca5ad716 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/WebClientConfigurator.java @@ -0,0 +1,134 @@ +package com.provectus.kafka.ui.util; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.provectus.kafka.ui.config.ClustersProperties; +import com.provectus.kafka.ui.exception.ValidationException; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import java.io.FileInputStream; +import java.security.KeyStore; +import java.util.function.Consumer; +import javax.annotation.Nullable; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManagerFactory; +import lombok.SneakyThrows; +import org.openapitools.jackson.nullable.JsonNullableModule; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.http.codec.ClientCodecConfigurer; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.util.ResourceUtils; +import org.springframework.util.unit.DataSize; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.netty.http.client.HttpClient; + +public class WebClientConfigurator { + + private final WebClient.Builder builder = WebClient.builder(); + private HttpClient httpClient = HttpClient + .create() + .proxyWithSystemProperties(); + + public WebClientConfigurator() { + configureObjectMapper(defaultOM()); + } + + private static ObjectMapper defaultOM() { + return new ObjectMapper() + .registerModule(new JavaTimeModule()) + .registerModule(new JsonNullableModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + public WebClientConfigurator configureSsl(@Nullable ClustersProperties.TruststoreConfig truststoreConfig, + @Nullable ClustersProperties.KeystoreConfig keystoreConfig) { + return configureSsl( + keystoreConfig != null ? keystoreConfig.getKeystoreLocation() : null, + keystoreConfig != null ? keystoreConfig.getKeystorePassword() : null, + truststoreConfig != null ? truststoreConfig.getTruststoreLocation() : null, + truststoreConfig != null ? truststoreConfig.getTruststorePassword() : null + ); + } + + @SneakyThrows + private WebClientConfigurator configureSsl( + @Nullable String keystoreLocation, + @Nullable String keystorePassword, + @Nullable String truststoreLocation, + @Nullable String truststorePassword) { + if (truststoreLocation == null && keystoreLocation == null) { + return this; + } + + SslContextBuilder contextBuilder = SslContextBuilder.forClient(); + if (truststoreLocation != null && truststorePassword != null) { + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load( + new FileInputStream((ResourceUtils.getFile(truststoreLocation))), + truststorePassword.toCharArray() + ); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm() + ); + trustManagerFactory.init(trustStore); + contextBuilder.trustManager(trustManagerFactory); + } + + // Prepare keystore only if we got a keystore + if (keystoreLocation != null && keystorePassword != null) { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load( + new FileInputStream(ResourceUtils.getFile(keystoreLocation)), + keystorePassword.toCharArray() + ); + + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, keystorePassword.toCharArray()); + contextBuilder.keyManager(keyManagerFactory); + } + + // Create webclient + SslContext context = contextBuilder.build(); + + httpClient = httpClient.secure(t -> t.sslContext(context)); + return this; + } + + public WebClientConfigurator configureBasicAuth(@Nullable String username, @Nullable String password) { + if (username != null && password != null) { + builder.defaultHeaders(httpHeaders -> httpHeaders.setBasicAuth(username, password)); + } else if (username != null) { + throw new ValidationException("You specified username but did not specify password"); + } else if (password != null) { + throw new ValidationException("You specified password but did not specify username"); + } + return this; + } + + public WebClientConfigurator configureBufferSize(DataSize maxBuffSize) { + builder.codecs(c -> c.defaultCodecs().maxInMemorySize((int) maxBuffSize.toBytes())); + return this; + } + + public WebClientConfigurator configureObjectMapper(ObjectMapper mapper) { + builder.codecs(codecs -> { + codecs.defaultCodecs() + .jackson2JsonEncoder(new Jackson2JsonEncoder(mapper, MediaType.APPLICATION_JSON)); + codecs.defaultCodecs() + .jackson2JsonDecoder(new Jackson2JsonDecoder(mapper, MediaType.APPLICATION_JSON)); + }); + return this; + } + + public WebClientConfigurator configureCodecs(Consumer configurer) { + builder.codecs(configurer); + return this; + } + + public WebClient build() { + return builder.clientConnector(new ReactorClientHttpConnector(httpClient)).build(); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/annotations/KafkaClientInternalsDependant.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/annotation/KafkaClientInternalsDependant.java similarity index 75% rename from kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/annotations/KafkaClientInternalsDependant.java rename to kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/annotation/KafkaClientInternalsDependant.java index 1003ff0d7fd..440c9887834 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/annotations/KafkaClientInternalsDependant.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/annotation/KafkaClientInternalsDependant.java @@ -1,8 +1,9 @@ -package com.provectus.kafka.ui.util.annotations; +package com.provectus.kafka.ui.util.annotation; /** * All code places that depend on kafka-client's internals or implementation-specific logic * should be marked with this annotation to make further update process easier. */ public @interface KafkaClientInternalsDependant { + String value() default ""; } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/AnyFieldSchema.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/AnyFieldSchema.java new file mode 100644 index 00000000000..333a6bd633a --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/AnyFieldSchema.java @@ -0,0 +1,27 @@ +package com.provectus.kafka.ui.util.jsonschema; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +// Specifies field that can contain any kind of value - primitive, complex and nulls +class AnyFieldSchema implements FieldSchema { + + static AnyFieldSchema get() { + return new AnyFieldSchema(); + } + + private AnyFieldSchema() { + } + + @Override + public JsonNode toJsonNode(ObjectMapper mapper) { + var arr = mapper.createArrayNode(); + arr.add("number"); + arr.add("string"); + arr.add("object"); + arr.add("array"); + arr.add("boolean"); + arr.add("null"); + return mapper.createObjectNode().set("type", arr); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/ArrayFieldSchema.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/ArrayFieldSchema.java index c5cefe94c91..b20d09550c3 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/ArrayFieldSchema.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/ArrayFieldSchema.java @@ -4,10 +4,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -public class ArrayFieldSchema implements FieldSchema { +class ArrayFieldSchema implements FieldSchema { private final FieldSchema itemsSchema; - public ArrayFieldSchema(FieldSchema itemsSchema) { + ArrayFieldSchema(FieldSchema itemsSchema) { this.itemsSchema = itemsSchema; } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/AvroJsonSchemaConverter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/AvroJsonSchemaConverter.java index 61f742e7214..84f56b81dc0 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/AvroJsonSchemaConverter.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/AvroJsonSchemaConverter.java @@ -4,8 +4,8 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import org.apache.avro.Schema; import reactor.util.function.Tuple2; @@ -22,7 +22,7 @@ public JsonSchema convert(URI basePath, Schema schema) { builder.type(type); Map definitions = new HashMap<>(); - final FieldSchema root = convertSchema("root", schema, definitions, false); + final FieldSchema root = convertSchema(schema, definitions, true); builder.definitions(definitions); if (type.getType().equals(JsonType.Type.OBJECT)) { @@ -36,11 +36,15 @@ public JsonSchema convert(URI basePath, Schema schema) { private FieldSchema convertField(Schema.Field field, Map definitions) { - return convertSchema(field.name(), field.schema(), definitions, true); + return convertSchema(field.schema(), definitions, false); } - private FieldSchema convertSchema(String name, Schema schema, - Map definitions, boolean ref) { + private FieldSchema convertSchema(Schema schema, + Map definitions, boolean isRoot) { + Optional logicalTypeSchema = JsonAvroConversion.LogicalTypeConversion.getJsonSchema(schema); + if (logicalTypeSchema.isPresent()) { + return logicalTypeSchema.get(); + } if (!schema.isUnion()) { JsonType type = convertType(schema); switch (type.getType()) { @@ -53,12 +57,12 @@ private FieldSchema convertSchema(String name, Schema schema, return new SimpleFieldSchema(type); case OBJECT: if (schema.getType().equals(Schema.Type.MAP)) { - return new MapFieldSchema(convertSchema(name, schema.getValueType(), definitions, ref)); + return new MapFieldSchema(convertSchema(schema.getValueType(), definitions, isRoot)); } else { - return createObjectSchema(name, schema, definitions, ref); + return createObjectSchema(schema, definitions, isRoot); } case ARRAY: - return createArraySchema(name, schema, definitions); + return createArraySchema(schema, definitions); default: throw new RuntimeException("Unknown type"); } @@ -67,20 +71,25 @@ private FieldSchema convertSchema(String name, Schema schema, } } + // this method formats json-schema field in a way + // to fit avro-> json encoding rules (https://avro.apache.org/docs/1.11.1/specification/_print/#json-encoding) private FieldSchema createUnionSchema(Schema schema, Map definitions) { - final boolean nullable = schema.getTypes().stream() .anyMatch(t -> t.getType().equals(Schema.Type.NULL)); final Map fields = schema.getTypes().stream() .filter(t -> !t.getType().equals(Schema.Type.NULL)) - .map(f -> Tuples.of( - f.getType().getName().toLowerCase(Locale.ROOT), - convertSchema( - f.getType().getName().toLowerCase(Locale.ROOT), - f, definitions, true - ) - )).collect(Collectors.toMap( + .map(f -> { + String oneOfFieldName; + if (f.getType().equals(Schema.Type.RECORD)) { + // for records using full record name + oneOfFieldName = f.getFullName(); + } else { + // for primitive types - using type name + oneOfFieldName = f.getType().getName().toLowerCase(); + } + return Tuples.of(oneOfFieldName, convertSchema(f, definitions, false)); + }).collect(Collectors.toMap( Tuple2::getT1, Tuple2::getT2 )); @@ -97,8 +106,16 @@ private FieldSchema createUnionSchema(Schema schema, Map de } } - private FieldSchema createObjectSchema(String name, Schema schema, - Map definitions, boolean ref) { + private FieldSchema createObjectSchema(Schema schema, + Map definitions, + boolean isRoot) { + var definitionName = schema.getFullName(); + if (definitions.containsKey(definitionName)) { + return createRefField(definitionName); + } + // adding stub record, need to avoid infinite recursion + definitions.put(definitionName, ObjectFieldSchema.EMPTY); + final Map fields = schema.getFields().stream() .map(f -> Tuples.of(f.name(), convertField(f, definitions))) .collect(Collectors.toMap( @@ -110,47 +127,40 @@ private FieldSchema createObjectSchema(String name, Schema schema, .filter(f -> !f.schema().isNullable()) .map(Schema.Field::name).collect(Collectors.toList()); - if (ref) { - String definitionName = String.format("Record%s", schema.getName()); - definitions.put(definitionName, new ObjectFieldSchema(fields, required)); - return new RefFieldSchema(String.format("#/definitions/%s", definitionName)); + var objectSchema = new ObjectFieldSchema(fields, required); + if (isRoot) { + // replacing stub with self-reference (need for usage in json-schema's oneOf) + definitions.put(definitionName, new RefFieldSchema("#")); + return objectSchema; } else { - return new ObjectFieldSchema(fields, required); + // replacing stub record with actual object structure + definitions.put(definitionName, objectSchema); + return createRefField(definitionName); } } - private ArrayFieldSchema createArraySchema(String name, Schema schema, + private RefFieldSchema createRefField(String definitionName) { + return new RefFieldSchema(String.format("#/definitions/%s", definitionName)); + } + + private ArrayFieldSchema createArraySchema(Schema schema, Map definitions) { return new ArrayFieldSchema( - convertSchema(name, schema.getElementType(), definitions, true) + convertSchema(schema.getElementType(), definitions, false) ); } private JsonType convertType(Schema schema) { - switch (schema.getType()) { - case INT: - case LONG: - return new SimpleJsonType(JsonType.Type.INTEGER); - case MAP: - case RECORD: - return new SimpleJsonType(JsonType.Type.OBJECT); - case ENUM: - return new EnumJsonType(schema.getEnumSymbols()); - case BYTES: - case STRING: - return new SimpleJsonType(JsonType.Type.STRING); - case NULL: - return new SimpleJsonType(JsonType.Type.NULL); - case ARRAY: - return new SimpleJsonType(JsonType.Type.ARRAY); - case FIXED: - case FLOAT: - case DOUBLE: - return new SimpleJsonType(JsonType.Type.NUMBER); - case BOOLEAN: - return new SimpleJsonType(JsonType.Type.BOOLEAN); - default: - return new SimpleJsonType(JsonType.Type.STRING); - } + return switch (schema.getType()) { + case INT, LONG -> new SimpleJsonType(JsonType.Type.INTEGER); + case MAP, RECORD -> new SimpleJsonType(JsonType.Type.OBJECT); + case ENUM -> new EnumJsonType(schema.getEnumSymbols()); + case BYTES, STRING -> new SimpleJsonType(JsonType.Type.STRING); + case NULL -> new SimpleJsonType(JsonType.Type.NULL); + case ARRAY -> new SimpleJsonType(JsonType.Type.ARRAY); + case FIXED, FLOAT, DOUBLE -> new SimpleJsonType(JsonType.Type.NUMBER); + case BOOLEAN -> new SimpleJsonType(JsonType.Type.BOOLEAN); + default -> new SimpleJsonType(JsonType.Type.STRING); + }; } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/EnumJsonType.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/EnumJsonType.java index 715f7d5f442..a43d45cd84d 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/EnumJsonType.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/EnumJsonType.java @@ -7,10 +7,10 @@ import java.util.Map; -public class EnumJsonType extends JsonType { +class EnumJsonType extends JsonType { private final List values; - public EnumJsonType(List values) { + EnumJsonType(List values) { super(Type.ENUM); this.values = values; } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/FieldSchema.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/FieldSchema.java index 19166bf3106..c8ad7e953b2 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/FieldSchema.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/FieldSchema.java @@ -3,6 +3,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -public interface FieldSchema { +interface FieldSchema { JsonNode toJsonNode(ObjectMapper mapper); } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/JsonAvroConversion.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/JsonAvroConversion.java new file mode 100644 index 00000000000..74701d0c70c --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/JsonAvroConversion.java @@ -0,0 +1,544 @@ +package com.provectus.kafka.ui.util.jsonschema; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.fasterxml.jackson.databind.node.DecimalNode; +import com.fasterxml.jackson.databind.node.DoubleNode; +import com.fasterxml.jackson.databind.node.FloatNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.fasterxml.jackson.databind.node.LongNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.google.common.collect.Lists; +import com.provectus.kafka.ui.exception.JsonAvroConversionException; +import io.confluent.kafka.serializers.AvroData; +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; +import java.util.stream.Stream; +import org.apache.avro.Schema; +import org.apache.avro.generic.GenericData; + +// json <-> avro +public class JsonAvroConversion { + + private static final JsonMapper MAPPER = new JsonMapper(); + private static final Schema NULL_SCHEMA = Schema.create(Schema.Type.NULL); + private static final String FORMAT = "format"; + private static final String DATE_TIME = "date-time"; + + // converts json into Object that is expected input for KafkaAvroSerializer + // (with AVRO_USE_LOGICAL_TYPE_CONVERTERS flat enabled!) + public static Object convertJsonToAvro(String jsonString, Schema avroSchema) { + JsonNode rootNode = null; + try { + rootNode = MAPPER.readTree(jsonString); + } catch (JsonProcessingException e) { + throw new JsonAvroConversionException("String is not a valid json"); + } + return convert(rootNode, avroSchema); + } + + private static Object convert(JsonNode node, Schema avroSchema) { + return switch (avroSchema.getType()) { + case RECORD -> { + assertJsonType(node, JsonNodeType.OBJECT); + var rec = new GenericData.Record(avroSchema); + for (Schema.Field field : avroSchema.getFields()) { + if (node.has(field.name()) && !node.get(field.name()).isNull()) { + rec.put(field.name(), convert(node.get(field.name()), field.schema())); + } + } + yield rec; + } + case MAP -> { + assertJsonType(node, JsonNodeType.OBJECT); + var map = new LinkedHashMap(); + var valueSchema = avroSchema.getValueType(); + node.fields().forEachRemaining(f -> map.put(f.getKey(), convert(f.getValue(), valueSchema))); + yield map; + } + case ARRAY -> { + assertJsonType(node, JsonNodeType.ARRAY); + var lst = new ArrayList<>(); + node.elements().forEachRemaining(e -> lst.add(convert(e, avroSchema.getElementType()))); + yield lst; + } + case ENUM -> { + assertJsonType(node, JsonNodeType.STRING); + String symbol = node.textValue(); + if (!avroSchema.getEnumSymbols().contains(symbol)) { + throw new JsonAvroConversionException("%s is not a part of enum symbols [%s]" + .formatted(symbol, avroSchema.getEnumSymbols())); + } + yield new GenericData.EnumSymbol(avroSchema, symbol); + } + case UNION -> { + // for types from enum (other than null) payload should be an object with single key == name of type + // ex: schema = [ "null", "int", "string" ], possible payloads = null, { "string": "str" }, { "int": 123 } + if (node.isNull() && avroSchema.getTypes().contains(NULL_SCHEMA)) { + yield null; + } + + assertJsonType(node, JsonNodeType.OBJECT); + var elements = Lists.newArrayList(node.fields()); + if (elements.size() != 1) { + throw new JsonAvroConversionException( + "UNION field value should be an object with single field == type name"); + } + Map.Entry typeNameToValue = elements.get(0); + List candidates = new ArrayList<>(); + for (Schema unionType : avroSchema.getTypes()) { + if (typeNameToValue.getKey().equals(unionType.getFullName())) { + yield convert(typeNameToValue.getValue(), unionType); + } + if (typeNameToValue.getKey().equals(unionType.getName())) { + candidates.add(unionType); + } + } + if (candidates.size() == 1) { + yield convert(typeNameToValue.getValue(), candidates.get(0)); + } + if (candidates.size() > 1) { + throw new JsonAvroConversionException( + "Can't select type within union for value '%s'. Provide full type name.".formatted(node) + ); + } + throw new JsonAvroConversionException( + "json value '%s' is cannot be converted to any of union types [%s]" + .formatted(node, avroSchema.getTypes())); + } + case STRING -> { + if (isLogicalType(avroSchema)) { + yield processLogicalType(node, avroSchema); + } + assertJsonType(node, JsonNodeType.STRING); + yield node.textValue(); + } + case LONG -> { + if (isLogicalType(avroSchema)) { + yield processLogicalType(node, avroSchema); + } + assertJsonType(node, JsonNodeType.NUMBER); + assertJsonNumberType(node, JsonParser.NumberType.LONG, JsonParser.NumberType.INT); + yield node.longValue(); + } + case INT -> { + if (isLogicalType(avroSchema)) { + yield processLogicalType(node, avroSchema); + } + assertJsonType(node, JsonNodeType.NUMBER); + assertJsonNumberType(node, JsonParser.NumberType.INT); + yield node.intValue(); + } + case FLOAT -> { + assertJsonType(node, JsonNodeType.NUMBER); + assertJsonNumberType(node, JsonParser.NumberType.DOUBLE, JsonParser.NumberType.FLOAT); + yield node.floatValue(); + } + case DOUBLE -> { + assertJsonType(node, JsonNodeType.NUMBER); + assertJsonNumberType(node, JsonParser.NumberType.DOUBLE, JsonParser.NumberType.FLOAT); + yield node.doubleValue(); + } + case BOOLEAN -> { + assertJsonType(node, JsonNodeType.BOOLEAN); + yield node.booleanValue(); + } + case NULL -> { + assertJsonType(node, JsonNodeType.NULL); + yield null; + } + case BYTES -> { + if (isLogicalType(avroSchema)) { + yield processLogicalType(node, avroSchema); + } + assertJsonType(node, JsonNodeType.STRING); + // logic copied from JsonDecoder::readBytes + yield ByteBuffer.wrap(node.textValue().getBytes(StandardCharsets.ISO_8859_1)); + } + case FIXED -> { + if (isLogicalType(avroSchema)) { + yield processLogicalType(node, avroSchema); + } + assertJsonType(node, JsonNodeType.STRING); + byte[] bytes = node.textValue().getBytes(StandardCharsets.ISO_8859_1); + if (bytes.length != avroSchema.getFixedSize()) { + throw new JsonAvroConversionException( + "Fixed field has unexpected size %d (should be %d)" + .formatted(bytes.length, avroSchema.getFixedSize())); + } + yield new GenericData.Fixed(avroSchema, bytes); + } + }; + } + + // converts output of KafkaAvroDeserializer (with AVRO_USE_LOGICAL_TYPE_CONVERTERS flat enabled!) into json. + // Note: conversion should be compatible with AvroJsonSchemaConverter logic! + public static JsonNode convertAvroToJson(Object obj, Schema avroSchema) { + if (obj == null) { + return NullNode.getInstance(); + } + return switch (avroSchema.getType()) { + case RECORD -> { + var rec = (GenericData.Record) obj; + ObjectNode node = MAPPER.createObjectNode(); + for (Schema.Field field : avroSchema.getFields()) { + var fieldVal = rec.get(field.name()); + if (fieldVal != null) { + node.set(field.name(), convertAvroToJson(fieldVal, field.schema())); + } + } + yield node; + } + case MAP -> { + ObjectNode node = MAPPER.createObjectNode(); + ((Map) obj).forEach((k, v) -> node.set(k.toString(), convertAvroToJson(v, avroSchema.getValueType()))); + yield node; + } + case ARRAY -> { + var list = (List) obj; + ArrayNode node = MAPPER.createArrayNode(); + list.forEach(e -> node.add(convertAvroToJson(e, avroSchema.getElementType()))); + yield node; + } + case ENUM -> { + yield new TextNode(obj.toString()); + } + case UNION -> { + ObjectNode node = MAPPER.createObjectNode(); + int unionIdx = AvroData.getGenericData().resolveUnion(avroSchema, obj); + Schema selectedType = avroSchema.getTypes().get(unionIdx); + node.set( + selectUnionTypeFieldName(avroSchema, selectedType, unionIdx), + convertAvroToJson(obj, selectedType) + ); + yield node; + } + case STRING -> { + if (isLogicalType(avroSchema)) { + yield processLogicalType(obj, avroSchema); + } + yield new TextNode(obj.toString()); + } + case LONG -> { + if (isLogicalType(avroSchema)) { + yield processLogicalType(obj, avroSchema); + } + yield new LongNode((Long) obj); + } + case INT -> { + if (isLogicalType(avroSchema)) { + yield processLogicalType(obj, avroSchema); + } + yield new IntNode((Integer) obj); + } + case FLOAT -> new FloatNode((Float) obj); + case DOUBLE -> new DoubleNode((Double) obj); + case BOOLEAN -> BooleanNode.valueOf((Boolean) obj); + case NULL -> NullNode.getInstance(); + case BYTES -> { + if (isLogicalType(avroSchema)) { + yield processLogicalType(obj, avroSchema); + } + ByteBuffer bytes = (ByteBuffer) obj; + //see JsonEncoder::writeByteArray + yield new TextNode(new String(bytes.array(), StandardCharsets.ISO_8859_1)); + } + case FIXED -> { + if (isLogicalType(avroSchema)) { + yield processLogicalType(obj, avroSchema); + } + var fixed = (GenericData.Fixed) obj; + yield new TextNode(new String(fixed.bytes(), StandardCharsets.ISO_8859_1)); + } + }; + } + + // select name for a key field that represents type name of union. + // For records selects short name, if it is possible. + private static String selectUnionTypeFieldName(Schema unionSchema, + Schema chosenType, + int chosenTypeIdx) { + var types = unionSchema.getTypes(); + if (types.size() == 2 && types.contains(NULL_SCHEMA)) { + return chosenType.getName(); + } + for (int i = 0; i < types.size(); i++) { + if (i != chosenTypeIdx && chosenType.getName().equals(types.get(i).getName())) { + // there is another type inside union with the same name + // so, we have to use fullname + return chosenType.getFullName(); + } + } + return chosenType.getName(); + } + + private static Object processLogicalType(JsonNode node, Schema schema) { + return findConversion(schema) + .map(c -> c.jsonToAvroConversion.apply(node, schema)) + .orElseThrow(() -> + new JsonAvroConversionException("'%s' logical type is not supported" + .formatted(schema.getLogicalType().getName()))); + } + + private static JsonNode processLogicalType(Object obj, Schema schema) { + return findConversion(schema) + .map(c -> c.avroToJsonConversion.apply(obj, schema)) + .orElseThrow(() -> + new JsonAvroConversionException("'%s' logical type is not supported" + .formatted(schema.getLogicalType().getName()))); + } + + private static Optional findConversion(Schema schema) { + String logicalTypeName = schema.getLogicalType().getName(); + return Stream.of(LogicalTypeConversion.values()) + .filter(t -> t.name.equalsIgnoreCase(logicalTypeName)) + .findFirst(); + } + + private static boolean isLogicalType(Schema schema) { + return schema.getLogicalType() != null; + } + + private static void assertJsonType(JsonNode node, JsonNodeType... allowedTypes) { + if (Stream.of(allowedTypes).noneMatch(t -> node.getNodeType() == t)) { + throw new JsonAvroConversionException( + "%s node has unexpected type, allowed types %s, actual type %s" + .formatted(node, Arrays.toString(allowedTypes), node.getNodeType())); + } + } + + private static void assertJsonNumberType(JsonNode node, JsonParser.NumberType... allowedTypes) { + if (Stream.of(allowedTypes).noneMatch(t -> node.numberType() == t)) { + throw new JsonAvroConversionException( + "%s node has unexpected numeric type, allowed types %s, actual type %s" + .formatted(node, Arrays.toString(allowedTypes), node.numberType())); + } + } + + enum LogicalTypeConversion { + + UUID("uuid", + (node, schema) -> { + assertJsonType(node, JsonNodeType.STRING); + return java.util.UUID.fromString(node.asText()); + }, + (obj, schema) -> { + return new TextNode(obj.toString()); + }, + new SimpleFieldSchema( + new SimpleJsonType( + JsonType.Type.STRING, + Map.of(FORMAT, new TextNode("uuid")))) + ), + + DECIMAL("decimal", + (node, schema) -> { + if (node.isTextual()) { + return new BigDecimal(node.asText()); + } else if (node.isNumber()) { + return new BigDecimal(node.numberValue().toString()); + } + throw new JsonAvroConversionException( + "node '%s' can't be converted to decimal logical type" + .formatted(node)); + }, + (obj, schema) -> { + return new DecimalNode((BigDecimal) obj); + }, + new SimpleFieldSchema(new SimpleJsonType(JsonType.Type.NUMBER)) + ), + + DATE("date", + (node, schema) -> { + if (node.isInt()) { + return LocalDate.ofEpochDay(node.intValue()); + } else if (node.isTextual()) { + return LocalDate.parse(node.asText()); + } else { + throw new JsonAvroConversionException( + "node '%s' can't be converted to date logical type" + .formatted(node)); + } + }, + (obj, schema) -> { + return new TextNode(obj.toString()); + }, + new SimpleFieldSchema( + new SimpleJsonType( + JsonType.Type.STRING, + Map.of(FORMAT, new TextNode("date")))) + ), + + TIME_MILLIS("time-millis", + (node, schema) -> { + if (node.isIntegralNumber()) { + return LocalTime.ofNanoOfDay(TimeUnit.MILLISECONDS.toNanos(node.longValue())); + } else if (node.isTextual()) { + return LocalTime.parse(node.asText()); + } else { + throw new JsonAvroConversionException( + "node '%s' can't be converted to time-millis logical type" + .formatted(node)); + } + }, + (obj, schema) -> { + return new TextNode(obj.toString()); + }, + new SimpleFieldSchema( + new SimpleJsonType( + JsonType.Type.STRING, + Map.of(FORMAT, new TextNode("time")))) + ), + + TIME_MICROS("time-micros", + (node, schema) -> { + if (node.isIntegralNumber()) { + return LocalTime.ofNanoOfDay(TimeUnit.MICROSECONDS.toNanos(node.longValue())); + } else if (node.isTextual()) { + return LocalTime.parse(node.asText()); + } else { + throw new JsonAvroConversionException( + "node '%s' can't be converted to time-micros logical type" + .formatted(node)); + } + }, + (obj, schema) -> { + return new TextNode(obj.toString()); + }, + new SimpleFieldSchema( + new SimpleJsonType( + JsonType.Type.STRING, + Map.of(FORMAT, new TextNode("time")))) + ), + + TIMESTAMP_MILLIS("timestamp-millis", + (node, schema) -> { + if (node.isIntegralNumber()) { + return Instant.ofEpochMilli(node.longValue()); + } else if (node.isTextual()) { + return Instant.parse(node.asText()); + } else { + throw new JsonAvroConversionException( + "node '%s' can't be converted to timestamp-millis logical type" + .formatted(node)); + } + }, + (obj, schema) -> { + return new TextNode(obj.toString()); + }, + new SimpleFieldSchema( + new SimpleJsonType( + JsonType.Type.STRING, + Map.of(FORMAT, new TextNode(DATE_TIME)))) + ), + + TIMESTAMP_MICROS("timestamp-micros", + (node, schema) -> { + if (node.isIntegralNumber()) { + // TimeConversions.TimestampMicrosConversion for impl + long microsFromEpoch = node.longValue(); + long epochSeconds = microsFromEpoch / (1_000_000L); + long nanoAdjustment = (microsFromEpoch % (1_000_000L)) * 1_000L; + return Instant.ofEpochSecond(epochSeconds, nanoAdjustment); + } else if (node.isTextual()) { + return Instant.parse(node.asText()); + } else { + throw new JsonAvroConversionException( + "node '%s' can't be converted to timestamp-millis logical type" + .formatted(node)); + } + }, + (obj, schema) -> { + return new TextNode(obj.toString()); + }, + new SimpleFieldSchema( + new SimpleJsonType( + JsonType.Type.STRING, + Map.of(FORMAT, new TextNode(DATE_TIME)))) + ), + + LOCAL_TIMESTAMP_MILLIS("local-timestamp-millis", + (node, schema) -> { + if (node.isTextual()) { + return LocalDateTime.parse(node.asText()); + } + // TimeConversions.TimestampMicrosConversion for impl + Instant instant = (Instant) TIMESTAMP_MILLIS.jsonToAvroConversion.apply(node, schema); + return LocalDateTime.ofInstant(instant, ZoneOffset.UTC); + }, + (obj, schema) -> { + return new TextNode(obj.toString()); + }, + new SimpleFieldSchema( + new SimpleJsonType( + JsonType.Type.STRING, + Map.of(FORMAT, new TextNode(DATE_TIME)))) + ), + + LOCAL_TIMESTAMP_MICROS("local-timestamp-micros", + (node, schema) -> { + if (node.isTextual()) { + return LocalDateTime.parse(node.asText()); + } + Instant instant = (Instant) TIMESTAMP_MICROS.jsonToAvroConversion.apply(node, schema); + return LocalDateTime.ofInstant(instant, ZoneOffset.UTC); + }, + (obj, schema) -> { + return new TextNode(obj.toString()); + }, + new SimpleFieldSchema( + new SimpleJsonType( + JsonType.Type.STRING, + Map.of(FORMAT, new TextNode(DATE_TIME)))) + ); + + private final String name; + private final BiFunction jsonToAvroConversion; + private final BiFunction avroToJsonConversion; + private final FieldSchema jsonSchema; + + LogicalTypeConversion(String name, + BiFunction jsonToAvroConversion, + BiFunction avroToJsonConversion, + FieldSchema jsonSchema) { + this.name = name; + this.jsonToAvroConversion = jsonToAvroConversion; + this.avroToJsonConversion = avroToJsonConversion; + this.jsonSchema = jsonSchema; + } + + static Optional getJsonSchema(Schema schema) { + if (schema.getLogicalType() == null) { + return Optional.empty(); + } + String logicalTypeName = schema.getLogicalType().getName(); + return Stream.of(JsonAvroConversion.LogicalTypeConversion.values()) + .filter(t -> t.name.equalsIgnoreCase(logicalTypeName)) + .map(c -> c.jsonSchema) + .findFirst(); + } + } + + +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/JsonSchema.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/JsonSchema.java index e8d0594c6cb..248cd027d47 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/JsonSchema.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/JsonSchema.java @@ -23,6 +23,7 @@ public class JsonSchema { private final Map properties; private final Map definitions; private final List required; + private final String rootRef; public String toJson() { final ObjectMapper mapper = new ObjectMapper(); @@ -53,6 +54,9 @@ public String toJson() { )) )); } + if (rootRef != null) { + objectNode.set("$ref", new TextNode(rootRef)); + } return objectNode.toString(); } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/JsonType.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/JsonType.java index 79d73c6813d..392a2260c30 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/JsonType.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/JsonType.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Map; -public abstract class JsonType { +abstract class JsonType { protected final Type type; @@ -12,13 +12,13 @@ protected JsonType(Type type) { this.type = type; } - public Type getType() { + Type getType() { return type; } - public abstract Map toJsonNode(ObjectMapper mapper); + abstract Map toJsonNode(ObjectMapper mapper); - public enum Type { + enum Type { NULL, BOOLEAN, OBJECT, diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/MapFieldSchema.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/MapFieldSchema.java index c7c52acbaba..6b2422ef7da 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/MapFieldSchema.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/MapFieldSchema.java @@ -2,21 +2,27 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.BooleanNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; +import javax.annotation.Nullable; -public class MapFieldSchema implements FieldSchema { - private final FieldSchema itemSchema; +class MapFieldSchema implements FieldSchema { + private final @Nullable FieldSchema itemSchema; - public MapFieldSchema(FieldSchema itemSchema) { + MapFieldSchema(@Nullable FieldSchema itemSchema) { this.itemSchema = itemSchema; } + MapFieldSchema() { + this(null); + } + @Override public JsonNode toJsonNode(ObjectMapper mapper) { final ObjectNode objectNode = mapper.createObjectNode(); objectNode.set("type", new TextNode(JsonType.Type.OBJECT.getName())); - objectNode.set("additionalProperties", itemSchema.toJsonNode(mapper)); + objectNode.set("additionalProperties", itemSchema != null ? itemSchema.toJsonNode(mapper) : BooleanNode.TRUE); return objectNode; } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/ObjectFieldSchema.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/ObjectFieldSchema.java index 7a279e465e6..21d3402288a 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/ObjectFieldSchema.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/ObjectFieldSchema.java @@ -9,21 +9,24 @@ import reactor.util.function.Tuple2; import reactor.util.function.Tuples; -public class ObjectFieldSchema implements FieldSchema { +class ObjectFieldSchema implements FieldSchema { + + static final ObjectFieldSchema EMPTY = new ObjectFieldSchema(Map.of(), List.of()); + private final Map properties; private final List required; - public ObjectFieldSchema(Map properties, + ObjectFieldSchema(Map properties, List required) { this.properties = properties; this.required = required; } - public Map getProperties() { + Map getProperties() { return properties; } - public List getRequired() { + List getRequired() { return required; } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/OneOfFieldSchema.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/OneOfFieldSchema.java index cec8282b70a..3f0b11373e8 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/OneOfFieldSchema.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/OneOfFieldSchema.java @@ -5,11 +5,10 @@ import java.util.List; import java.util.stream.Collectors; -public class OneOfFieldSchema implements FieldSchema { +class OneOfFieldSchema implements FieldSchema { private final List schemaList; - public OneOfFieldSchema( - List schemaList) { + OneOfFieldSchema(List schemaList) { this.schemaList = schemaList; } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/ProtobufSchemaConverter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/ProtobufSchemaConverter.java index 57b49363230..8a0c1071db7 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/ProtobufSchemaConverter.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/ProtobufSchemaConverter.java @@ -1,90 +1,109 @@ package com.provectus.kafka.ui.util.jsonschema; +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.databind.node.BigIntegerNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.LongNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.google.common.primitives.UnsignedInteger; +import com.google.common.primitives.UnsignedLong; +import com.google.protobuf.Any; +import com.google.protobuf.BoolValue; +import com.google.protobuf.BytesValue; import com.google.protobuf.Descriptors; +import com.google.protobuf.DoubleValue; +import com.google.protobuf.Duration; +import com.google.protobuf.FieldMask; +import com.google.protobuf.FloatValue; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; +import com.google.protobuf.ListValue; +import com.google.protobuf.StringValue; +import com.google.protobuf.Struct; +import com.google.protobuf.Timestamp; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.UInt64Value; +import com.google.protobuf.Value; import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import reactor.util.function.Tuple2; import reactor.util.function.Tuples; public class ProtobufSchemaConverter implements JsonSchemaConverter { - @Override - public JsonSchema convert(URI basePath, Descriptors.Descriptor schema) { - final JsonSchema.JsonSchemaBuilder builder = JsonSchema.builder(); - builder.id(basePath.resolve(schema.getFullName())); - builder.type(new SimpleJsonType(JsonType.Type.OBJECT)); + private static final String MAXIMUM = "maximum"; + private static final String MINIMUM = "minimum"; + + private final Set simpleTypesWrapperNames = Set.of( + BoolValue.getDescriptor().getFullName(), + Int32Value.getDescriptor().getFullName(), + UInt32Value.getDescriptor().getFullName(), + Int64Value.getDescriptor().getFullName(), + UInt64Value.getDescriptor().getFullName(), + StringValue.getDescriptor().getFullName(), + BytesValue.getDescriptor().getFullName(), + FloatValue.getDescriptor().getFullName(), + DoubleValue.getDescriptor().getFullName() + ); + @Override + public JsonSchema convert(URI basePath, Descriptors.Descriptor schema) { Map definitions = new HashMap<>(); - final ObjectFieldSchema root = - (ObjectFieldSchema) convertObjectSchema(schema, definitions, false); - builder.definitions(definitions); - - builder.properties(root.getProperties()); - builder.required(root.getRequired()); - - return builder.build(); + RefFieldSchema rootRef = registerObjectAndReturnRef(schema, definitions); + return JsonSchema.builder() + .id(basePath.resolve(schema.getFullName())) + .type(new SimpleJsonType(JsonType.Type.OBJECT)) + .rootRef(rootRef.getRef()) + .definitions(definitions) + .build(); } - private FieldSchema convertObjectSchema(Descriptors.Descriptor schema, - Map definitions, boolean ref) { - final Map fields = schema.getFields().stream() - .map(f -> Tuples.of(f.getName(), convertField(f, definitions))) - .collect(Collectors.toMap( - Tuple2::getT1, - Tuple2::getT2 - )); - - final Map oneOfFields = schema.getOneofs().stream().map(o -> - Tuples.of( - o.getName(), - new OneOfFieldSchema( - o.getFields().stream().map( - Descriptors.FieldDescriptor::getName - ).map(fields::get).collect(Collectors.toList()) - ) - ) - ).collect(Collectors.toMap( - Tuple2::getT1, - Tuple2::getT2 - )); - - final List allOneOfFields = schema.getOneofs().stream().flatMap(o -> - o.getFields().stream().map(Descriptors.FieldDescriptor::getName) - ).collect(Collectors.toList()); + private RefFieldSchema registerObjectAndReturnRef(Descriptors.Descriptor schema, + Map definitions) { + var definition = schema.getFullName(); + if (definitions.containsKey(definition)) { + return createRefField(definition); + } + // adding stub record, need to avoid infinite recursion + definitions.put(definition, ObjectFieldSchema.EMPTY); - final Map excludedOneOf = fields.entrySet().stream() - .filter(f -> !allOneOfFields.contains(f.getKey())) - .collect(Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue - )); + Map fields = schema.getFields().stream() + .map(f -> Tuples.of(f.getName(), convertField(f, definitions))) + .collect(Collectors.toMap(Tuple2::getT1, Tuple2::getT2)); - Map finalFields = new HashMap<>(excludedOneOf); - finalFields.putAll(oneOfFields); + List required = schema.getFields().stream() + .filter(Descriptors.FieldDescriptor::isRequired) + .map(Descriptors.FieldDescriptor::getName) + .collect(Collectors.toList()); - final List required = schema.getFields().stream() - .filter(f -> !f.isOptional()) - .map(Descriptors.FieldDescriptor::getName).collect(Collectors.toList()); + // replacing stub record with actual object structure + definitions.put(definition, new ObjectFieldSchema(fields, required)); + return createRefField(definition); + } - if (ref) { - String definitionName = String.format("record.%s", schema.getFullName()); - definitions.put(definitionName, new ObjectFieldSchema(finalFields, required)); - return new RefFieldSchema(String.format("#/definitions/%s", definitionName)); - } else { - return new ObjectFieldSchema(fields, required); - } + private RefFieldSchema createRefField(String definition) { + return new RefFieldSchema("#/definitions/%s".formatted(definition)); } private FieldSchema convertField(Descriptors.FieldDescriptor field, Map definitions) { + Optional wellKnownTypeSchema = convertProtoWellKnownTypes(field); + if (wellKnownTypeSchema.isPresent()) { + return wellKnownTypeSchema.get(); + } + if (field.isMapField()) { + return new MapFieldSchema(); + } final JsonType jsonType = convertType(field); - FieldSchema fieldSchema; if (jsonType.getType().equals(JsonType.Type.OBJECT)) { - fieldSchema = convertObjectSchema(field.getMessageType(), definitions, true); + fieldSchema = registerObjectAndReturnRef(field.getMessageType(), definitions); } else { fieldSchema = new SimpleFieldSchema(jsonType); } @@ -96,39 +115,87 @@ private FieldSchema convertField(Descriptors.FieldDescriptor field, } } + // converts Protobuf Well-known type (from google.protobuf.* packages) to Json-schema types + // see JsonFormat::buildWellKnownTypePrinters for impl details + private Optional convertProtoWellKnownTypes(Descriptors.FieldDescriptor field) { + // all well-known types are messages + if (field.getType() != Descriptors.FieldDescriptor.Type.MESSAGE) { + return Optional.empty(); + } + String typeName = field.getMessageType().getFullName(); + if (typeName.equals(Timestamp.getDescriptor().getFullName())) { + return Optional.of( + new SimpleFieldSchema( + new SimpleJsonType(JsonType.Type.STRING, Map.of("format", new TextNode("date-time"))))); + } + if (typeName.equals(Duration.getDescriptor().getFullName())) { + return Optional.of( + new SimpleFieldSchema( + //TODO: current UI is failing when format=duration is set - need to fix this first + new SimpleJsonType(JsonType.Type.STRING // , Map.of("format", new TextNode("duration")) + ))); + } + if (typeName.equals(FieldMask.getDescriptor().getFullName())) { + return Optional.of(new SimpleFieldSchema(new SimpleJsonType(JsonType.Type.STRING))); + } + if (typeName.equals(Any.getDescriptor().getFullName()) || typeName.equals(Struct.getDescriptor().getFullName())) { + return Optional.of(ObjectFieldSchema.EMPTY); + } + if (typeName.equals(Value.getDescriptor().getFullName())) { + return Optional.of(AnyFieldSchema.get()); + } + if (typeName.equals(ListValue.getDescriptor().getFullName())) { + return Optional.of(new ArrayFieldSchema(AnyFieldSchema.get())); + } + if (simpleTypesWrapperNames.contains(typeName)) { + return Optional.of(new SimpleFieldSchema( + convertType(requireNonNull(field.getMessageType().findFieldByName("value"))))); + } + return Optional.empty(); + } private JsonType convertType(Descriptors.FieldDescriptor field) { - switch (field.getType()) { - case INT32: - case INT64: - case SINT32: - case SINT64: - case UINT32: - case UINT64: - case FIXED32: - case FIXED64: - case SFIXED32: - case SFIXED64: - return new SimpleJsonType(JsonType.Type.INTEGER); - case MESSAGE: - case GROUP: - return new SimpleJsonType(JsonType.Type.OBJECT); - case ENUM: - return new EnumJsonType( - field.getEnumType().getValues().stream() - .map(Descriptors.EnumValueDescriptor::getName) - .collect(Collectors.toList()) - ); - case BYTES: - case STRING: - return new SimpleJsonType(JsonType.Type.STRING); - case FLOAT: - case DOUBLE: - return new SimpleJsonType(JsonType.Type.NUMBER); - case BOOL: - return new SimpleJsonType(JsonType.Type.BOOLEAN); - default: - return new SimpleJsonType(JsonType.Type.STRING); - } + return switch (field.getType()) { + case INT32, FIXED32, SFIXED32, SINT32 -> new SimpleJsonType( + JsonType.Type.INTEGER, + Map.of( + MAXIMUM, IntNode.valueOf(Integer.MAX_VALUE), + MINIMUM, IntNode.valueOf(Integer.MIN_VALUE) + ) + ); + case UINT32 -> new SimpleJsonType( + JsonType.Type.INTEGER, + Map.of( + MAXIMUM, LongNode.valueOf(UnsignedInteger.MAX_VALUE.longValue()), + MINIMUM, IntNode.valueOf(0) + ) + ); + //TODO: actually all *64 types will be printed with quotes (as strings), + // see JsonFormat::printSingleFieldValue for impl. This can cause problems when you copy-paste from messages + // table to `Produce` area - need to think if it is critical or not. + case INT64, FIXED64, SFIXED64, SINT64 -> new SimpleJsonType( + JsonType.Type.INTEGER, + Map.of( + MAXIMUM, LongNode.valueOf(Long.MAX_VALUE), + MINIMUM, LongNode.valueOf(Long.MIN_VALUE) + ) + ); + case UINT64 -> new SimpleJsonType( + JsonType.Type.INTEGER, + Map.of( + MAXIMUM, new BigIntegerNode(UnsignedLong.MAX_VALUE.bigIntegerValue()), + MINIMUM, LongNode.valueOf(0) + ) + ); + case MESSAGE, GROUP -> new SimpleJsonType(JsonType.Type.OBJECT); + case ENUM -> new EnumJsonType( + field.getEnumType().getValues().stream() + .map(Descriptors.EnumValueDescriptor::getName) + .collect(Collectors.toList()) + ); + case BYTES, STRING -> new SimpleJsonType(JsonType.Type.STRING); + case FLOAT, DOUBLE -> new SimpleJsonType(JsonType.Type.NUMBER); + case BOOL -> new SimpleJsonType(JsonType.Type.BOOLEAN); + }; } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/RefFieldSchema.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/RefFieldSchema.java index 82d544c6964..ca8e50a0878 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/RefFieldSchema.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/RefFieldSchema.java @@ -4,10 +4,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.TextNode; -public class RefFieldSchema implements FieldSchema { +class RefFieldSchema implements FieldSchema { private final String ref; - public RefFieldSchema(String ref) { + RefFieldSchema(String ref) { this.ref = ref; } @@ -15,4 +15,8 @@ public RefFieldSchema(String ref) { public JsonNode toJsonNode(ObjectMapper mapper) { return mapper.createObjectNode().set("$ref", new TextNode(ref)); } + + String getRef() { + return ref; + } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/SimpleFieldSchema.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/SimpleFieldSchema.java index 158cceb6bfe..339ab4cc863 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/SimpleFieldSchema.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/SimpleFieldSchema.java @@ -3,10 +3,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -public class SimpleFieldSchema implements FieldSchema { +class SimpleFieldSchema implements FieldSchema { private final JsonType type; - public SimpleFieldSchema(JsonType type) { + SimpleFieldSchema(JsonType type) { this.type = type; } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/SimpleJsonType.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/SimpleJsonType.java index 806ce95a9fe..b46d3407e3b 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/SimpleJsonType.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/SimpleJsonType.java @@ -3,19 +3,27 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.TextNode; +import com.google.common.collect.ImmutableMap; import java.util.Map; -public class SimpleJsonType extends JsonType { +class SimpleJsonType extends JsonType { - public SimpleJsonType(Type type) { + private final Map additionalTypeProperties; + + SimpleJsonType(Type type) { + this(type, Map.of()); + } + + SimpleJsonType(Type type, Map additionalTypeProperties) { super(type); + this.additionalTypeProperties = additionalTypeProperties; } @Override public Map toJsonNode(ObjectMapper mapper) { - return Map.of( - "type", - new TextNode(type.getName()) - ); + return ImmutableMap.builder() + .put("type", new TextNode(type.getName())) + .putAll(additionalTypeProperties) + .build(); } } diff --git a/kafka-ui-api/src/main/resources/application-gauth.yml b/kafka-ui-api/src/main/resources/application-gauth.yml deleted file mode 100644 index 877d773fcfe..00000000000 --- a/kafka-ui-api/src/main/resources/application-gauth.yml +++ /dev/null @@ -1,10 +0,0 @@ -auth: - type: OAUTH2 -spring: - security: - oauth2: - client: - registration: - google: - client-id: [put your client id here] - client-secret: [put your client secret here] diff --git a/kafka-ui-api/src/main/resources/application-local.yml b/kafka-ui-api/src/main/resources/application-local.yml index cef37e15dee..7848f1fdc49 100644 --- a/kafka-ui-api/src/main/resources/application-local.yml +++ b/kafka-ui-api/src/main/resources/application-local.yml @@ -1,34 +1,149 @@ +logging: + level: + root: INFO + com.provectus: DEBUG + #org.springframework.http.codec.json.Jackson2JsonEncoder: DEBUG + #org.springframework.http.codec.json.Jackson2JsonDecoder: DEBUG + reactor.netty.http.server.AccessLog: INFO + org.springframework.security: DEBUG + +#server: +# port: 8080 #- Port in which kafka-ui will run. + +spring: + jmx: + enabled: true + ldap: + urls: ldap://localhost:10389 + base: "cn={0},ou=people,dc=planetexpress,dc=com" + admin-user: "cn=admin,dc=planetexpress,dc=com" + admin-password: "GoodNewsEveryone" + user-filter-search-base: "dc=planetexpress,dc=com" + user-filter-search-filter: "(&(uid={0})(objectClass=inetOrgPerson))" + group-filter-search-base: "ou=people,dc=planetexpress,dc=com" + kafka: clusters: - name: local bootstrapServers: localhost:9092 - zookeeper: localhost:2181 schemaRegistry: http://localhost:8085 ksqldbServer: http://localhost:8088 kafkaConnect: - name: first address: http://localhost:8083 - jmxPort: 9997 - # - - # name: secondLocal - # bootstrapServers: localhost:9093 - # zookeeper: localhost:2182 - # schemaRegistry: http://localhost:18085 - # kafkaConnect: - # - name: first - # address: http://localhost:8083 - # jmxPort: 9998 - # read-only: true - # - - # name: localUsingProtobufFile - # bootstrapServers: localhost:9092 - # protobufFile: messages.proto - # protobufMessageName: GenericMessage - # protobufMessageNameByTopic: - # input-topic: InputMessage - # output-topic: OutputMessage -spring: - jmx: - enabled: true + metrics: + port: 9997 + type: JMX + +dynamic.config.enabled: true + +oauth2: + ldap: + activeDirectory: false + aсtiveDirectory.domain: domain.com + auth: type: DISABLED + # type: OAUTH2 + # type: LDAP + oauth2: + client: + cognito: + clientId: # CLIENT ID + clientSecret: # CLIENT SECRET + scope: openid + client-name: cognito + provider: cognito + redirect-uri: http://localhost:8080/login/oauth2/code/cognito + authorization-grant-type: authorization_code + issuer-uri: https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_M7cIUn1nj + jwk-set-uri: https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_M7cIUn1nj/.well-known/jwks.json + user-name-attribute: cognito:username + custom-params: + type: cognito + logoutUrl: https://kafka-ui.auth.eu-central-1.amazoncognito.com/logout + google: + provider: google + clientId: # CLIENT ID + clientSecret: # CLIENT SECRET + user-name-attribute: email + custom-params: + type: google + allowedDomain: provectus.com + github: + provider: github + clientId: # CLIENT ID + clientSecret: # CLIENT SECRET + scope: + - read:org + user-name-attribute: login + custom-params: + type: github + +rbac: + roles: + - name: "memelords" + clusters: + - local + subjects: + - provider: oauth_google + type: domain + value: "provectus.com" + - provider: oauth_google + type: user + value: "name@provectus.com" + + - provider: oauth_github + type: organization + value: "provectus" + - provider: oauth_github + type: user + value: "memelord" + + - provider: oauth_cognito + type: user + value: "username" + - provider: oauth_cognito + type: group + value: "memelords" + + - provider: ldap + type: group + value: "admin_staff" + + # NOT IMPLEMENTED YET + # - provider: ldap_ad + # type: group + # value: "admin_staff" + + permissions: + - resource: applicationconfig + actions: all + + - resource: clusterconfig + actions: all + + - resource: topic + value: ".*" + actions: all + + - resource: consumer + value: ".*" + actions: all + + - resource: schema + value: ".*" + actions: all + + - resource: connect + value: "*" + actions: all + + - resource: ksql + actions: all + + - resource: acl + actions: all + + - resource: audit + actions: all diff --git a/kafka-ui-api/src/main/resources/application-sdp.yml b/kafka-ui-api/src/main/resources/application-sdp.yml deleted file mode 100644 index f8d4445fedb..00000000000 --- a/kafka-ui-api/src/main/resources/application-sdp.yml +++ /dev/null @@ -1,13 +0,0 @@ -kafka: - clusters: - - name: local - bootstrapServers: b-1.kad-msk.57w67o.c6.kafka.eu-central-1.amazonaws.com:9094 - properties: - security.protocol: SSL -# zookeeper: localhost:2181 -# schemaRegistry: http://kad-ecs-application-lb-857515197.eu-west-1.elb.amazonaws.com:9000/api/schema-registry - # - - # name: secondLocal - # zookeeper: zookeeper1:2181 - # bootstrapServers: kafka1:29092 - # schemaRegistry: http://schemaregistry1:8085 diff --git a/kafka-ui-api/src/main/resources/application.yml b/kafka-ui-api/src/main/resources/application.yml index a6a4c8e9716..e8799206132 100644 --- a/kafka-ui-api/src/main/resources/application.yml +++ b/kafka-ui-api/src/main/resources/application.yml @@ -10,16 +10,12 @@ management: endpoints: web: exposure: - include: "info,health" - health: - ldap: - enabled: false + include: "info,health,prometheus" logging: level: root: INFO com.provectus: DEBUG - #org.springframework.http.codec.json.Jackson2JsonEncoder: DEBUG - #org.springframework.http.codec.json.Jackson2JsonDecoder: DEBUG reactor.netty.http.server.AccessLog: INFO + org.hibernate.validator: WARN diff --git a/kafka-ui-api/src/main/resources/logback-spring.xml b/kafka-ui-api/src/main/resources/logback-spring.xml index a0705128172..a33692146bb 100644 --- a/kafka-ui-api/src/main/resources/logback-spring.xml +++ b/kafka-ui-api/src/main/resources/logback-spring.xml @@ -1,17 +1,14 @@ - - - - %black(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%c{1}): %msg%n%throwable - - + + + %black(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%c{1}): %msg%n%throwable + - + diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/AbstractIntegrationTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/AbstractIntegrationTest.java index 5de2b93baac..d185e646714 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/AbstractIntegrationTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/AbstractIntegrationTest.java @@ -1,15 +1,17 @@ package com.provectus.kafka.ui; import com.provectus.kafka.ui.container.KafkaConnectContainer; +import com.provectus.kafka.ui.container.KsqlDbContainer; import com.provectus.kafka.ui.container.SchemaRegistryContainer; +import java.nio.file.Path; import java.util.List; import java.util.Properties; import org.apache.kafka.clients.admin.AdminClient; import org.apache.kafka.clients.admin.AdminClientConfig; import org.apache.kafka.clients.admin.NewTopic; import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.function.ThrowingConsumer; +import org.junit.jupiter.api.io.TempDir; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.context.SpringBootTest; @@ -17,7 +19,7 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; -import org.springframework.util.SocketUtils; +import org.springframework.test.util.TestSocketUtils; import org.testcontainers.containers.KafkaContainer; import org.testcontainers.containers.Network; import org.testcontainers.utility.DockerImageName; @@ -31,7 +33,7 @@ public abstract class AbstractIntegrationTest { public static final String LOCAL = "local"; public static final String SECOND_LOCAL = "secondLocal"; - private static final String CONFLUENT_PLATFORM_VERSION = "5.5.0"; + private static final String CONFLUENT_PLATFORM_VERSION = "7.2.1"; // Append ".arm64" for a local run public static final KafkaContainer kafka = new KafkaContainer( DockerImageName.parse("confluentinc/cp-kafka").withTag(CONFLUENT_PLATFORM_VERSION)) @@ -48,6 +50,14 @@ public abstract class AbstractIntegrationTest { .dependsOn(kafka) .dependsOn(schemaRegistry); + protected static final KsqlDbContainer KSQL_DB = new KsqlDbContainer( + DockerImageName.parse("confluentinc/cp-ksqldb-server") + .withTag(CONFLUENT_PLATFORM_VERSION)) + .withKafka(kafka); + + @TempDir + public static Path tmpDir; + static { kafka.start(); schemaRegistry.start(); @@ -62,11 +72,18 @@ public void initialize(@NotNull ConfigurableApplicationContext context) { System.setProperty("kafka.clusters.0.bootstrapServers", kafka.getBootstrapServers()); // List unavailable hosts to verify failover System.setProperty("kafka.clusters.0.schemaRegistry", String.format("http://localhost:%1$s,http://localhost:%1$s,%2$s", - SocketUtils.findAvailableTcpPort(), schemaRegistry.getUrl())); + TestSocketUtils.findAvailableTcpPort(), schemaRegistry.getUrl())); System.setProperty("kafka.clusters.0.kafkaConnect.0.name", "kafka-connect"); System.setProperty("kafka.clusters.0.kafkaConnect.0.userName", "kafka-connect"); System.setProperty("kafka.clusters.0.kafkaConnect.0.password", "kafka-connect"); System.setProperty("kafka.clusters.0.kafkaConnect.0.address", kafkaConnect.getTarget()); + System.setProperty("kafka.clusters.0.kafkaConnect.1.name", "notavailable"); + System.setProperty("kafka.clusters.0.kafkaConnect.1.address", "http://notavailable:6666"); + System.setProperty("kafka.clusters.0.masking.0.type", "REPLACE"); + System.setProperty("kafka.clusters.0.masking.0.replacement", "***"); + System.setProperty("kafka.clusters.0.masking.0.topicValuesPattern", "masking-test-.*"); + System.setProperty("kafka.clusters.0.audit.topicAuditEnabled", "true"); + System.setProperty("kafka.clusters.0.audit.consoleAuditEnabled", "true"); System.setProperty("kafka.clusters.1.name", SECOND_LOCAL); System.setProperty("kafka.clusters.1.readOnly", "true"); @@ -74,6 +91,9 @@ public void initialize(@NotNull ConfigurableApplicationContext context) { System.setProperty("kafka.clusters.1.schemaRegistry", schemaRegistry.getUrl()); System.setProperty("kafka.clusters.1.kafkaConnect.0.name", "kafka-connect"); System.setProperty("kafka.clusters.1.kafkaConnect.0.address", kafkaConnect.getTarget()); + + System.setProperty("dynamic.config.enabled", "true"); + System.setProperty("config.related.uploads.dir", tmpDir.toString()); } } diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaConnectServiceTests.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaConnectServiceTests.java index e886a0884d7..a8271835219 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaConnectServiceTests.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaConnectServiceTests.java @@ -1,6 +1,7 @@ package com.provectus.kafka.ui; import static java.util.function.Predicate.not; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import com.provectus.kafka.ui.model.ConnectorDTO; @@ -141,9 +142,8 @@ public void shouldListConnectors() { .uri("/api/clusters/{clusterName}/connects/{connectName}/connectors", LOCAL, connectName) .exchange() .expectStatus().isOk() - .expectBody() - .jsonPath(String.format("$[?(@ == '%s')]", connectorName)) - .exists(); + .expectBodyList(String.class) + .contains(connectorName); } @Test @@ -335,7 +335,7 @@ public void shouldRetrieveConnectorPlugins() { .exchange() .expectStatus().isOk() .expectBodyList(ConnectorPluginDTO.class) - .value(plugins -> assertEquals(14, plugins.size())); + .value(plugins -> assertThat(plugins.size()).isGreaterThan(0)); } @Test diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaConsumerGroupTests.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaConsumerGroupTests.java index 17e7a19ee7f..98f8394060f 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaConsumerGroupTests.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaConsumerGroupTests.java @@ -14,6 +14,7 @@ import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; import lombok.val; +import org.apache.commons.lang3.RandomStringUtils; import org.apache.kafka.clients.admin.NewTopic; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.KafkaConsumer; @@ -126,6 +127,21 @@ void shouldReturnConsumerGroupsWithPagination() throws Exception { assertThat(page.getConsumerGroups()) .isSortedAccordingTo(Comparator.comparing(ConsumerGroupDTO::getGroupId).reversed()); }); + + webTestClient + .get() + .uri("/api/clusters/{clusterName}/consumer-groups/paged?perPage=10&&search" + + "=cgPageTest&orderBy=MEMBERS&sortOrder=DESC", LOCAL) + .exchange() + .expectStatus() + .isOk() + .expectBody(ConsumerGroupsPageResponseDTO.class) + .value(page -> { + assertThat(page.getPageCount()).isEqualTo(1); + assertThat(page.getConsumerGroups().size()).isEqualTo(5); + assertThat(page.getConsumerGroups()) + .isSortedAccordingTo(Comparator.comparing(ConsumerGroupDTO::getMembers).reversed()); + }); } } @@ -133,7 +149,7 @@ private Closeable startConsumerGroups(int count, String consumerGroupPrefix) { String topicName = createTopicWithRandomName(); var consumers = Stream.generate(() -> { - String groupId = consumerGroupPrefix + UUID.randomUUID(); + String groupId = consumerGroupPrefix + RandomStringUtils.randomAlphabetic(5); val consumer = createTestConsumerWithGroupId(groupId); consumer.subscribe(List.of(topicName)); consumer.poll(Duration.ofMillis(100)); diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaConsumerTests.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaConsumerTests.java index d248edf5e8c..ff11aa6656a 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaConsumerTests.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaConsumerTests.java @@ -3,10 +3,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.http.MediaType.TEXT_EVENT_STREAM; -import com.provectus.kafka.ui.api.model.TopicConfig; import com.provectus.kafka.ui.model.BrokerConfigDTO; import com.provectus.kafka.ui.model.PartitionsIncreaseDTO; import com.provectus.kafka.ui.model.PartitionsIncreaseResponseDTO; +import com.provectus.kafka.ui.model.TopicConfigDTO; import com.provectus.kafka.ui.model.TopicCreationDTO; import com.provectus.kafka.ui.model.TopicDetailsDTO; import com.provectus.kafka.ui.model.TopicMessageEventDTO; @@ -206,12 +206,12 @@ public void shouldRetrieveTopicConfig() { .expectStatus() .isOk(); - List configs = webTestClient.get() + List configs = webTestClient.get() .uri("/api/clusters/{clusterName}/topics/{topicName}/config", LOCAL, topicName) .exchange() .expectStatus() .isOk() - .expectBodyList(TopicConfig.class) + .expectBodyList(TopicConfigDTO.class) .returnResult() .getResponseBody(); diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/SchemaRegistryServiceTests.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/SchemaRegistryServiceTests.java index 190831da98b..5fa9aee7667 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/SchemaRegistryServiceTests.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/SchemaRegistryServiceTests.java @@ -2,6 +2,7 @@ import com.provectus.kafka.ui.model.CompatibilityLevelDTO; import com.provectus.kafka.ui.model.NewSchemaSubjectDTO; +import com.provectus.kafka.ui.model.SchemaReferenceDTO; import com.provectus.kafka.ui.model.SchemaSubjectDTO; import com.provectus.kafka.ui.model.SchemaSubjectsResponseDTO; import com.provectus.kafka.ui.model.SchemaTypeDTO; @@ -190,6 +191,58 @@ void shouldCreateNewProtobufSchema() { Assertions.assertEquals(schema, actual.getSchema()); } + + @Test + void shouldCreateNewProtobufSchemaWithRefs() { + NewSchemaSubjectDTO requestBody = new NewSchemaSubjectDTO() + .schemaType(SchemaTypeDTO.PROTOBUF) + .subject(subject + "-ref") + .schema(""" + syntax = "proto3"; + message MyRecord { + int32 id = 1; + string name = 2; + } + """); + + webTestClient + .post() + .uri("/api/clusters/{clusterName}/schemas", LOCAL) + .contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromPublisher(Mono.just(requestBody), NewSchemaSubjectDTO.class)) + .exchange() + .expectStatus() + .isOk(); + + requestBody = new NewSchemaSubjectDTO() + .schemaType(SchemaTypeDTO.PROTOBUF) + .subject(subject) + .schema(""" + syntax = "proto3"; + import "MyRecord.proto"; + message MyRecordWithRef { + int32 id = 1; + MyRecord my_ref = 2; + } + """) + .references(List.of(new SchemaReferenceDTO().name("MyRecord.proto").subject(subject + "-ref").version(1))); + + SchemaSubjectDTO actual = webTestClient + .post() + .uri("/api/clusters/{clusterName}/schemas", LOCAL) + .contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromPublisher(Mono.just(requestBody), NewSchemaSubjectDTO.class)) + .exchange() + .expectStatus() + .isOk() + .expectBody(SchemaSubjectDTO.class) + .returnResult() + .getResponseBody(); + + Assertions.assertNotNull(actual); + Assertions.assertEquals(requestBody.getReferences(), actual.getReferences()); + } + @Test public void shouldReturnBackwardAsGlobalCompatibilityLevelByDefault() { webTestClient @@ -274,6 +327,21 @@ public void shouldOkWhenCreateNewSchemaThenGetAndUpdateItsCompatibilityLevel() { }); } + @Test + void shouldCreateNewSchemaWhenSubjectIncludesNonAsciiCharacters() { + String schema = + "{\"subject\":\"test/test\",\"schemaType\":\"JSON\",\"schema\":" + + "\"{\\\"type\\\": \\\"string\\\"}\"}"; + + webTestClient + .post() + .uri("/api/clusters/{clusterName}/schemas", LOCAL) + .contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(schema)) + .exchange() + .expectStatus().isOk(); + } + private void createNewSubjectAndAssert(String subject) { webTestClient .post() diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/container/KafkaConnectContainer.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/container/KafkaConnectContainer.java index dd8d5d03c41..00ed50cc200 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/container/KafkaConnectContainer.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/container/KafkaConnectContainer.java @@ -12,6 +12,7 @@ public class KafkaConnectContainer extends GenericContainer> generateBody(ClassPathResource resource) { + MultipartBodyBuilder builder = new MultipartBodyBuilder(); + builder.part("file", resource); + return builder.build(); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/MessageFiltersTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/MessageFiltersTest.java index ded921bfcd9..4e9f5034cd2 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/MessageFiltersTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/MessageFiltersTest.java @@ -13,6 +13,7 @@ import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.function.Predicate; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.Nested; @@ -73,6 +74,20 @@ void canCheckPartition() { assertFalse(f.test(msg().partition(0))); } + @Test + void canCheckOffset() { + var f = groovyScriptFilter("offset == 100"); + assertTrue(f.test(msg().offset(100L))); + assertFalse(f.test(msg().offset(200L))); + } + + @Test + void canCheckHeaders() { + var f = groovyScriptFilter("headers.size() == 2 && headers['k1'] == 'v1'"); + assertTrue(f.test(msg().headers(Map.of("k1", "v1", "k2", "v2")))); + assertFalse(f.test(msg().headers(Map.of("k1", "unexpected", "k2", "v2")))); + } + @Test void canCheckTimestampMs() { var ts = OffsetDateTime.now(); @@ -103,10 +118,18 @@ void canCheckKeyAsJsonObjectIfItCanBeParsedToJson() { } @Test - void keySetToNullIfKeyCantBeParsedToJson() { - var f = groovyScriptFilter("key == null"); + void keySetToKeyStringIfCantBeParsedToJson() { + var f = groovyScriptFilter("key == \"not json\""); assertTrue(f.test(msg().key("not json"))); - assertFalse(f.test(msg().key("{ \"k\" : \"v\" }"))); + } + + @Test + void keyAndKeyAsTextSetToNullIfRecordsKeyIsNull() { + var f = groovyScriptFilter("key == null"); + assertTrue(f.test(msg().key(null))); + + f = groovyScriptFilter("keyAsText == null"); + assertTrue(f.test(msg().key(null))); } @Test @@ -117,10 +140,18 @@ void canCheckValueAsJsonObjectIfItCanBeParsedToJson() { } @Test - void valueSetToNullIfKeyCantBeParsedToJson() { - var f = groovyScriptFilter("value == null"); + void valueSetToContentStringIfCantBeParsedToJson() { + var f = groovyScriptFilter("value == \"not json\""); assertTrue(f.test(msg().content("not json"))); - assertFalse(f.test(msg().content("{ \"k\" : \"v\" }"))); + } + + @Test + void valueAndValueAsTextSetToNullIfRecordsContentIsNull() { + var f = groovyScriptFilter("value == null"); + assertTrue(f.test(msg().content(null))); + + f = groovyScriptFilter("valueAsText == null"); + assertTrue(f.test(msg().content(null))); } @Test @@ -140,7 +171,7 @@ void canRunMultiStatementScripts() { @Test - void filterSpeedIsAtLeast10kPerSec() { + void filterSpeedIsAtLeast5kPerSec() { var f = groovyScriptFilter("value.name.first == 'user1' && keyAsText.startsWith('a') "); List toFilter = new ArrayList<>(); @@ -159,7 +190,7 @@ void filterSpeedIsAtLeast10kPerSec() { long matched = toFilter.stream().filter(f).count(); long took = System.currentTimeMillis() - before; - assertThat(took).isLessThan(500); + assertThat(took).isLessThan(1000); assertThat(matched).isGreaterThan(0); } } @@ -170,4 +201,4 @@ private TopicMessageDTO msg() { .partition(1); } -} \ No newline at end of file +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/MessagesProcessingTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/MessagesProcessingTest.java new file mode 100644 index 00000000000..245dd095f68 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/MessagesProcessingTest.java @@ -0,0 +1,69 @@ +package com.provectus.kafka.ui.emitter; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.common.header.internals.RecordHeaders; +import org.apache.kafka.common.record.TimestampType; +import org.apache.kafka.common.utils.Bytes; +import org.junit.jupiter.api.RepeatedTest; + +class MessagesProcessingTest { + + + @RepeatedTest(5) + void testSortingAsc() { + var messagesInOrder = List.of( + consumerRecord(1, 100L, "1999-01-01T00:00:00+00:00"), + consumerRecord(0, 0L, "2000-01-01T00:00:00+00:00"), + consumerRecord(1, 200L, "2000-01-05T00:00:00+00:00"), + consumerRecord(0, 10L, "2000-01-10T00:00:00+00:00"), + consumerRecord(0, 20L, "2000-01-20T00:00:00+00:00"), + consumerRecord(1, 300L, "3000-01-01T00:00:00+00:00"), + consumerRecord(2, 1000L, "4000-01-01T00:00:00+00:00"), + consumerRecord(2, 1001L, "2000-01-01T00:00:00+00:00"), + consumerRecord(2, 1003L, "3000-01-01T00:00:00+00:00") + ); + + var shuffled = new ArrayList<>(messagesInOrder); + Collections.shuffle(shuffled); + + var sortedList = MessagesProcessing.sortForSending(shuffled, true); + assertThat(sortedList).containsExactlyElementsOf(messagesInOrder); + } + + @RepeatedTest(5) + void testSortingDesc() { + var messagesInOrder = List.of( + consumerRecord(1, 300L, "3000-01-01T00:00:00+00:00"), + consumerRecord(2, 1003L, "3000-01-01T00:00:00+00:00"), + consumerRecord(0, 20L, "2000-01-20T00:00:00+00:00"), + consumerRecord(0, 10L, "2000-01-10T00:00:00+00:00"), + consumerRecord(1, 200L, "2000-01-05T00:00:00+00:00"), + consumerRecord(0, 0L, "2000-01-01T00:00:00+00:00"), + consumerRecord(2, 1001L, "2000-01-01T00:00:00+00:00"), + consumerRecord(2, 1000L, "4000-01-01T00:00:00+00:00"), + consumerRecord(1, 100L, "1999-01-01T00:00:00+00:00") + ); + + var shuffled = new ArrayList<>(messagesInOrder); + Collections.shuffle(shuffled); + + var sortedList = MessagesProcessing.sortForSending(shuffled, false); + assertThat(sortedList).containsExactlyElementsOf(messagesInOrder); + } + + private ConsumerRecord consumerRecord(int partition, long offset, String ts) { + return new ConsumerRecord<>( + "topic", partition, offset, OffsetDateTime.parse(ts).toInstant().toEpochMilli(), + TimestampType.CREATE_TIME, + 0, 0, null, null, new RecordHeaders(), Optional.empty() + ); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/OffsetsInfoTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/OffsetsInfoTest.java new file mode 100644 index 00000000000..156f62846bf --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/OffsetsInfoTest.java @@ -0,0 +1,53 @@ +package com.provectus.kafka.ui.emitter; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.kafka.clients.consumer.MockConsumer; +import org.apache.kafka.clients.consumer.OffsetResetStrategy; +import org.apache.kafka.common.PartitionInfo; +import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.common.utils.Bytes; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class OffsetsInfoTest { + + final String topic = "test"; + final TopicPartition tp0 = new TopicPartition(topic, 0); //offsets: start 0, end 0 + final TopicPartition tp1 = new TopicPartition(topic, 1); //offsets: start 10, end 10 + final TopicPartition tp2 = new TopicPartition(topic, 2); //offsets: start 0, end 20 + final TopicPartition tp3 = new TopicPartition(topic, 3); //offsets: start 25, end 30 + + MockConsumer consumer; + + @BeforeEach + void initMockConsumer() { + consumer = new MockConsumer<>(OffsetResetStrategy.EARLIEST); + consumer.updatePartitions( + topic, + Stream.of(tp0, tp1, tp2, tp3) + .map(tp -> new PartitionInfo(topic, tp.partition(), null, null, null, null)) + .collect(Collectors.toList())); + consumer.updateBeginningOffsets(Map.of(tp0, 0L, tp1, 10L, tp2, 0L, tp3, 25L)); + consumer.updateEndOffsets(Map.of(tp0, 0L, tp1, 10L, tp2, 20L, tp3, 30L)); + } + + @Test + void fillsInnerFieldsAccordingToTopicState() { + var offsets = new OffsetsInfo(consumer, List.of(tp0, tp1, tp2, tp3)); + + assertThat(offsets.getBeginOffsets()).containsEntry(tp0, 0L).containsEntry(tp1, 10L).containsEntry(tp2, 0L) + .containsEntry(tp3, 25L); + + assertThat(offsets.getEndOffsets()).containsEntry(tp0, 0L).containsEntry(tp1, 10L).containsEntry(tp2, 20L) + .containsEntry(tp3, 30L); + + assertThat(offsets.getEmptyPartitions()).contains(tp0, tp1); + assertThat(offsets.getNonEmptyPartitions()).contains(tp2, tp3); + } + +} \ No newline at end of file diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/SeekOperationsTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/SeekOperationsTest.java new file mode 100644 index 00000000000..affa423123c --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/SeekOperationsTest.java @@ -0,0 +1,88 @@ +package com.provectus.kafka.ui.emitter; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.provectus.kafka.ui.model.SeekTypeDTO; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.kafka.clients.consumer.MockConsumer; +import org.apache.kafka.clients.consumer.OffsetResetStrategy; +import org.apache.kafka.common.PartitionInfo; +import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.common.utils.Bytes; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class SeekOperationsTest { + + final String topic = "test"; + final TopicPartition tp0 = new TopicPartition(topic, 0); //offsets: start 0, end 0 + final TopicPartition tp1 = new TopicPartition(topic, 1); //offsets: start 10, end 10 + final TopicPartition tp2 = new TopicPartition(topic, 2); //offsets: start 0, end 20 + final TopicPartition tp3 = new TopicPartition(topic, 3); //offsets: start 25, end 30 + + MockConsumer consumer; + + @BeforeEach + void initMockConsumer() { + consumer = new MockConsumer<>(OffsetResetStrategy.EARLIEST); + consumer.updatePartitions( + topic, + Stream.of(tp0, tp1, tp2, tp3) + .map(tp -> new PartitionInfo(topic, tp.partition(), null, null, null, null)) + .collect(Collectors.toList())); + consumer.updateBeginningOffsets(Map.of(tp0, 0L, tp1, 10L, tp2, 0L, tp3, 25L)); + consumer.updateEndOffsets(Map.of(tp0, 0L, tp1, 10L, tp2, 20L, tp3, 30L)); + } + + @Nested + class GetOffsetsForSeek { + + @Test + void latest() { + var offsets = SeekOperations.getOffsetsForSeek( + consumer, + new OffsetsInfo(consumer, topic), + SeekTypeDTO.LATEST, + null + ); + assertThat(offsets).containsExactlyInAnyOrderEntriesOf(Map.of(tp2, 20L, tp3, 30L)); + } + + @Test + void beginning() { + var offsets = SeekOperations.getOffsetsForSeek( + consumer, + new OffsetsInfo(consumer, topic), + SeekTypeDTO.BEGINNING, + null + ); + assertThat(offsets).containsExactlyInAnyOrderEntriesOf(Map.of(tp2, 0L, tp3, 25L)); + } + + @Test + void offsets() { + var offsets = SeekOperations.getOffsetsForSeek( + consumer, + new OffsetsInfo(consumer, topic), + SeekTypeDTO.OFFSET, + Map.of(tp1, 10L, tp2, 10L, tp3, 26L) + ); + assertThat(offsets).containsExactlyInAnyOrderEntriesOf(Map.of(tp2, 10L, tp3, 26L)); + } + + @Test + void offsetsWithBoundsFixing() { + var offsets = SeekOperations.getOffsetsForSeek( + consumer, + new OffsetsInfo(consumer, topic), + SeekTypeDTO.OFFSET, + Map.of(tp1, 10L, tp2, 21L, tp3, 24L) + ); + assertThat(offsets).containsExactlyInAnyOrderEntriesOf(Map.of(tp2, 20L, tp3, 25L)); + } + } + +} \ No newline at end of file diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/TailingEmitterTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/TailingEmitterTest.java index 9cdf28128a5..2798bd213fe 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/TailingEmitterTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/TailingEmitterTest.java @@ -111,10 +111,13 @@ private Flux createTailingFlux( return applicationContext.getBean(MessagesService.class) .loadMessages(cluster, topicName, - new ConsumerPosition(SeekTypeDTO.LATEST, Map.of(), SeekDirectionDTO.TAILING), + new ConsumerPosition(SeekTypeDTO.LATEST, topic, null), query, MessageFilterTypeDTO.STRING_CONTAINS, - 0); + 0, + SeekDirectionDTO.TAILING, + "String", + "String"); } private List startTailing(String filterQuery) { @@ -135,9 +138,9 @@ private void waitUntilTailingInitialized(List fluxOutput) Awaitility.await() .pollInSameThread() .pollDelay(Duration.ofMillis(100)) - .atMost(Duration.ofSeconds(10)) + .atMost(Duration.ofSeconds(200)) .until(() -> fluxOutput.stream() .anyMatch(msg -> msg.getType() == TopicMessageEventDTO.TypeEnum.CONSUMING)); } -} \ No newline at end of file +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/model/FailoverUrlListTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/model/FailoverUrlListTest.java deleted file mode 100644 index 5cbbaf73539..00000000000 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/model/FailoverUrlListTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.provectus.kafka.ui.model; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - - -class FailoverUrlListTest { - - public static final int RETRY_GRACE_PERIOD_IN_MS = 10; - - @Nested - @SuppressWarnings("all") - class ShouldHaveFailoverAvailableWhen { - - private FailoverUrlList failoverUrlList; - - @BeforeEach - void before() { - failoverUrlList = new FailoverUrlList(List.of("localhost:123", "farawayhost:5678"), RETRY_GRACE_PERIOD_IN_MS); - } - - @Test - void thereAreNoFailures() { - assertThat(failoverUrlList.isFailoverAvailable()).isTrue(); - } - - @Test - void withLessFailuresThenAvailableUrls() { - failoverUrlList.fail(failoverUrlList.current()); - - assertThat(failoverUrlList.isFailoverAvailable()).isTrue(); - } - - @Test - void withAllFailuresAndAtLeastOneAfterTheGraceTimeoutPeriod() throws InterruptedException { - failoverUrlList.fail(failoverUrlList.current()); - failoverUrlList.fail(failoverUrlList.current()); - - Thread.sleep(RETRY_GRACE_PERIOD_IN_MS + 1); - - assertThat(failoverUrlList.isFailoverAvailable()).isTrue(); - } - - @Nested - @SuppressWarnings("all") - class ShouldNotHaveFailoverAvailableWhen { - - private FailoverUrlList failoverUrlList; - - @BeforeEach - void before() { - failoverUrlList = new FailoverUrlList(List.of("localhost:123", "farawayhost:5678"), 1000); - } - - @Test - void allFailuresWithinGracePeriod() { - failoverUrlList.fail(failoverUrlList.current()); - failoverUrlList.fail(failoverUrlList.current()); - - assertThat(failoverUrlList.isFailoverAvailable()).isFalse(); - } - } - } -} - diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/model/PartitionDistributionStatsTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/model/PartitionDistributionStatsTest.java new file mode 100644 index 00000000000..c83c4f5cd86 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/model/PartitionDistributionStatsTest.java @@ -0,0 +1,83 @@ +package com.provectus.kafka.ui.model; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.provectus.kafka.ui.service.ReactiveAdminClient; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.kafka.clients.admin.TopicDescription; +import org.apache.kafka.common.Node; +import org.apache.kafka.common.TopicPartitionInfo; +import org.assertj.core.data.Percentage; +import org.junit.jupiter.api.Test; + +class PartitionDistributionStatsTest { + + @Test + void skewCalculatedBasedOnPartitionsCounts() { + Node n1 = new Node(1, "n1", 9092); + Node n2 = new Node(2, "n2", 9092); + Node n3 = new Node(3, "n3", 9092); + Node n4 = new Node(4, "n4", 9092); + + var stats = PartitionDistributionStats.create( + Statistics.builder() + .clusterDescription( + new ReactiveAdminClient.ClusterDescription(null, "test", Set.of(n1, n2, n3), null)) + .topicDescriptions( + Map.of( + "t1", new TopicDescription( + "t1", false, + List.of( + new TopicPartitionInfo(0, n1, List.of(n1, n2), List.of(n1, n2)), + new TopicPartitionInfo(1, n2, List.of(n2, n3), List.of(n2, n3)) + ) + ), + "t2", new TopicDescription( + "t2", false, + List.of( + new TopicPartitionInfo(0, n1, List.of(n1, n2), List.of(n1, n2)), + new TopicPartitionInfo(1, null, List.of(n2, n1), List.of(n1)) + ) + ) + ) + ) + .build(), 4 + ); + + assertThat(stats.getPartitionLeaders()) + .containsExactlyInAnyOrderEntriesOf(Map.of(n1, 2, n2, 1)); + assertThat(stats.getPartitionsCount()) + .containsExactlyInAnyOrderEntriesOf(Map.of(n1, 3, n2, 4, n3, 1)); + assertThat(stats.getInSyncPartitions()) + .containsExactlyInAnyOrderEntriesOf(Map.of(n1, 3, n2, 3, n3, 1)); + + // Node(partitions): n1(3), n2(4), n3(1), n4(0) + // average partitions cnt = (3+4+1) / 3 = 2.666 (counting only nodes with partitions!) + assertThat(stats.getAvgPartitionsPerBroker()) + .isCloseTo(2.666, Percentage.withPercentage(1)); + + assertThat(stats.partitionsSkew(n1)) + .isCloseTo(BigDecimal.valueOf(12.5), Percentage.withPercentage(1)); + assertThat(stats.partitionsSkew(n2)) + .isCloseTo(BigDecimal.valueOf(50), Percentage.withPercentage(1)); + assertThat(stats.partitionsSkew(n3)) + .isCloseTo(BigDecimal.valueOf(-62.5), Percentage.withPercentage(1)); + assertThat(stats.partitionsSkew(n4)) + .isCloseTo(BigDecimal.valueOf(-100), Percentage.withPercentage(1)); + + // Node(leaders): n1(2), n2(1), n3(0), n4(0) + // average leaders cnt = (2+1) / 2 = 1.5 (counting only nodes with leaders!) + assertThat(stats.leadersSkew(n1)) + .isCloseTo(BigDecimal.valueOf(33.33), Percentage.withPercentage(1)); + assertThat(stats.leadersSkew(n2)) + .isCloseTo(BigDecimal.valueOf(-33.33), Percentage.withPercentage(1)); + assertThat(stats.leadersSkew(n3)) + .isCloseTo(BigDecimal.valueOf(-100), Percentage.withPercentage(1)); + assertThat(stats.leadersSkew(n4)) + .isCloseTo(BigDecimal.valueOf(-100), Percentage.withPercentage(1)); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/producer/KafkaTestProducer.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/producer/KafkaTestProducer.java index c8924188431..eaceb9ef245 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/producer/KafkaTestProducer.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/producer/KafkaTestProducer.java @@ -2,7 +2,6 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.clients.producer.ProducerRecord; diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serde/ProtobufFileRecordSerDeTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serde/ProtobufFileRecordSerDeTest.java deleted file mode 100644 index ce74b25c201..00000000000 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serde/ProtobufFileRecordSerDeTest.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.provectus.kafka.ui.serde; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.google.protobuf.DynamicMessage; -import com.google.protobuf.util.JsonFormat; -import com.provectus.kafka.ui.serde.schemaregistry.MessageFormat; -import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.Map; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.common.utils.Bytes; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -class ProtobufFileRecordSerDeTest { - - // Sample message of type `test.Person` - private static byte[] personMessage; - // Sample message of type `test.AddressBook` - private static byte[] addressBookMessage; - private static Path protobufSchemaPath; - - @BeforeAll - static void setUp() throws URISyntaxException, IOException { - protobufSchemaPath = Paths.get(ProtobufFileRecordSerDeTest.class.getClassLoader() - .getResource("address-book.proto").toURI()); - ProtobufSchema protobufSchema = new ProtobufSchema(Files.readString(protobufSchemaPath)); - - DynamicMessage.Builder builder = protobufSchema.newMessageBuilder("test.Person"); - JsonFormat.parser().merge( - "{ \"name\": \"My Name\",\"id\": 101, \"email\": \"user1@example.com\" }", builder); - personMessage = builder.build().toByteArray(); - - builder = protobufSchema.newMessageBuilder("test.AddressBook"); - JsonFormat.parser().merge( - "{\"version\": 1, \"people\": [" - + "{ \"name\": \"My Name\",\"id\": 102, \"email\": \"user2@example.com\" }]}", builder); - addressBookMessage = builder.build().toByteArray(); - } - - @Test - void testDeserialize() throws IOException { - var messageNameMap = Map.of( - "topic1", "test.Person", - "topic2", "test.AddressBook"); - var keyMessageNameMap = Map.of( - "topic2", "test.Person"); - var deserializer = - new ProtobufFileRecordSerDe(protobufSchemaPath, messageNameMap, keyMessageNameMap, null, null); - var msg1 = deserializer - .deserialize(new ConsumerRecord<>("topic1", 1, 0, Bytes.wrap("key".getBytes()), - Bytes.wrap(personMessage))); - assertEquals(MessageFormat.PROTOBUF, msg1.getValueFormat()); - assertTrue(msg1.getValue().contains("user1@example.com")); - - var msg2 = deserializer - .deserialize(new ConsumerRecord<>("topic2", 1, 1, Bytes.wrap(personMessage), - Bytes.wrap(addressBookMessage))); - assertEquals(MessageFormat.PROTOBUF, msg2.getKeyFormat()); - assertTrue(msg2.getKey().contains("user1@example.com")); - assertTrue(msg2.getValue().contains("user2@example.com")); - } - - @Test - void testNoDefaultMessageName() throws IOException { - // by default the first message type defined in proto definition is used - var deserializer = - new ProtobufFileRecordSerDe(protobufSchemaPath, Collections.emptyMap(), null, null, null); - var msg = deserializer - .deserialize(new ConsumerRecord<>("topic", 1, 0, Bytes.wrap("key".getBytes()), - Bytes.wrap(personMessage))); - assertTrue(msg.getValue().contains("user1@example.com")); - } - - @Test - void testDefaultMessageName() throws IOException { - var messageNameMap = Map.of("topic1", "test.Person"); - var deserializer = - new ProtobufFileRecordSerDe(protobufSchemaPath, messageNameMap, null, "test.AddressBook", null); - var msg = deserializer - .deserialize(new ConsumerRecord<>("a_random_topic", 1, 0, Bytes.wrap(addressBookMessage), - Bytes.wrap(addressBookMessage))); - assertTrue(msg.getValue().contains("user2@example.com")); - } - - @Test - void testDefaultKeyMessageName() throws IOException { - var messageNameMap = Map.of("topic1", "test.Person"); - var deserializer = - new ProtobufFileRecordSerDe(protobufSchemaPath, messageNameMap, messageNameMap, "test.AddressBook", - "test.AddressBook"); - var msg = deserializer - .deserialize(new ConsumerRecord<>("a_random_topic", 1, 0, Bytes.wrap(addressBookMessage), - Bytes.wrap(addressBookMessage))); - assertTrue(msg.getKey().contains("user2@example.com")); - } - - @Test - void testSerialize() throws IOException { - var messageNameMap = Map.of("topic1", "test.Person"); - var serializer = - new ProtobufFileRecordSerDe(protobufSchemaPath, messageNameMap, null, "test.AddressBook", null); - var serialized = serializer.serialize("topic1", "key1", "{\"name\":\"MyName\"}", 0); - assertNotNull(serialized.value()); - } - - @Test - void testSerializeKeyAndValue() throws IOException { - var messageNameMap = Map.of("topic1", "test.Person"); - var serializer = - new ProtobufFileRecordSerDe(protobufSchemaPath, messageNameMap, messageNameMap, "test.AddressBook", - "test.AddressBook"); - var serialized = serializer.serialize("topic1", "{\"name\":\"MyName\"}", "{\"name\":\"MyName\"}", 0); - assertNotNull(serialized.key()); - assertNotNull(serialized.value()); - } -} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serde/SimpleRecordSerDeTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serde/SimpleRecordSerDeTest.java deleted file mode 100644 index 7f9fd66525a..00000000000 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serde/SimpleRecordSerDeTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.provectus.kafka.ui.serde; - -import static com.provectus.kafka.ui.serde.RecordSerDe.DeserializedKeyValue; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import com.provectus.kafka.ui.serde.schemaregistry.MessageFormat; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.common.utils.Bytes; -import org.junit.jupiter.api.Test; - -class SimpleRecordSerDeTest { - - private final SimpleRecordSerDe serde = new SimpleRecordSerDe(); - - @Test - public void shouldDeserializeStringValue() { - var value = "test"; - var deserializedRecord = serde.deserialize( - new ConsumerRecord<>("topic", 1, 0, Bytes.wrap("key".getBytes()), - Bytes.wrap(value.getBytes()))); - DeserializedKeyValue expected = DeserializedKeyValue.builder() - .key("key") - .keyFormat(MessageFormat.UNKNOWN) - .value(value) - .valueFormat(MessageFormat.UNKNOWN) - .build(); - assertEquals(expected, deserializedRecord); - } - - @Test - public void shouldDeserializeNullValueRecordToEmptyMap() { - var deserializedRecord = serde - .deserialize(new ConsumerRecord<>("topic", 1, 0, Bytes.wrap("key".getBytes()), null)); - DeserializedKeyValue expected = DeserializedKeyValue.builder() - .key("key") - .keyFormat(MessageFormat.UNKNOWN) - .build(); - assertEquals(expected, deserializedRecord); - } -} \ No newline at end of file diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serde/schemaregistry/SchemaRegistryAwareRecordSerDeTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serde/schemaregistry/SchemaRegistryAwareRecordSerDeTest.java deleted file mode 100644 index 65929e6be2b..00000000000 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serde/schemaregistry/SchemaRegistryAwareRecordSerDeTest.java +++ /dev/null @@ -1,203 +0,0 @@ -package com.provectus.kafka.ui.serde.schemaregistry; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.json.JsonMapper; -import com.provectus.kafka.ui.model.KafkaCluster; -import io.confluent.kafka.schemaregistry.avro.AvroSchema; -import io.confluent.kafka.schemaregistry.avro.AvroSchemaUtils; -import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient; -import io.confluent.kafka.schemaregistry.client.rest.exceptions.RestClientException; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import org.apache.avro.generic.GenericDatumWriter; -import org.apache.avro.io.Encoder; -import org.apache.avro.io.EncoderFactory; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.common.utils.Bytes; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -class SchemaRegistryAwareRecordSerDeTest { - - private final SchemaRegistryClient registryClient = mock(SchemaRegistryClient.class); - - private final SchemaRegistryAwareRecordSerDe serde = new SchemaRegistryAwareRecordSerDe( - KafkaCluster.builder().build(), - registryClient - ); - - @Nested - class Deserialize { - - @Test - void callsSchemaFormatterWhenValueHasMagicByteAndValidSchemaId() throws Exception { - AvroSchema schema = new AvroSchema( - "{" - + " \"type\": \"record\"," - + " \"name\": \"TestAvroRecord1\"," - + " \"fields\": [" - + " {" - + " \"name\": \"field1\"," - + " \"type\": \"string\"" - + " }," - + " {" - + " \"name\": \"field2\"," - + " \"type\": \"int\"" - + " }" - + " ]" - + "}" - ); - - String jsonValueForSchema = "{ \"field1\":\"testStr\", \"field2\": 123 }"; - - int schemaId = 1234; - when(registryClient.getSchemaById(schemaId)).thenReturn(schema); - when(registryClient.getSchemaBySubjectAndId(null, schemaId)).thenReturn(schema); - - var result = serde.deserialize( - new ConsumerRecord<>( - "test-topic", - 1, - 100, - Bytes.wrap("key".getBytes()), - bytesWithMagicByteAndSchemaId(schemaId, jsonToAvro(jsonValueForSchema, schema)) - ) - ); - - // called once by serde code - verify(registryClient, times(1)).getSchemaById(schemaId); - //called once by formatter (will be cached) - verify(registryClient, times(1)).getSchemaBySubjectAndId(null, schemaId); - - assertThat(result.getKeySchemaId()).isNull(); - assertThat(result.getKeyFormat()).isEqualTo(MessageFormat.UNKNOWN); - assertThat(result.getKey()).isEqualTo("key"); - - assertThat(result.getValueSchemaId()).isEqualTo(schemaId + ""); - assertThat(result.getValueFormat()).isEqualTo(MessageFormat.AVRO); - assertJsonsEqual(jsonValueForSchema, result.getValue()); - } - - @Test - void fallsBackToStringFormatterIfValueContainsMagicByteButSchemaNotFound() throws Exception { - int nonExistingSchemaId = 12341234; - when(registryClient.getSchemaById(nonExistingSchemaId)) - .thenThrow(new RestClientException("not fount", 404, 404)); - - Bytes value = bytesWithMagicByteAndSchemaId(nonExistingSchemaId, "somedata".getBytes()); - var result = serde.deserialize( - new ConsumerRecord<>( - "test-topic", - 1, - 100, - Bytes.wrap("key".getBytes()), - value - ) - ); - - // called to get schema by id - will throw not found - verify(registryClient, times(1)).getSchemaById(nonExistingSchemaId); - - assertThat(result.getKeySchemaId()).isNull(); - assertThat(result.getKeyFormat()).isEqualTo(MessageFormat.UNKNOWN); - assertThat(result.getKey()).isEqualTo("key"); - - assertThat(result.getValueSchemaId()).isNull(); - assertThat(result.getValueFormat()).isEqualTo(MessageFormat.UNKNOWN); - assertThat(result.getValue()).isEqualTo(new String(value.get())); - } - - @Test - void fallsBackToStringFormatterIfMagicByteAndSchemaIdFoundButFormatterFailed() throws Exception { - int schemaId = 1234; - - final var schema = new AvroSchema("{ \"type\": \"string\" }"); - - when(registryClient.getSchemaById(schemaId)) - .thenReturn(schema); - when(registryClient.getSchemaBySubjectAndId(null, schemaId)).thenReturn(schema); - - // will cause exception in avro deserializer - Bytes nonAvroValue = bytesWithMagicByteAndSchemaId(schemaId, "123".getBytes()); - var result = serde.deserialize( - new ConsumerRecord<>( - "test-topic", - 1, - 100, - Bytes.wrap("key".getBytes()), - nonAvroValue - ) - ); - - // called once by serde code - verify(registryClient, times(1)).getSchemaById(schemaId); - //called once by formatter (will be cached) - verify(registryClient, times(1)).getSchemaBySubjectAndId(null, schemaId); - - assertThat(result.getKeySchemaId()).isNull(); - assertThat(result.getKeyFormat()).isEqualTo(MessageFormat.UNKNOWN); - assertThat(result.getKey()).isEqualTo("key"); - - assertThat(result.getValueSchemaId()).isNull(); - assertThat(result.getValueFormat()).isEqualTo(MessageFormat.UNKNOWN); - assertThat(result.getValue()).isEqualTo(new String(nonAvroValue.get())); - } - - @Test - void useStringFormatterWithoutRegistryManipulationIfMagicByteNotSet() { - var result = serde.deserialize( - new ConsumerRecord<>( - "test-topic", - 1, - 100, - Bytes.wrap("key".getBytes()), - Bytes.wrap("val".getBytes()) - ) - ); - - verifyZeroInteractions(registryClient); - - assertThat(result.getKeySchemaId()).isNull(); - assertThat(result.getKeyFormat()).isEqualTo(MessageFormat.UNKNOWN); - assertThat(result.getKey()).isEqualTo("key"); - - assertThat(result.getValueSchemaId()).isNull(); - assertThat(result.getValueFormat()).isEqualTo(MessageFormat.UNKNOWN); - assertThat(result.getValue()).isEqualTo("val"); - } - - private void assertJsonsEqual(String expected, String actual) throws JsonProcessingException { - var mapper = new JsonMapper(); - assertThat(mapper.readTree(actual)).isEqualTo(mapper.readTree(expected)); - } - - private Bytes bytesWithMagicByteAndSchemaId(int schemaId, byte[] body) { - return new Bytes( - ByteBuffer.allocate(1 + 4 + body.length) - .put((byte) 0) - .putInt(schemaId) - .put(body) - .array() - ); - } - - private byte[] jsonToAvro(String json, AvroSchema schema) throws IOException { - GenericDatumWriter writer = new GenericDatumWriter<>(schema.rawSchema()); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - Encoder encoder = EncoderFactory.get().binaryEncoder(output, null); - writer.write(AvroSchemaUtils.toObject(json, schema), encoder); - encoder.flush(); - return output.toByteArray(); - } - } - - -} \ No newline at end of file diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/ConsumerRecordDeserializerTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/ConsumerRecordDeserializerTest.java new file mode 100644 index 00000000000..87e13da3d25 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/ConsumerRecordDeserializerTest.java @@ -0,0 +1,30 @@ +package com.provectus.kafka.ui.serdes; + +import static com.provectus.kafka.ui.serde.api.DeserializeResult.Type.STRING; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import com.provectus.kafka.ui.model.TopicMessageDTO; +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.Serde; +import java.util.Map; +import java.util.function.UnaryOperator; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.common.utils.Bytes; +import org.junit.jupiter.api.Test; + +class ConsumerRecordDeserializerTest { + + @Test + void dataMaskingAppliedOnDeserializedMessage() { + UnaryOperator maskerMock = mock(); + Serde.Deserializer deser = (headers, data) -> new DeserializeResult("test", STRING, Map.of()); + + var recordDeser = new ConsumerRecordDeserializer("test", deser, "test", deser, "test", deser, deser, maskerMock); + recordDeser.deserialize(new ConsumerRecord<>("t", 1, 1L, Bytes.wrap("t".getBytes()), Bytes.wrap("t".getBytes()))); + + verify(maskerMock).apply(any(TopicMessageDTO.class)); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/PropertyResolverImplTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/PropertyResolverImplTest.java new file mode 100644 index 00000000000..11f5ea0bb10 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/PropertyResolverImplTest.java @@ -0,0 +1,156 @@ +package com.provectus.kafka.ui.serdes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.boot.context.properties.bind.BindException; +import org.springframework.mock.env.MockEnvironment; + +class PropertyResolverImplTest { + + private static final String TEST_STRING_VALUE = "testStr"; + private static final int TEST_INT_VALUE = 123; + private static final List TEST_STRING_LIST = List.of("v1", "v2", "v3"); + private static final List TEST_INT_LIST = List.of(1, 2, 3); + + private final MockEnvironment env = new MockEnvironment(); + + @Data + @AllArgsConstructor + public static class CustomPropertiesClass { + private String f1; + private Integer f2; + } + + @Test + void returnsEmptyOptionalWhenPropertyNotExist() { + var resolver = new PropertyResolverImpl(env); + assertThat(resolver.getProperty("nonExistingProp", String.class)).isEmpty(); + assertThat(resolver.getListProperty("nonExistingProp", String.class)).isEmpty(); + assertThat(resolver.getMapProperty("nonExistingProp", String.class, String.class)).isEmpty(); + } + + @Test + void throwsExceptionWhenPropertyCantBeResolverToRequstedClass() { + env.setProperty("prop.0.strProp", "testStr"); + env.setProperty("prop.0.strLst", "v1,v2,v3"); + env.setProperty("prop.0.strMap.k1", "v1"); + + var resolver = new PropertyResolverImpl(env); + assertThatCode(() -> resolver.getProperty("prop.0.strProp", Integer.class)) + .isInstanceOf(BindException.class); + assertThatCode(() -> resolver.getListProperty("prop.0.strLst", Integer.class)) + .isInstanceOf(BindException.class); + assertThatCode(() -> resolver.getMapProperty("prop.0.strMap", Integer.class, String.class)) + .isInstanceOf(BindException.class); + } + + @Test + void resolvedSingleValueProperties() { + env.setProperty("prop.0.strProp", "testStr"); + env.setProperty("prop.0.intProp", "123"); + + var resolver = new PropertyResolverImpl(env); + assertThat(resolver.getProperty("prop.0.strProp", String.class)) + .hasValue("testStr"); + assertThat(resolver.getProperty("prop.0.intProp", Integer.class)) + .hasValue(123); + } + + @Test + void resolvesListProperties() { + env.setProperty("prop.0.strLst", "v1,v2,v3"); + env.setProperty("prop.0.intLst", "1,2,3"); + + var resolver = new PropertyResolverImpl(env); + assertThat(resolver.getListProperty("prop.0.strLst", String.class)) + .hasValue(List.of("v1", "v2", "v3")); + assertThat(resolver.getListProperty("prop.0.intLst", Integer.class)) + .hasValue(List.of(1, 2, 3)); + } + + @Test + void resolvesCustomConfigClassProperties() { + env.setProperty("prop.0.custProps.f1", "f1val"); + env.setProperty("prop.0.custProps.f2", "1234"); + + var resolver = new PropertyResolverImpl(env); + assertThat(resolver.getProperty("prop.0.custProps", CustomPropertiesClass.class)) + .hasValue(new CustomPropertiesClass("f1val", 1234)); + } + + @Test + void resolvesMapProperties() { + env.setProperty("prop.0.strMap.k1", "v1"); + env.setProperty("prop.0.strMap.k2", "v2"); + env.setProperty("prop.0.intToLongMap.100", "111"); + env.setProperty("prop.0.intToLongMap.200", "222"); + + var resolver = new PropertyResolverImpl(env); + assertThat(resolver.getMapProperty("prop.0.strMap", String.class, String.class)) + .hasValue(Map.of("k1", "v1", "k2", "v2")); + assertThat(resolver.getMapProperty("prop.0.intToLongMap", Integer.class, Long.class)) + .hasValue(Map.of(100, 111L, 200, 222L)); + } + + + @Nested + class WithPrefix { + + @Test + void resolvedSingleValueProperties() { + env.setProperty("prop.0.strProp", "testStr"); + env.setProperty("prop.0.intProp", "123"); + + var resolver = new PropertyResolverImpl(env, "prop.0"); + assertThat(resolver.getProperty("strProp", String.class)) + .hasValue(TEST_STRING_VALUE); + + assertThat(resolver.getProperty("intProp", Integer.class)) + .hasValue(TEST_INT_VALUE); + } + + @Test + void resolvesListProperties() { + env.setProperty("prop.0.strLst", "v1,v2,v3"); + env.setProperty("prop.0.intLst", "1,2,3"); + + var resolver = new PropertyResolverImpl(env, "prop.0"); + assertThat(resolver.getListProperty("strLst", String.class)) + .hasValue(TEST_STRING_LIST); + assertThat(resolver.getListProperty("intLst", Integer.class)) + .hasValue(TEST_INT_LIST); + } + + @Test + void resolvesCustomConfigClassProperties() { + env.setProperty("prop.0.custProps.f1", "f1val"); + env.setProperty("prop.0.custProps.f2", "1234"); + + var resolver = new PropertyResolverImpl(env, "prop.0"); + assertThat(resolver.getProperty("custProps", CustomPropertiesClass.class)) + .hasValue(new CustomPropertiesClass("f1val", 1234)); + } + + @Test + void resolvesMapProperties() { + env.setProperty("prop.0.strMap.k1", "v1"); + env.setProperty("prop.0.strMap.k2", "v2"); + env.setProperty("prop.0.intToLongMap.100", "111"); + env.setProperty("prop.0.intToLongMap.200", "222"); + + var resolver = new PropertyResolverImpl(env, "prop.0."); + assertThat(resolver.getMapProperty("strMap", String.class, String.class)) + .hasValue(Map.of("k1", "v1", "k2", "v2")); + assertThat(resolver.getMapProperty("intToLongMap", Integer.class, Long.class)) + .hasValue(Map.of(100, 111L, 200, 222L)); + } + } + +} \ No newline at end of file diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/SerdesInitializerTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/SerdesInitializerTest.java new file mode 100644 index 00000000000..4041f755edd --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/SerdesInitializerTest.java @@ -0,0 +1,183 @@ +package com.provectus.kafka.ui.serdes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.provectus.kafka.ui.config.ClustersProperties; +import com.provectus.kafka.ui.exception.ValidationException; +import com.provectus.kafka.ui.serde.api.PropertyResolver; +import com.provectus.kafka.ui.serdes.builtin.Int32Serde; +import com.provectus.kafka.ui.serdes.builtin.StringSerde; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.springframework.core.env.Environment; +import org.springframework.mock.env.MockEnvironment; + +class SerdesInitializerTest { + + private final Environment env = new MockEnvironment(); + private final CustomSerdeLoader customSerdeLoaderMock = mock(CustomSerdeLoader.class); + + private final SerdesInitializer initializer = new SerdesInitializer( + Map.of( + "BuiltIn1", BuiltInSerdeWithAutoconfigure.class, + "BuiltIn2", BuiltInSerdeMock2NoAutoConfigure.class, + Int32Serde.name(), Int32Serde.class, + StringSerde.name(), StringSerde.class + ), + customSerdeLoaderMock + ); + + @Test + void pluggedSerdesInitializedByLoader() { + ClustersProperties.SerdeConfig customSerdeConfig = new ClustersProperties.SerdeConfig(); + customSerdeConfig.setName("MyPluggedSerde"); + customSerdeConfig.setFilePath("/custom.jar"); + customSerdeConfig.setClassName("org.test.MyPluggedSerde"); + customSerdeConfig.setTopicKeysPattern("keys"); + customSerdeConfig.setTopicValuesPattern("values"); + + when(customSerdeLoaderMock.loadAndConfigure(anyString(), anyString(), any(), any(), any())) + .thenReturn(new CustomSerdeLoader.CustomSerde(new StringSerde(), new URLClassLoader(new URL[]{}))); + + var serdes = init(customSerdeConfig); + + SerdeInstance customSerdeInstance = serdes.serdes.get("MyPluggedSerde"); + verifyPatternsMatch(customSerdeConfig, customSerdeInstance); + assertThat(customSerdeInstance.classLoader).isNotNull(); + + verify(customSerdeLoaderMock).loadAndConfigure( + eq(customSerdeConfig.getClassName()), + eq(customSerdeConfig.getFilePath()), + any(), any(), any() + ); + } + + @Test + void serdeWithBuiltInNameAndNoPropertiesCantBeInitializedIfSerdeNotSupportAutoConfigure() { + ClustersProperties.SerdeConfig serdeConfig = new ClustersProperties.SerdeConfig(); + serdeConfig.setName("BuiltIn2"); //auto-configuration not supported + serdeConfig.setTopicKeysPattern("keys"); + serdeConfig.setTopicValuesPattern("vals"); + + assertThatCode(() -> initializer.init(env, createProperties(serdeConfig), 0)) + .isInstanceOf(ValidationException.class); + } + + @Test + void serdeWithBuiltInNameAndNoPropertiesIsAutoConfiguredIfPossible() { + ClustersProperties.SerdeConfig serdeConfig = new ClustersProperties.SerdeConfig(); + serdeConfig.setName("BuiltIn1"); // supports auto-configuration + serdeConfig.setTopicKeysPattern("keys"); + serdeConfig.setTopicValuesPattern("vals"); + + var serdes = init(serdeConfig); + + SerdeInstance autoConfiguredSerde = serdes.serdes.get("BuiltIn1"); + verifyAutoConfigured(autoConfiguredSerde); + verifyPatternsMatch(serdeConfig, autoConfiguredSerde); + } + + @Test + void serdeWithBuiltInNameAndSetPropertiesAreExplicitlyConfigured() { + ClustersProperties.SerdeConfig serdeConfig = new ClustersProperties.SerdeConfig(); + serdeConfig.setName("BuiltIn1"); + serdeConfig.setProperties(Map.of("any", "property")); + serdeConfig.setTopicKeysPattern("keys"); + serdeConfig.setTopicValuesPattern("vals"); + + var serdes = init(serdeConfig); + + SerdeInstance explicitlyConfiguredSerde = serdes.serdes.get("BuiltIn1"); + verifyExplicitlyConfigured(explicitlyConfiguredSerde); + verifyPatternsMatch(serdeConfig, explicitlyConfiguredSerde); + } + + @Test + void serdeWithCustomNameAndBuiltInClassnameAreExplicitlyConfigured() { + ClustersProperties.SerdeConfig serdeConfig = new ClustersProperties.SerdeConfig(); + serdeConfig.setName("SomeSerde"); + serdeConfig.setClassName(BuiltInSerdeWithAutoconfigure.class.getName()); + serdeConfig.setTopicKeysPattern("keys"); + serdeConfig.setTopicValuesPattern("vals"); + + var serdes = init(serdeConfig); + + SerdeInstance explicitlyConfiguredSerde = serdes.serdes.get("SomeSerde"); + verifyExplicitlyConfigured(explicitlyConfiguredSerde); + verifyPatternsMatch(serdeConfig, explicitlyConfiguredSerde); + } + + private ClusterSerdes init(ClustersProperties.SerdeConfig... serdeConfigs) { + return initializer.init(env, createProperties(serdeConfigs), 0); + } + + private ClustersProperties createProperties(ClustersProperties.SerdeConfig... serdeConfigs) { + ClustersProperties.Cluster cluster = new ClustersProperties.Cluster(); + cluster.setName("test"); + cluster.setSerde(List.of(serdeConfigs)); + + ClustersProperties clustersProperties = new ClustersProperties(); + clustersProperties.setClusters(List.of(cluster)); + return clustersProperties; + } + + private void verifyExplicitlyConfigured(SerdeInstance serde) { + assertThat(((BuiltInSerdeWithAutoconfigure) serde.serde).autoConfigureCheckCalled).isFalse(); + assertThat(((BuiltInSerdeWithAutoconfigure) serde.serde).autoConfigured).isFalse(); + assertThat(((BuiltInSerdeWithAutoconfigure) serde.serde).explicitlyConfigured).isTrue(); + } + + private void verifyAutoConfigured(SerdeInstance serde) { + assertThat(((BuiltInSerdeWithAutoconfigure) serde.serde).autoConfigureCheckCalled).isTrue(); + assertThat(((BuiltInSerdeWithAutoconfigure) serde.serde).autoConfigured).isTrue(); + assertThat(((BuiltInSerdeWithAutoconfigure) serde.serde).explicitlyConfigured).isFalse(); + } + + private void verifyPatternsMatch(ClustersProperties.SerdeConfig config, SerdeInstance serde) { + assertThat(serde.topicKeyPattern.pattern()).isEqualTo(config.getTopicKeysPattern()); + assertThat(serde.topicValuePattern.pattern()).isEqualTo(config.getTopicValuesPattern()); + } + + static class BuiltInSerdeWithAutoconfigure extends StringSerde { + + boolean explicitlyConfigured = false; + boolean autoConfigured = false; + boolean autoConfigureCheckCalled = false; + + @Override + public boolean canBeAutoConfigured(PropertyResolver kafkaClusterProperties, PropertyResolver globalProperties) { + this.autoConfigureCheckCalled = true; + return true; + } + + @Override + public void autoConfigure(PropertyResolver kafkaClusterProperties, PropertyResolver globalProperties) { + this.autoConfigured = true; + } + + @Override + public void configure(PropertyResolver serdeProperties, + PropertyResolver kafkaClusterProperties, + PropertyResolver globalProperties) { + this.explicitlyConfigured = true; + } + } + + static class BuiltInSerdeMock2NoAutoConfigure extends BuiltInSerdeWithAutoconfigure { + @Override + public boolean canBeAutoConfigured(PropertyResolver kafkaClusterProperties, PropertyResolver globalProperties) { + this.autoConfigureCheckCalled = true; + return false; + } + } +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/AvroEmbeddedSerdeTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/AvroEmbeddedSerdeTest.java new file mode 100644 index 00000000000..2f4734ce069 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/AvroEmbeddedSerdeTest.java @@ -0,0 +1,92 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.Serde; +import com.provectus.kafka.ui.serdes.PropertyResolverImpl; +import io.confluent.kafka.schemaregistry.avro.AvroSchemaUtils; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import org.apache.avro.Schema; +import org.apache.avro.file.DataFileWriter; +import org.apache.avro.generic.GenericData; +import org.apache.avro.generic.GenericDatumWriter; +import org.apache.avro.generic.GenericRecord; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class AvroEmbeddedSerdeTest { + + private AvroEmbeddedSerde avroEmbeddedSerde; + + @BeforeEach + void init() { + avroEmbeddedSerde = new AvroEmbeddedSerde(); + avroEmbeddedSerde.configure( + PropertyResolverImpl.empty(), + PropertyResolverImpl.empty(), + PropertyResolverImpl.empty() + ); + } + + @ParameterizedTest + @EnumSource + void canDeserializeReturnsTrueForAllTargets(Serde.Target target) { + assertThat(avroEmbeddedSerde.canDeserialize("anyTopic", target)) + .isTrue(); + } + + @ParameterizedTest + @EnumSource + void canSerializeReturnsFalseForAllTargets(Serde.Target target) { + assertThat(avroEmbeddedSerde.canSerialize("anyTopic", target)) + .isFalse(); + } + + @Test + void deserializerParsesAvroDataWithEmbeddedSchema() throws Exception { + Schema schema = new Schema.Parser().parse(""" + { + "type": "record", + "name": "TestAvroRecord", + "fields": [ + { "name": "field1", "type": "string" }, + { "name": "field2", "type": "int" } + ] + } + """ + ); + GenericRecord record = new GenericData.Record(schema); + record.put("field1", "this is test msg"); + record.put("field2", 100500); + + String jsonRecord = new String(AvroSchemaUtils.toJson(record)); + byte[] serializedRecordBytes = serializeAvroWithEmbeddedSchema(record); + + var deserializer = avroEmbeddedSerde.deserializer("anyTopic", Serde.Target.KEY); + DeserializeResult result = deserializer.deserialize(null, serializedRecordBytes); + assertThat(result.getType()).isEqualTo(DeserializeResult.Type.JSON); + assertThat(result.getAdditionalProperties()).isEmpty(); + assertJsonEquals(jsonRecord, result.getResult()); + } + + private void assertJsonEquals(String expected, String actual) throws IOException { + var mapper = new JsonMapper(); + assertThat(mapper.readTree(actual)).isEqualTo(mapper.readTree(expected)); + } + + private byte[] serializeAvroWithEmbeddedSchema(GenericRecord record) throws IOException { + try (DataFileWriter writer = new DataFileWriter<>(new GenericDatumWriter<>()); + ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + writer.create(record.getSchema(), baos); + writer.append(record); + writer.flush(); + return baos.toByteArray(); + } + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/Base64SerdeTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/Base64SerdeTest.java new file mode 100644 index 00000000000..51816e49802 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/Base64SerdeTest.java @@ -0,0 +1,66 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.Serde; +import com.provectus.kafka.ui.serdes.PropertyResolverImpl; +import com.provectus.kafka.ui.serdes.RecordHeadersImpl; +import java.util.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class Base64SerdeTest { + + private static final byte[] TEST_BYTES = "some bytes go here".getBytes(); + private static final String TEST_BYTES_BASE64_ENCODED = Base64.getEncoder().encodeToString(TEST_BYTES); + + private Serde base64Serde; + + @BeforeEach + void init() { + base64Serde = new Base64Serde(); + base64Serde.configure( + PropertyResolverImpl.empty(), + PropertyResolverImpl.empty(), + PropertyResolverImpl.empty() + ); + } + + @ParameterizedTest + @EnumSource + void serializesInputAsBase64String(Serde.Target type) { + var serializer = base64Serde.serializer("anyTopic", type); + byte[] bytes = serializer.serialize(TEST_BYTES_BASE64_ENCODED); + assertThat(bytes).isEqualTo(TEST_BYTES); + } + + @ParameterizedTest + @EnumSource + void deserializesDataAsBase64Bytes(Serde.Target type) { + var deserializer = base64Serde.deserializer("anyTopic", type); + var result = deserializer.deserialize(new RecordHeadersImpl(), TEST_BYTES); + assertThat(result.getResult()).isEqualTo(TEST_BYTES_BASE64_ENCODED); + assertThat(result.getType()).isEqualTo(DeserializeResult.Type.STRING); + assertThat(result.getAdditionalProperties()).isEmpty(); + } + + @ParameterizedTest + @EnumSource + void getSchemaReturnsEmpty(Serde.Target type) { + assertThat(base64Serde.getSchema("anyTopic", type)).isEmpty(); + } + + @ParameterizedTest + @EnumSource + void canDeserializeReturnsTrueForAllInputs(Serde.Target type) { + assertThat(base64Serde.canDeserialize("anyTopic", type)).isTrue(); + } + + @ParameterizedTest + @EnumSource + void canSerializeReturnsTrueForAllInput(Serde.Target type) { + assertThat(base64Serde.canSerialize("anyTopic", type)).isTrue(); + } +} \ No newline at end of file diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/ConsumerOffsetsSerdeTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/ConsumerOffsetsSerdeTest.java new file mode 100644 index 00000000000..1fab56322a8 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/ConsumerOffsetsSerdeTest.java @@ -0,0 +1,185 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import static com.provectus.kafka.ui.serdes.builtin.ConsumerOffsetsSerde.TOPIC; +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.provectus.kafka.ui.AbstractIntegrationTest; +import com.provectus.kafka.ui.producer.KafkaTestProducer; +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.Serde; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.UUID; +import lombok.SneakyThrows; +import org.apache.kafka.clients.admin.NewTopic; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.common.serialization.BytesDeserializer; +import org.apache.kafka.common.utils.Bytes; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testcontainers.shaded.org.awaitility.Awaitility; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +class ConsumerOffsetsSerdeTest extends AbstractIntegrationTest { + + private static final int MSGS_TO_GENERATE = 10; + + private static String consumerGroupName; + private static String committedTopic; + + @BeforeAll + static void createTopicAndCommitItsOffset() { + committedTopic = ConsumerOffsetsSerdeTest.class.getSimpleName() + "-" + UUID.randomUUID(); + consumerGroupName = committedTopic + "-group"; + createTopic(new NewTopic(committedTopic, 1, (short) 1)); + + try (var producer = KafkaTestProducer.forKafka(kafka)) { + for (int i = 0; i < MSGS_TO_GENERATE; i++) { + producer.send(committedTopic, "i=" + i); + } + } + try (var consumer = createConsumer(consumerGroupName)) { + consumer.subscribe(List.of(committedTopic)); + int polled = 0; + while (polled < MSGS_TO_GENERATE) { + polled += consumer.poll(Duration.ofMillis(100)).count(); + } + consumer.commitSync(); + } + } + + @AfterAll + static void cleanUp() { + deleteTopic(committedTopic); + } + + @Test + void canOnlyDeserializeConsumerOffsetsTopic() { + var serde = new ConsumerOffsetsSerde(); + assertThat(serde.canDeserialize(ConsumerOffsetsSerde.TOPIC, Serde.Target.KEY)).isTrue(); + assertThat(serde.canDeserialize(ConsumerOffsetsSerde.TOPIC, Serde.Target.VALUE)).isTrue(); + assertThat(serde.canDeserialize("anyOtherTopic", Serde.Target.KEY)).isFalse(); + assertThat(serde.canDeserialize("anyOtherTopic", Serde.Target.VALUE)).isFalse(); + } + + @Test + void deserializesMessagesMadeByConsumerActivity() { + var serde = new ConsumerOffsetsSerde(); + var keyDeserializer = serde.deserializer(TOPIC, Serde.Target.KEY); + var valueDeserializer = serde.deserializer(TOPIC, Serde.Target.VALUE); + + try (var consumer = createConsumer(consumerGroupName + "-check")) { + consumer.subscribe(List.of(ConsumerOffsetsSerde.TOPIC)); + List> polled = new ArrayList<>(); + + Awaitility.await() + .pollInSameThread() + .atMost(Duration.ofMinutes(1)) + .untilAsserted(() -> { + for (var rec : consumer.poll(Duration.ofMillis(200))) { + DeserializeResult key = rec.key() != null + ? keyDeserializer.deserialize(null, rec.key().get()) + : null; + DeserializeResult val = rec.value() != null + ? valueDeserializer.deserialize(null, rec.value().get()) + : null; + if (key != null && val != null) { + polled.add(Tuples.of(key, val)); + } + } + assertThat(polled).anyMatch(t -> isCommitMessage(t.getT1(), t.getT2())); + assertThat(polled).anyMatch(t -> isGroupMetadataMessage(t.getT1(), t.getT2())); + }); + } + } + + // Sample commit record: + // + // key: { + // "group": "test_Members_3", + // "topic": "test", + // "partition": 0 + // } + // + // value: + // { + // "offset": 2, + // "leader_epoch": 0, + // "metadata": "", + // "commit_timestamp": 1683112980588 + // } + private boolean isCommitMessage(DeserializeResult key, DeserializeResult value) { + var keyJson = toMapFromJsom(key); + boolean keyIsOk = consumerGroupName.equals(keyJson.get("group")) + && committedTopic.equals(keyJson.get("topic")) + && ((Integer) 0).equals(keyJson.get("partition")); + + var valueJson = toMapFromJsom(value); + boolean valueIsOk = valueJson.containsKey("offset") + && valueJson.get("offset").equals(MSGS_TO_GENERATE) + && valueJson.containsKey("commit_timestamp"); + + return keyIsOk && valueIsOk; + } + + // Sample group metadata record: + // + // key: { + // "group": "test_Members_3" + // } + // + // value: + // { + // "protocol_type": "consumer", + // "generation": 1, + // "protocol": "range", + // "leader": "consumer-test_Members_3-1-5a37876e-e42f-420e-9c7d-6902889bd5dd", + // "current_state_timestamp": 1683112974561, + // "members": [ + // { + // "member_id": "consumer-test_Members_3-1-5a37876e-e42f-420e-9c7d-6902889bd5dd", + // "group_instance_id": null, + // "client_id": "consumer-test_Members_3-1", + // "client_host": "/192.168.16.1", + // "rebalance_timeout": 300000, + // "session_timeout": 45000, + // "subscription": "AAEAAAABAAR0ZXN0/////wAAAAA=", + // "assignment": "AAEAAAABAAR0ZXN0AAAAAQAAAAD/////" + // } + // ] + // } + private boolean isGroupMetadataMessage(DeserializeResult key, DeserializeResult value) { + var keyJson = toMapFromJsom(key); + boolean keyIsOk = consumerGroupName.equals(keyJson.get("group")) && keyJson.size() == 1; + + var valueJson = toMapFromJsom(value); + boolean valueIsOk = valueJson.keySet() + .containsAll(Set.of("protocol_type", "generation", "leader", "members")); + + return keyIsOk && valueIsOk; + } + + @SneakyThrows + private Map toMapFromJsom(DeserializeResult result) { + return new JsonMapper().readValue(result.getResult(), Map.class); + } + + private static KafkaConsumer createConsumer(String groupId) { + Properties props = new Properties(); + props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); + props.put(ConsumerConfig.CLIENT_ID_CONFIG, groupId); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers()); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, BytesDeserializer.class); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, BytesDeserializer.class); + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + return new KafkaConsumer<>(props); + } +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/HexSerdeTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/HexSerdeTest.java new file mode 100644 index 00000000000..4ec28c15099 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/HexSerdeTest.java @@ -0,0 +1,80 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.Serde; +import com.provectus.kafka.ui.serdes.PropertyResolverImpl; +import com.provectus.kafka.ui.serdes.RecordHeadersImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EnumSource; + +public class HexSerdeTest { + + private static final byte[] TEST_BYTES = "hello world".getBytes(); + private static final String TEST_BYTES_HEX_ENCODED = "68 65 6C 6C 6F 20 77 6F 72 6C 64"; + + private HexSerde hexSerde; + + @BeforeEach + void init() { + hexSerde = new HexSerde(); + hexSerde.autoConfigure(PropertyResolverImpl.empty(), PropertyResolverImpl.empty()); + } + + + @ParameterizedTest + @CsvSource({ + "68656C6C6F20776F726C64", // uppercase + "68656c6c6f20776f726c64", // lowercase + "68:65:6c:6c:6f:20:77:6f:72:6c:64", // ':' delim + "68 65 6C 6C 6F 20 77 6F 72 6C 64", // space delim, UC + "68 65 6c 6c 6f 20 77 6f 72 6c 64", // space delim, LC + "#68 #65 #6C #6C #6F #20 #77 #6F #72 #6C #64" // '#' prefix, space delim + }) + void serializesInputAsHexString(String hexString) { + for (Serde.Target type : Serde.Target.values()) { + var serializer = hexSerde.serializer("anyTopic", type); + byte[] bytes = serializer.serialize(hexString); + assertThat(bytes).isEqualTo(TEST_BYTES); + } + } + + @ParameterizedTest + @EnumSource + void serializesEmptyStringAsEmptyBytesArray(Serde.Target type) { + var serializer = hexSerde.serializer("anyTopic", type); + byte[] bytes = serializer.serialize(""); + assertThat(bytes).isEqualTo(new byte[] {}); + } + + @ParameterizedTest + @EnumSource + void deserializesDataAsHexBytes(Serde.Target type) { + var deserializer = hexSerde.deserializer("anyTopic", type); + var result = deserializer.deserialize(new RecordHeadersImpl(), TEST_BYTES); + assertThat(result.getResult()).isEqualTo(TEST_BYTES_HEX_ENCODED); + assertThat(result.getType()).isEqualTo(DeserializeResult.Type.STRING); + assertThat(result.getAdditionalProperties()).isEmpty(); + } + + @ParameterizedTest + @EnumSource + void getSchemaReturnsEmpty(Serde.Target type) { + assertThat(hexSerde.getSchema("anyTopic", type)).isEmpty(); + } + + @ParameterizedTest + @EnumSource + void canDeserializeReturnsTrueForAllInputs(Serde.Target type) { + assertThat(hexSerde.canDeserialize("anyTopic", type)).isTrue(); + } + + @ParameterizedTest + @EnumSource + void canSerializeReturnsTrueForAllInput(Serde.Target type) { + assertThat(hexSerde.canSerialize("anyTopic", type)).isTrue(); + } +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/Int32SerdeTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/Int32SerdeTest.java new file mode 100644 index 00000000000..c6384becf8a --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/Int32SerdeTest.java @@ -0,0 +1,46 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.primitives.Ints; +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.Serde; +import com.provectus.kafka.ui.serdes.PropertyResolverImpl; +import com.provectus.kafka.ui.serdes.RecordHeadersImpl; +import org.apache.kafka.common.header.internals.RecordHeaders; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class Int32SerdeTest { + + private Int32Serde serde; + + @BeforeEach + void init() { + serde = new Int32Serde(); + serde.configure( + PropertyResolverImpl.empty(), + PropertyResolverImpl.empty(), + PropertyResolverImpl.empty() + ); + } + + @ParameterizedTest + @EnumSource + void serializeUses4BytesIntRepresentation(Serde.Target type) { + var serializer = serde.serializer("anyTopic", type); + byte[] bytes = serializer.serialize("1234"); + assertThat(bytes).isEqualTo(Ints.toByteArray(1234)); + } + + @ParameterizedTest + @EnumSource + void deserializeUses4BytesIntRepresentation(Serde.Target type) { + var deserializer = serde.deserializer("anyTopic", type); + var result = deserializer.deserialize(new RecordHeadersImpl(), Ints.toByteArray(1234)); + assertThat(result.getResult()).isEqualTo("1234"); + assertThat(result.getType()).isEqualTo(DeserializeResult.Type.JSON); + } + +} \ No newline at end of file diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/Int64SerdeTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/Int64SerdeTest.java new file mode 100644 index 00000000000..16fc211f798 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/Int64SerdeTest.java @@ -0,0 +1,47 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.primitives.Longs; +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.Serde; +import com.provectus.kafka.ui.serdes.PropertyResolverImpl; +import com.provectus.kafka.ui.serdes.RecordHeadersImpl; +import org.apache.kafka.common.header.internals.RecordHeaders; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + + +class Int64SerdeTest { + + private Int64Serde serde; + + @BeforeEach + void init() { + serde = new Int64Serde(); + serde.configure( + PropertyResolverImpl.empty(), + PropertyResolverImpl.empty(), + PropertyResolverImpl.empty() + ); + } + + @ParameterizedTest + @EnumSource + void serializeUses8BytesLongRepresentation(Serde.Target type) { + var serializer = serde.serializer("anyTopic", type); + byte[] bytes = serializer.serialize("1234"); + assertThat(bytes).isEqualTo(Longs.toByteArray(1234)); + } + + @ParameterizedTest + @EnumSource + void deserializeUses8BytesLongRepresentation(Serde.Target type) { + var deserializer = serde.deserializer("anyTopic", type); + var result = deserializer.deserialize(new RecordHeadersImpl(), Longs.toByteArray(1234)); + assertThat(result.getResult()).isEqualTo("1234"); + assertThat(result.getType()).isEqualTo(DeserializeResult.Type.JSON); + } + +} \ No newline at end of file diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/ProtobufFileSerdeTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/ProtobufFileSerdeTest.java new file mode 100644 index 00000000000..1b295dc77a4 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/ProtobufFileSerdeTest.java @@ -0,0 +1,383 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.google.protobuf.Descriptors; +import com.google.protobuf.util.JsonFormat; +import com.provectus.kafka.ui.serde.api.PropertyResolver; +import com.provectus.kafka.ui.serde.api.Serde; +import com.provectus.kafka.ui.serdes.builtin.ProtobufFileSerde.Configuration; +import com.squareup.wire.schema.ProtoFile; +import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.util.ResourceUtils; + +class ProtobufFileSerdeTest { + + private static final String samplePersonMsgJson = + "{ \"name\": \"My Name\",\"id\": 101, \"email\": \"user1@example.com\", \"phones\":[] }"; + + private static final String sampleBookMsgJson = "{\"version\": 1, \"people\": [" + + "{ \"name\": \"My Name\",\"id\": 102, \"email\": \"addrBook@example.com\", \"phones\":[]}]}"; + + private static final String sampleLangDescriptionMsgJson = "{ \"lang\": \"EN\", " + + "\"descr\": \"Some description here\" }"; + + // Sample message of type `test.Person` + private byte[] personMessageBytes; + // Sample message of type `test.AddressBook` + private byte[] addressBookMessageBytes; + private byte[] langDescriptionMessageBytes; + private Descriptors.Descriptor personDescriptor; + private Descriptors.Descriptor addressBookDescriptor; + private Descriptors.Descriptor langDescriptionDescriptor; + private Map descriptorPaths; + + @BeforeEach + void setUp() throws Exception { + Map files = ProtobufFileSerde.Configuration.loadSchemas( + Optional.empty(), + Optional.of(protoFilesDir()) + ); + + Path addressBookSchemaPath = ResourceUtils.getFile("classpath:protobuf-serde/address-book.proto").toPath(); + var addressBookSchema = files.get(addressBookSchemaPath); + var builder = addressBookSchema.newMessageBuilder("test.Person"); + JsonFormat.parser().merge(samplePersonMsgJson, builder); + personMessageBytes = builder.build().toByteArray(); + + builder = addressBookSchema.newMessageBuilder("test.AddressBook"); + JsonFormat.parser().merge(sampleBookMsgJson, builder); + addressBookMessageBytes = builder.build().toByteArray(); + personDescriptor = addressBookSchema.toDescriptor("test.Person"); + addressBookDescriptor = addressBookSchema.toDescriptor("test.AddressBook"); + + Path languageDescriptionPath = ResourceUtils.getFile("classpath:protobuf-serde/lang-description.proto").toPath(); + var languageDescriptionSchema = files.get(languageDescriptionPath); + builder = languageDescriptionSchema.newMessageBuilder("test.LanguageDescription"); + JsonFormat.parser().merge(sampleLangDescriptionMsgJson, builder); + langDescriptionMessageBytes = builder.build().toByteArray(); + langDescriptionDescriptor = languageDescriptionSchema.toDescriptor("test.LanguageDescription"); + + descriptorPaths = Map.of( + personDescriptor, addressBookSchemaPath, + addressBookDescriptor, addressBookSchemaPath + ); + } + + @Test + void loadsAllProtoFiledFromTargetDirectory() throws Exception { + var protoDir = ResourceUtils.getFile("classpath:protobuf-serde/").getPath(); + List files = new ProtobufFileSerde.ProtoSchemaLoader(protoDir).load(); + assertThat(files).hasSize(4); + assertThat(files) + .map(f -> f.getLocation().getPath()) + .containsExactlyInAnyOrder( + "language/language.proto", + "sensor.proto", + "address-book.proto", + "lang-description.proto" + ); + } + + @SneakyThrows + private String protoFilesDir() { + return ResourceUtils.getFile("classpath:protobuf-serde/").getPath(); + } + + @Nested + class ConfigurationTests { + + @Test + void canBeAutoConfiguredReturnsNoProtoPropertiesProvided() { + PropertyResolver resolver = mock(PropertyResolver.class); + assertThat(Configuration.canBeAutoConfigured(resolver)) + .isFalse(); + } + + @Test + void canBeAutoConfiguredReturnsTrueIfProtoFilesHasBeenProvided() { + PropertyResolver resolver = mock(PropertyResolver.class); + when(resolver.getListProperty("protobufFiles", String.class)) + .thenReturn(Optional.of(List.of("file.proto"))); + assertThat(Configuration.canBeAutoConfigured(resolver)) + .isTrue(); + } + + @Test + void canBeAutoConfiguredReturnsTrueIfProtoFilesDirProvided() { + PropertyResolver resolver = mock(PropertyResolver.class); + when(resolver.getProperty("protobufFilesDir", String.class)) + .thenReturn(Optional.of("/filesDir")); + assertThat(Configuration.canBeAutoConfigured(resolver)) + .isTrue(); + } + + @Test + void unknownSchemaAsDefaultThrowsException() { + PropertyResolver resolver = mock(PropertyResolver.class); + when(resolver.getProperty("protobufFilesDir", String.class)) + .thenReturn(Optional.of(protoFilesDir())); + + when(resolver.getProperty("protobufMessageName", String.class)) + .thenReturn(Optional.of("test.NotExistent")); + + assertThatThrownBy(() -> Configuration.create(resolver)) + .isInstanceOf(NullPointerException.class) + .hasMessage("The given message type not found in protobuf definition: test.NotExistent"); + } + + @Test + void unknownSchemaAsDefaultForKeyThrowsException() { + PropertyResolver resolver = mock(PropertyResolver.class); + when(resolver.getProperty("protobufFilesDir", String.class)) + .thenReturn(Optional.of(protoFilesDir())); + + when(resolver.getProperty("protobufMessageNameForKey", String.class)) + .thenReturn(Optional.of("test.NotExistent")); + + assertThatThrownBy(() -> Configuration.create(resolver)) + .isInstanceOf(NullPointerException.class) + .hasMessage("The given message type not found in protobuf definition: test.NotExistent"); + } + + @Test + void unknownSchemaAsTopicSchemaThrowsException() { + PropertyResolver resolver = mock(PropertyResolver.class); + when(resolver.getProperty("protobufFilesDir", String.class)) + .thenReturn(Optional.of(protoFilesDir())); + + when(resolver.getMapProperty("protobufMessageNameByTopic", String.class, String.class)) + .thenReturn(Optional.of(Map.of("persons", "test.NotExistent"))); + + assertThatThrownBy(() -> Configuration.create(resolver)) + .isInstanceOf(NullPointerException.class) + .hasMessage("The given message type not found in protobuf definition: test.NotExistent"); + } + + @Test + void unknownSchemaAsTopicSchemaForKeyThrowsException() { + PropertyResolver resolver = mock(PropertyResolver.class); + when(resolver.getProperty("protobufFilesDir", String.class)) + .thenReturn(Optional.of(protoFilesDir())); + + when(resolver.getMapProperty("protobufMessageNameForKeyByTopic", String.class, String.class)) + .thenReturn(Optional.of(Map.of("persons", "test.NotExistent"))); + + assertThatThrownBy(() -> Configuration.create(resolver)) + .isInstanceOf(NullPointerException.class) + .hasMessage("The given message type not found in protobuf definition: test.NotExistent"); + } + + @Test + void createConfigureFillsDescriptorMappingsWhenProtoFilesListProvided() throws Exception { + PropertyResolver resolver = mock(PropertyResolver.class); + when(resolver.getListProperty("protobufFiles", String.class)) + .thenReturn(Optional.of( + List.of( + ResourceUtils.getFile("classpath:protobuf-serde/sensor.proto").getPath(), + ResourceUtils.getFile("classpath:protobuf-serde/address-book.proto").getPath()))); + + when(resolver.getProperty("protobufMessageName", String.class)) + .thenReturn(Optional.of("test.Sensor")); + + when(resolver.getProperty("protobufMessageNameForKey", String.class)) + .thenReturn(Optional.of("test.AddressBook")); + + when(resolver.getMapProperty("protobufMessageNameByTopic", String.class, String.class)) + .thenReturn(Optional.of( + Map.of( + "topic1", "test.Sensor", + "topic2", "test.AddressBook"))); + + when(resolver.getMapProperty("protobufMessageNameForKeyByTopic", String.class, String.class)) + .thenReturn(Optional.of( + Map.of( + "topic1", "test.Person", + "topic2", "test.AnotherPerson"))); + + var configuration = Configuration.create(resolver); + + assertThat(configuration.defaultMessageDescriptor()) + .matches(d -> d.getFullName().equals("test.Sensor")); + assertThat(configuration.defaultKeyMessageDescriptor()) + .matches(d -> d.getFullName().equals("test.AddressBook")); + + assertThat(configuration.messageDescriptorMap()) + .containsOnlyKeys("topic1", "topic2") + .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo("test.Sensor")) + .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo("test.AddressBook")); + + assertThat(configuration.keyMessageDescriptorMap()) + .containsOnlyKeys("topic1", "topic2") + .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo("test.Person")) + .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo("test.AnotherPerson")); + } + + @Test + void createConfigureFillsDescriptorMappingsWhenProtoFileDirProvided() throws Exception { + PropertyResolver resolver = mock(PropertyResolver.class); + when(resolver.getProperty("protobufFilesDir", String.class)) + .thenReturn(Optional.of(protoFilesDir())); + + when(resolver.getProperty("protobufMessageName", String.class)) + .thenReturn(Optional.of("test.Sensor")); + + when(resolver.getProperty("protobufMessageNameForKey", String.class)) + .thenReturn(Optional.of("test.AddressBook")); + + when(resolver.getMapProperty("protobufMessageNameByTopic", String.class, String.class)) + .thenReturn(Optional.of( + Map.of( + "topic1", "test.Sensor", + "topic2", "test.LanguageDescription"))); + + when(resolver.getMapProperty("protobufMessageNameForKeyByTopic", String.class, String.class)) + .thenReturn(Optional.of( + Map.of( + "topic1", "test.Person", + "topic2", "test.AnotherPerson"))); + + var configuration = Configuration.create(resolver); + + assertThat(configuration.defaultMessageDescriptor()) + .matches(d -> d.getFullName().equals("test.Sensor")); + assertThat(configuration.defaultKeyMessageDescriptor()) + .matches(d -> d.getFullName().equals("test.AddressBook")); + + assertThat(configuration.messageDescriptorMap()) + .containsOnlyKeys("topic1", "topic2") + .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo("test.Sensor")) + .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo("test.LanguageDescription")); + + assertThat(configuration.keyMessageDescriptorMap()) + .containsOnlyKeys("topic1", "topic2") + .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo("test.Person")) + .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo("test.AnotherPerson")); + } + } + + @Test + void deserializeUsesTopicsMappingToFindMsgDescriptor() { + var messageNameMap = Map.of( + "persons", personDescriptor, + "books", addressBookDescriptor, + "langs", langDescriptionDescriptor + ); + var keyMessageNameMap = Map.of( + "books", addressBookDescriptor); + var serde = new ProtobufFileSerde(); + serde.configure( + new Configuration( + null, + null, + descriptorPaths, + messageNameMap, + keyMessageNameMap + ) + ); + + var deserializedPerson = serde.deserializer("persons", Serde.Target.VALUE) + .deserialize(null, personMessageBytes); + assertJsonEquals(samplePersonMsgJson, deserializedPerson.getResult()); + + var deserializedBook = serde.deserializer("books", Serde.Target.KEY) + .deserialize(null, addressBookMessageBytes); + assertJsonEquals(sampleBookMsgJson, deserializedBook.getResult()); + + var deserializedSensor = serde.deserializer("langs", Serde.Target.VALUE) + .deserialize(null, langDescriptionMessageBytes); + assertJsonEquals(sampleLangDescriptionMsgJson, deserializedSensor.getResult()); + } + + @Test + void deserializeUsesDefaultDescriptorIfTopicMappingNotFound() { + var serde = new ProtobufFileSerde(); + serde.configure( + new Configuration( + personDescriptor, + addressBookDescriptor, + descriptorPaths, + Map.of(), + Map.of() + ) + ); + + var deserializedPerson = serde.deserializer("persons", Serde.Target.VALUE) + .deserialize(null, personMessageBytes); + assertJsonEquals(samplePersonMsgJson, deserializedPerson.getResult()); + + var deserializedBook = serde.deserializer("books", Serde.Target.KEY) + .deserialize(null, addressBookMessageBytes); + assertJsonEquals(sampleBookMsgJson, deserializedBook.getResult()); + } + + @Test + void serializeUsesTopicsMappingToFindMsgDescriptor() { + var messageNameMap = Map.of( + "persons", personDescriptor, + "books", addressBookDescriptor, + "langs", langDescriptionDescriptor + ); + var keyMessageNameMap = Map.of( + "books", addressBookDescriptor); + + var serde = new ProtobufFileSerde(); + serde.configure( + new Configuration( + null, + null, + descriptorPaths, + messageNameMap, + keyMessageNameMap + ) + ); + + var personBytes = serde.serializer("langs", Serde.Target.VALUE) + .serialize(sampleLangDescriptionMsgJson); + assertThat(personBytes).isEqualTo(langDescriptionMessageBytes); + + var booksBytes = serde.serializer("books", Serde.Target.KEY) + .serialize(sampleBookMsgJson); + assertThat(booksBytes).isEqualTo(addressBookMessageBytes); + } + + @Test + void serializeUsesDefaultDescriptorIfTopicMappingNotFound() { + var serde = new ProtobufFileSerde(); + serde.configure( + new Configuration( + personDescriptor, + addressBookDescriptor, + descriptorPaths, + Map.of(), + Map.of() + ) + ); + + var personBytes = serde.serializer("persons", Serde.Target.VALUE) + .serialize(samplePersonMsgJson); + assertThat(personBytes).isEqualTo(personMessageBytes); + + var booksBytes = serde.serializer("books", Serde.Target.KEY) + .serialize(sampleBookMsgJson); + assertThat(booksBytes).isEqualTo(addressBookMessageBytes); + } + + @SneakyThrows + private void assertJsonEquals(String expectedJson, String actualJson) { + var mapper = new JsonMapper(); + assertThat(mapper.readTree(actualJson)).isEqualTo(mapper.readTree(expectedJson)); + } +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/ProtobufRawSerdeTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/ProtobufRawSerdeTest.java new file mode 100644 index 00000000000..a71e9969a84 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/ProtobufRawSerdeTest.java @@ -0,0 +1,108 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.google.protobuf.DescriptorProtos; +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; +import com.provectus.kafka.ui.exception.ValidationException; +import com.provectus.kafka.ui.serde.api.Serde; +import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ProtobufRawSerdeTest { + + private static final String DUMMY_TOPIC = "dummy-topic"; + + private ProtobufRawSerde serde; + + @BeforeEach + void init() { + serde = new ProtobufRawSerde(); + } + + @SneakyThrows + ProtobufSchema getSampleSchema() { + return new ProtobufSchema( + """ + syntax = "proto3"; + message Message1 { + int32 my_field = 1; + } + """ + ); + } + + @SneakyThrows + private byte[] getProtobufMessage() { + DynamicMessage.Builder builder = DynamicMessage.newBuilder(getSampleSchema().toDescriptor("Message1")); + builder.setField(builder.getDescriptorForType().findFieldByName("my_field"), 5); + return builder.build().toByteArray(); + } + + @Test + void deserializeSimpleMessage() { + var deserialized = serde.deserializer(DUMMY_TOPIC, Serde.Target.VALUE) + .deserialize(null, getProtobufMessage()); + assertThat(deserialized.getResult()).isEqualTo("1: 5\n"); + } + + @Test + void deserializeEmptyMessage() { + var deserialized = serde.deserializer(DUMMY_TOPIC, Serde.Target.VALUE) + .deserialize(null, new byte[0]); + assertThat(deserialized.getResult()).isEqualTo(""); + } + + @Test + void deserializeInvalidMessage() { + var deserializer = serde.deserializer(DUMMY_TOPIC, Serde.Target.VALUE); + assertThatThrownBy(() -> deserializer.deserialize(null, new byte[] { 1, 2, 3 })) + .isInstanceOf(ValidationException.class) + .hasMessageContaining("Protocol message contained an invalid tag"); + } + + @Test + void deserializeNullMessage() { + var deserializer = serde.deserializer(DUMMY_TOPIC, Serde.Target.VALUE); + assertThatThrownBy(() -> deserializer.deserialize(null, null)) + .isInstanceOf(ValidationException.class) + .hasMessageContaining("Cannot read the array length"); + } + + ProtobufSchema getSampleNestedSchema() { + return new ProtobufSchema( + """ + syntax = "proto3"; + message Message2 { + int32 my_nested_field = 1; + } + message Message1 { + int32 my_field = 1; + Message2 my_nested_message = 2; + } + """ + ); + } + + @SneakyThrows + private byte[] getComplexProtobufMessage() { + DynamicMessage.Builder builder = DynamicMessage.newBuilder(getSampleNestedSchema().toDescriptor("Message1")); + builder.setField(builder.getDescriptorForType().findFieldByName("my_field"), 5); + DynamicMessage.Builder nestedBuilder = DynamicMessage.newBuilder(getSampleNestedSchema().toDescriptor("Message2")); + nestedBuilder.setField(nestedBuilder.getDescriptorForType().findFieldByName("my_nested_field"), 10); + builder.setField(builder.getDescriptorForType().findFieldByName("my_nested_message"), nestedBuilder.build()); + + return builder.build().toByteArray(); + } + + @Test + void deserializeNestedMessage() { + var deserialized = serde.deserializer(DUMMY_TOPIC, Serde.Target.VALUE) + .deserialize(null, getComplexProtobufMessage()); + assertThat(deserialized.getResult()).isEqualTo("1: 5\n2: {\n 1: 10\n}\n"); + } +} \ No newline at end of file diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/UInt32SerdeTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/UInt32SerdeTest.java new file mode 100644 index 00000000000..13ad3ed5187 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/UInt32SerdeTest.java @@ -0,0 +1,59 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.google.common.primitives.Ints; +import com.google.common.primitives.UnsignedInteger; +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.Serde; +import com.provectus.kafka.ui.serdes.PropertyResolverImpl; +import com.provectus.kafka.ui.serdes.RecordHeadersImpl; +import org.apache.kafka.common.header.internals.RecordHeaders; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class UInt32SerdeTest { + + private UInt32Serde serde; + + @BeforeEach + void init() { + serde = new UInt32Serde(); + serde.configure( + PropertyResolverImpl.empty(), + PropertyResolverImpl.empty(), + PropertyResolverImpl.empty() + ); + } + + @ParameterizedTest + @EnumSource + void serializeUses4BytesUInt32Representation(Serde.Target type) { + var serializer = serde.serializer("anyTopic", type); + String uint32String = UnsignedInteger.MAX_VALUE.toString(); + byte[] bytes = serializer.serialize(uint32String); + assertThat(bytes).isEqualTo(Ints.toByteArray(UnsignedInteger.MAX_VALUE.intValue())); + } + + @ParameterizedTest + @EnumSource + void serializeThrowsNfeIfNegativeValuePassed(Serde.Target type) { + var serializer = serde.serializer("anyTopic", type); + String negativeIntString = "-100"; + assertThatThrownBy(() -> serializer.serialize(negativeIntString)) + .isInstanceOf(NumberFormatException.class); + } + + @ParameterizedTest + @EnumSource + void deserializeUses4BytesUInt32Representation(Serde.Target type) { + var deserializer = serde.deserializer("anyTopic", type); + byte[] uint32Bytes = Ints.toByteArray(UnsignedInteger.MAX_VALUE.intValue()); + var result = deserializer.deserialize(new RecordHeadersImpl(), uint32Bytes); + assertThat(result.getResult()).isEqualTo(UnsignedInteger.MAX_VALUE.toString()); + assertThat(result.getType()).isEqualTo(DeserializeResult.Type.JSON); + } + +} \ No newline at end of file diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/UInt64SerdeTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/UInt64SerdeTest.java new file mode 100644 index 00000000000..b155e57de97 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/UInt64SerdeTest.java @@ -0,0 +1,58 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.google.common.primitives.Longs; +import com.google.common.primitives.UnsignedLong; +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.Serde; +import com.provectus.kafka.ui.serdes.PropertyResolverImpl; +import com.provectus.kafka.ui.serdes.RecordHeadersImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class UInt64SerdeTest { + + private UInt64Serde serde; + + @BeforeEach + void init() { + serde = new UInt64Serde(); + serde.configure( + PropertyResolverImpl.empty(), + PropertyResolverImpl.empty(), + PropertyResolverImpl.empty() + ); + } + + @ParameterizedTest + @EnumSource + void serializeUses8BytesUInt64Representation(Serde.Target type) { + var serializer = serde.serializer("anyTopic", type); + String uint64String = UnsignedLong.MAX_VALUE.toString(); + byte[] bytes = serializer.serialize(uint64String); + assertThat(bytes).isEqualTo(Longs.toByteArray(UnsignedLong.MAX_VALUE.longValue())); + } + + @ParameterizedTest + @EnumSource + void serializeThrowsNfeIfNegativeValuePassed(Serde.Target type) { + var serializer = serde.serializer("anyTopic", type); + String negativeIntString = "-100"; + assertThatThrownBy(() -> serializer.serialize(negativeIntString)) + .isInstanceOf(NumberFormatException.class); + } + + @ParameterizedTest + @EnumSource + void deserializeUses8BytesUIn64tRepresentation(Serde.Target type) { + var deserializer = serde.deserializer("anyTopic", type); + byte[] uint64Bytes = Longs.toByteArray(UnsignedLong.MAX_VALUE.longValue()); + var result = deserializer.deserialize(new RecordHeadersImpl(), uint64Bytes); + assertThat(result.getResult()).isEqualTo(UnsignedLong.MAX_VALUE.toString()); + assertThat(result.getType()).isEqualTo(DeserializeResult.Type.JSON); + } + +} \ No newline at end of file diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/UuidBinarySerdeTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/UuidBinarySerdeTest.java new file mode 100644 index 00000000000..5f387c4942c --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/UuidBinarySerdeTest.java @@ -0,0 +1,101 @@ +package com.provectus.kafka.ui.serdes.builtin; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.Serde; +import com.provectus.kafka.ui.serdes.PropertyResolverImpl; +import com.provectus.kafka.ui.serdes.RecordHeadersImpl; +import java.nio.ByteBuffer; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.mock.env.MockEnvironment; + +class UuidBinarySerdeTest { + + @Nested + class MsbFirst { + + private UuidBinarySerde serde; + + @BeforeEach + void init() { + serde = new UuidBinarySerde(); + serde.configure( + PropertyResolverImpl.empty(), + PropertyResolverImpl.empty(), + PropertyResolverImpl.empty() + ); + } + + @ParameterizedTest + @EnumSource + void serializerUses16bytesUuidBinaryRepresentation(Serde.Target type) { + var serializer = serde.serializer("anyTopic", type); + var uuid = UUID.randomUUID(); + byte[] bytes = serializer.serialize(uuid.toString()); + var bb = ByteBuffer.wrap(bytes); + assertThat(bb.getLong()).isEqualTo(uuid.getMostSignificantBits()); + assertThat(bb.getLong()).isEqualTo(uuid.getLeastSignificantBits()); + } + + @ParameterizedTest + @EnumSource + void deserializerUses16bytesUuidBinaryRepresentation(Serde.Target type) { + var uuid = UUID.randomUUID(); + var bb = ByteBuffer.allocate(16); + bb.putLong(uuid.getMostSignificantBits()); + bb.putLong(uuid.getLeastSignificantBits()); + + var result = serde.deserializer("anyTopic", type).deserialize(new RecordHeadersImpl(), bb.array()); + assertThat(result.getType()).isEqualTo(DeserializeResult.Type.STRING); + assertThat(result.getAdditionalProperties()).isEmpty(); + assertThat(result.getResult()).isEqualTo(uuid.toString()); + } + } + + @Nested + class MsbLast { + + private UuidBinarySerde serde; + + @BeforeEach + void init() { + serde = new UuidBinarySerde(); + serde.configure( + new PropertyResolverImpl(new MockEnvironment().withProperty("mostSignificantBitsFirst", "false")), + PropertyResolverImpl.empty(), + PropertyResolverImpl.empty() + ); + } + + @ParameterizedTest + @EnumSource + void serializerUses16bytesUuidBinaryRepresentation(Serde.Target type) { + var serializer = serde.serializer("anyTopic", type); + var uuid = UUID.randomUUID(); + byte[] bytes = serializer.serialize(uuid.toString()); + var bb = ByteBuffer.wrap(bytes); + assertThat(bb.getLong()).isEqualTo(uuid.getLeastSignificantBits()); + assertThat(bb.getLong()).isEqualTo(uuid.getMostSignificantBits()); + } + + @ParameterizedTest + @EnumSource + void deserializerUses16bytesUuidBinaryRepresentation(Serde.Target type) { + var uuid = UUID.randomUUID(); + var bb = ByteBuffer.allocate(16); + bb.putLong(uuid.getLeastSignificantBits()); + bb.putLong(uuid.getMostSignificantBits()); + + var result = serde.deserializer("anyTopic", type).deserialize(new RecordHeadersImpl(), bb.array()); + assertThat(result.getType()).isEqualTo(DeserializeResult.Type.STRING); + assertThat(result.getAdditionalProperties()).isEmpty(); + assertThat(result.getResult()).isEqualTo(uuid.toString()); + } + } + +} \ No newline at end of file diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/sr/SchemaRegistrySerdeTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/sr/SchemaRegistrySerdeTest.java new file mode 100644 index 00000000000..b70450cea5c --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/sr/SchemaRegistrySerdeTest.java @@ -0,0 +1,385 @@ +package com.provectus.kafka.ui.serdes.builtin.sr; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.provectus.kafka.ui.serde.api.DeserializeResult; +import com.provectus.kafka.ui.serde.api.SchemaDescription; +import com.provectus.kafka.ui.serde.api.Serde; +import com.provectus.kafka.ui.util.jsonschema.JsonAvroConversion; +import io.confluent.kafka.schemaregistry.avro.AvroSchema; +import io.confluent.kafka.schemaregistry.client.MockSchemaRegistryClient; +import io.confluent.kafka.schemaregistry.client.rest.exceptions.RestClientException; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import lombok.SneakyThrows; +import net.bytebuddy.utility.RandomString; +import org.apache.avro.generic.GenericDatumWriter; +import org.apache.avro.io.Encoder; +import org.apache.avro.io.EncoderFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class SchemaRegistrySerdeTest { + + private final MockSchemaRegistryClient registryClient = new MockSchemaRegistryClient(); + + private SchemaRegistrySerde serde; + + @BeforeEach + void init() { + serde = new SchemaRegistrySerde(); + serde.configure(List.of("wontbeused"), registryClient, "%s-key", "%s-value", true); + } + + @ParameterizedTest + @CsvSource({ + "test_topic, test_topic-key, KEY", + "test_topic, test_topic-value, VALUE" + }) + @SneakyThrows + void returnsSchemaDescriptionIfSchemaRegisteredInSR(String topic, String subject, Serde.Target target) { + int schemaId = registryClient.register(subject, new AvroSchema("{ \"type\": \"int\" }")); + int registeredVersion = registryClient.getLatestSchemaMetadata(subject).getVersion(); + + var schemaOptional = serde.getSchema(topic, target); + assertThat(schemaOptional).isPresent(); + + SchemaDescription schemaDescription = schemaOptional.get(); + assertThat(schemaDescription.getSchema()) + .contains( + "{\"$id\":\"int\",\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"type\":\"integer\"}"); + assertThat(schemaDescription.getAdditionalProperties()) + .containsOnlyKeys("subject", "schemaId", "latestVersion", "type") + .containsEntry("subject", subject) + .containsEntry("schemaId", schemaId) + .containsEntry("latestVersion", registeredVersion) + .containsEntry("type", "AVRO"); + } + + @Test + void returnsEmptyDescriptorIfSchemaNotRegisteredInSR() { + String topic = "test"; + assertThat(serde.getSchema(topic, Serde.Target.KEY)).isEmpty(); + assertThat(serde.getSchema(topic, Serde.Target.VALUE)).isEmpty(); + } + + @Test + void serializeTreatsInputAsJsonAvroSchemaPayload() throws RestClientException, IOException { + AvroSchema schema = new AvroSchema( + "{" + + " \"type\": \"record\"," + + " \"name\": \"TestAvroRecord1\"," + + " \"fields\": [" + + " {" + + " \"name\": \"field1\"," + + " \"type\": \"string\"" + + " }," + + " {" + + " \"name\": \"field2\"," + + " \"type\": \"int\"" + + " }" + + " ]" + + "}" + ); + String jsonValue = "{ \"field1\":\"testStr\", \"field2\": 123 }"; + String topic = "test"; + + int schemaId = registryClient.register(topic + "-value", schema); + byte[] serialized = serde.serializer(topic, Serde.Target.VALUE).serialize(jsonValue); + byte[] expected = toBytesWithMagicByteAndSchemaId(schemaId, jsonValue, schema); + assertThat(serialized).isEqualTo(expected); + } + + @Test + void deserializeReturnsJsonAvroMsgJsonRepresentation() throws RestClientException, IOException { + AvroSchema schema = new AvroSchema( + "{" + + " \"type\": \"record\"," + + " \"name\": \"TestAvroRecord1\"," + + " \"fields\": [" + + " {" + + " \"name\": \"field1\"," + + " \"type\": \"string\"" + + " }," + + " {" + + " \"name\": \"field2\"," + + " \"type\": \"int\"" + + " }" + + " ]" + + "}" + ); + String jsonValue = "{ \"field1\":\"testStr\", \"field2\": 123 }"; + + String topic = "test"; + int schemaId = registryClient.register(topic + "-value", schema); + + byte[] data = toBytesWithMagicByteAndSchemaId(schemaId, jsonValue, schema); + var result = serde.deserializer(topic, Serde.Target.VALUE).deserialize(null, data); + + assertJsonsEqual(jsonValue, result.getResult()); + assertThat(result.getType()).isEqualTo(DeserializeResult.Type.JSON); + assertThat(result.getAdditionalProperties()) + .contains(Map.entry("type", "AVRO")) + .contains(Map.entry("schemaId", schemaId)); + } + + @Nested + class SerdeWithDisabledSubjectExistenceCheck { + + @BeforeEach + void init() { + serde.configure(List.of("wontbeused"), registryClient, "%s-key", "%s-value", false); + } + + @Test + void canDeserializeAlwaysReturnsTrue() { + String topic = RandomString.make(10); + assertThat(serde.canDeserialize(topic, Serde.Target.KEY)).isTrue(); + assertThat(serde.canDeserialize(topic, Serde.Target.VALUE)).isTrue(); + } + } + + @Nested + class SerdeWithEnabledSubjectExistenceCheck { + + @BeforeEach + void init() { + serde.configure(List.of("wontbeused"), registryClient, "%s-key", "%s-value", true); + } + + @Test + void canDeserializeReturnsTrueIfSubjectExists() throws Exception { + String topic = RandomString.make(10); + registryClient.register(topic + "-key", new AvroSchema("\"int\"")); + registryClient.register(topic + "-value", new AvroSchema("\"int\"")); + + assertThat(serde.canDeserialize(topic, Serde.Target.KEY)).isTrue(); + assertThat(serde.canDeserialize(topic, Serde.Target.VALUE)).isTrue(); + } + + @Test + void canDeserializeReturnsFalseIfSubjectDoesNotExist() { + String topic = RandomString.make(10); + assertThat(serde.canDeserialize(topic, Serde.Target.KEY)).isFalse(); + assertThat(serde.canDeserialize(topic, Serde.Target.VALUE)).isFalse(); + } + } + + @Test + void canDeserializeAndCanSerializeReturnsTrueIfSubjectExists() throws Exception { + String topic = RandomString.make(10); + registryClient.register(topic + "-key", new AvroSchema("\"int\"")); + registryClient.register(topic + "-value", new AvroSchema("\"int\"")); + + assertThat(serde.canSerialize(topic, Serde.Target.KEY)).isTrue(); + assertThat(serde.canSerialize(topic, Serde.Target.VALUE)).isTrue(); + } + + @Test + void canSerializeReturnsFalseIfSubjectDoesNotExist() { + String topic = RandomString.make(10); + assertThat(serde.canSerialize(topic, Serde.Target.KEY)).isFalse(); + assertThat(serde.canSerialize(topic, Serde.Target.VALUE)).isFalse(); + } + + @SneakyThrows + private void assertJsonsEqual(String expected, String actual) { + var mapper = new JsonMapper(); + assertThat(mapper.readTree(actual)).isEqualTo(mapper.readTree(expected)); + } + + private byte[] toBytesWithMagicByteAndSchemaId(int schemaId, String json, AvroSchema schema) { + return toBytesWithMagicByteAndSchemaId(schemaId, jsonToAvro(json, schema)); + } + + private byte[] toBytesWithMagicByteAndSchemaId(int schemaId, byte[] body) { + return ByteBuffer.allocate(1 + 4 + body.length) + .put((byte) 0) + .putInt(schemaId) + .put(body) + .array(); + } + + @SneakyThrows + private byte[] jsonToAvro(String json, AvroSchema schema) { + GenericDatumWriter writer = new GenericDatumWriter<>(schema.rawSchema()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + Encoder encoder = EncoderFactory.get().binaryEncoder(output, null); + writer.write(JsonAvroConversion.convertJsonToAvro(json, schema.rawSchema()), encoder); + encoder.flush(); + return output.toByteArray(); + } + + @Test + void avroFieldsRepresentationIsConsistentForSerializationAndDeserialization() throws Exception { + AvroSchema schema = new AvroSchema( + """ + { + "type": "record", + "name": "TestAvroRecord", + "fields": [ + { + "name": "f_int", + "type": "int" + }, + { + "name": "f_long", + "type": "long" + }, + { + "name": "f_string", + "type": "string" + }, + { + "name": "f_boolean", + "type": "boolean" + }, + { + "name": "f_float", + "type": "float" + }, + { + "name": "f_double", + "type": "double" + }, + { + "name": "f_enum", + "type" : { + "type": "enum", + "name": "Suit", + "symbols" : ["SPADES", "HEARTS", "DIAMONDS", "CLUBS"] + } + }, + { + "name": "f_map", + "type": { + "type": "map", + "values" : "string", + "default": {} + } + }, + { + "name": "f_union", + "type": ["null", "string", "int" ] + }, + { + "name": "f_optional_to_test_not_filled_case", + "type": [ "null", "string"] + }, + { + "name" : "f_fixed", + "type" : { "type" : "fixed" ,"size" : 8, "name": "long_encoded" } + }, + { + "name" : "f_bytes", + "type": "bytes" + } + ] + }""" + ); + + String jsonPayload = """ + { + "f_int": 123, + "f_long": 4294967294, + "f_string": "string here", + "f_boolean": true, + "f_float": 123.1, + "f_double": 123456.123456, + "f_enum": "SPADES", + "f_map": { "k1": "string value" }, + "f_union": { "int": 123 }, + "f_fixed": "\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0004Ò", + "f_bytes": "\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\t)" + } + """; + + registryClient.register("test-value", schema); + assertSerdeCycle("test", jsonPayload); + } + + @Test + void avroLogicalTypesRepresentationIsConsistentForSerializationAndDeserialization() throws Exception { + AvroSchema schema = new AvroSchema( + """ + { + "type": "record", + "name": "TestAvroRecord", + "fields": [ + { + "name": "lt_date", + "type": { "type": "int", "logicalType": "date" } + }, + { + "name": "lt_uuid", + "type": { "type": "string", "logicalType": "uuid" } + }, + { + "name": "lt_decimal", + "type": { "type": "bytes", "logicalType": "decimal", "precision": 22, "scale":10 } + }, + { + "name": "lt_time_millis", + "type": { "type": "int", "logicalType": "time-millis"} + }, + { + "name": "lt_time_micros", + "type": { "type": "long", "logicalType": "time-micros"} + }, + { + "name": "lt_timestamp_millis", + "type": { "type": "long", "logicalType": "timestamp-millis" } + }, + { + "name": "lt_timestamp_micros", + "type": { "type": "long", "logicalType": "timestamp-micros" } + }, + { + "name": "lt_local_timestamp_millis", + "type": { "type": "long", "logicalType": "local-timestamp-millis" } + }, + { + "name": "lt_local_timestamp_micros", + "type": { "type": "long", "logicalType": "local-timestamp-micros" } + } + ] + }""" + ); + + String jsonPayload = """ + { + "lt_date":"1991-08-14", + "lt_decimal": 2.1617413862327545E11, + "lt_time_millis": "10:15:30.001", + "lt_time_micros": "10:15:30.123456", + "lt_uuid": "a37b75ca-097c-5d46-6119-f0637922e908", + "lt_timestamp_millis": "2007-12-03T10:15:30.123Z", + "lt_timestamp_micros": "2007-12-03T10:15:30.123456Z", + "lt_local_timestamp_millis": "2017-12-03T10:15:30.123", + "lt_local_timestamp_micros": "2017-12-03T10:15:30.123456" + } + """; + + registryClient.register("test-value", schema); + assertSerdeCycle("test", jsonPayload); + } + + // 1. serialize input json to binary + // 2. deserialize from binary + // 3. check that deserialized version equal to input + void assertSerdeCycle(String topic, String jsonInput) { + byte[] serializedBytes = serde.serializer(topic, Serde.Target.VALUE).serialize(jsonInput); + var deserializedJson = serde.deserializer(topic, Serde.Target.VALUE) + .deserialize(null, serializedBytes) + .getResult(); + assertJsonsEqual(jsonInput, deserializedJson); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/BrokerServiceTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/BrokerServiceTest.java index 33e90e3b0b5..62a6a0fc752 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/BrokerServiceTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/BrokerServiceTest.java @@ -1,7 +1,6 @@ package com.provectus.kafka.ui.service; import com.provectus.kafka.ui.AbstractIntegrationTest; -import com.provectus.kafka.ui.model.BrokerDTO; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import reactor.test.StepVerifier; @@ -16,14 +15,11 @@ class BrokerServiceTest extends AbstractIntegrationTest { @Test void getBrokersReturnsFilledBrokerDto() { - BrokerDTO expectedBroker = new BrokerDTO(); - expectedBroker.setId(1); - expectedBroker.setHost(kafka.getHost()); - expectedBroker.setPort(kafka.getFirstMappedPort()); - var localCluster = clustersStorage.getClusterByName(LOCAL).get(); StepVerifier.create(brokerService.getBrokers(localCluster)) - .expectNext(expectedBroker) + .expectNextMatches(b -> b.getId().equals(1) + && b.getHost().equals(kafka.getHost()) + && b.getPort().equals(kafka.getFirstMappedPort())) .verifyComplete(); } diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ConfigTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ConfigTest.java index 290a926ccf2..6bc114b4119 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ConfigTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ConfigTest.java @@ -4,10 +4,13 @@ import com.provectus.kafka.ui.AbstractIntegrationTest; import com.provectus.kafka.ui.model.BrokerConfigDTO; +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.ServerStatusDTO; import java.time.Duration; import java.util.List; import java.util.Map; import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.ParameterizedTypeReference; @@ -19,6 +22,18 @@ public class ConfigTest extends AbstractIntegrationTest { @Autowired private WebTestClient webTestClient; + @BeforeEach + void waitUntilStatsInitialized() { + Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInSameThread() + .until(() -> { + var stats = applicationContext.getBean(StatisticsCache.class) + .get(KafkaCluster.builder().name(LOCAL).build()); + return stats.getStatus() == ServerStatusDTO.ONLINE; + }); + } + @Test public void testAlterConfig() { String name = "background.threads"; diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/KafkaConfigSanitizerTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/KafkaConfigSanitizerTest.java index 0cc5a36c068..9cab6b2f13f 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/KafkaConfigSanitizerTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/KafkaConfigSanitizerTest.java @@ -3,15 +3,16 @@ import static org.assertj.core.api.Assertions.assertThat; import java.util.Arrays; -import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; -import org.springframework.boot.actuate.endpoint.Sanitizer; class KafkaConfigSanitizerTest { @Test void doNothingIfEnabledPropertySetToFalse() { - final Sanitizer sanitizer = new KafkaConfigSanitizer(false, Collections.emptyList()); + final var sanitizer = new KafkaConfigSanitizer(false, List.of()); assertThat(sanitizer.sanitize("password", "secret")).isEqualTo("secret"); assertThat(sanitizer.sanitize("sasl.jaas.config", "secret")).isEqualTo("secret"); assertThat(sanitizer.sanitize("database.password", "secret")).isEqualTo("secret"); @@ -19,18 +20,25 @@ void doNothingIfEnabledPropertySetToFalse() { @Test void obfuscateCredentials() { - final Sanitizer sanitizer = new KafkaConfigSanitizer(true, Collections.emptyList()); + final var sanitizer = new KafkaConfigSanitizer(true, List.of()); assertThat(sanitizer.sanitize("sasl.jaas.config", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("consumer.sasl.jaas.config", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("producer.sasl.jaas.config", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("main.consumer.sasl.jaas.config", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("database.password", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("basic.auth.user.info", "secret")).isEqualTo("******"); + + //AWS var sanitizing + assertThat(sanitizer.sanitize("aws.access.key.id", "secret")).isEqualTo("******"); + assertThat(sanitizer.sanitize("aws.accessKeyId", "secret")).isEqualTo("******"); + assertThat(sanitizer.sanitize("aws.secret.access.key", "secret")).isEqualTo("******"); + assertThat(sanitizer.sanitize("aws.secretAccessKey", "secret")).isEqualTo("******"); + assertThat(sanitizer.sanitize("aws.sessionToken", "secret")).isEqualTo("******"); } @Test void notObfuscateNormalConfigs() { - final Sanitizer sanitizer = new KafkaConfigSanitizer(true, Collections.emptyList()); + final var sanitizer = new KafkaConfigSanitizer(true, List.of()); assertThat(sanitizer.sanitize("security.protocol", "SASL_SSL")).isEqualTo("SASL_SSL"); final String[] bootstrapServer = new String[] {"test1:9092", "test2:9092"}; assertThat(sanitizer.sanitize("bootstrap.servers", bootstrapServer)).isEqualTo(bootstrapServer); @@ -38,7 +46,7 @@ void notObfuscateNormalConfigs() { @Test void obfuscateCredentialsWithDefinedPatterns() { - final Sanitizer sanitizer = new KafkaConfigSanitizer(true, Arrays.asList("kafka.ui", ".*test.*")); + final var sanitizer = new KafkaConfigSanitizer(true, Arrays.asList("kafka.ui", ".*test.*")); assertThat(sanitizer.sanitize("consumer.kafka.ui", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("this.is.test.credentials", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("this.is.not.credential", "not.credential")) @@ -46,4 +54,22 @@ void obfuscateCredentialsWithDefinedPatterns() { assertThat(sanitizer.sanitize("database.password", "no longer credential")) .isEqualTo("no longer credential"); } + + @Test + void sanitizeConnectorConfigDoNotFailOnNullableValues() { + Map originalConfig = new HashMap<>(); + originalConfig.put("password", "secret"); + originalConfig.put("asIs", "normal"); + originalConfig.put("nullVal", null); + + var sanitizedConfig = new KafkaConfigSanitizer(true, List.of()) + .sanitizeConnectorConfig(originalConfig); + + assertThat(sanitizedConfig) + .hasSize(3) + .containsEntry("password", "******") + .containsEntry("asIs", "normal") + .containsEntry("nullVal", null); + } + } diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/KsqlServiceTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/KsqlServiceTest.java deleted file mode 100644 index f41b595e791..00000000000 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/KsqlServiceTest.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.provectus.kafka.ui.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.provectus.kafka.ui.client.KsqlClient; -import com.provectus.kafka.ui.exception.KsqlDbNotFoundException; -import com.provectus.kafka.ui.exception.UnprocessableEntityException; -import com.provectus.kafka.ui.model.KafkaCluster; -import com.provectus.kafka.ui.model.KsqlCommandDTO; -import com.provectus.kafka.ui.model.KsqlCommandResponseDTO; -import com.provectus.kafka.ui.strategy.ksql.statement.BaseStrategy; -import com.provectus.kafka.ui.strategy.ksql.statement.DescribeStrategy; -import com.provectus.kafka.ui.strategy.ksql.statement.ShowStrategy; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -@ExtendWith(MockitoExtension.class) -class KsqlServiceTest { - private KsqlService ksqlService; - private BaseStrategy baseStrategy; - private BaseStrategy alternativeStrategy; - - @Mock - private ClustersStorage clustersStorage; - @Mock - private KsqlClient ksqlClient; - - - @BeforeEach - public void setUp() { - this.baseStrategy = new ShowStrategy(); - this.alternativeStrategy = new DescribeStrategy(); - this.ksqlService = new KsqlService( - this.ksqlClient, - List.of(baseStrategy, alternativeStrategy) - ); - } - - @Test - void shouldThrowKsqlDbNotFoundExceptionOnExecuteKsqlCommand() { - KsqlCommandDTO command = (new KsqlCommandDTO()).ksql("show streams;"); - KafkaCluster kafkaCluster = Mockito.mock(KafkaCluster.class); - when(kafkaCluster.getKsqldbServer()).thenReturn(null); - - StepVerifier.create(ksqlService.executeKsqlCommand(kafkaCluster, Mono.just(command))) - .verifyError(KsqlDbNotFoundException.class); - } - - @Test - void shouldThrowUnprocessableEntityExceptionOnExecuteKsqlCommand() { - KsqlCommandDTO command = - (new KsqlCommandDTO()).ksql("CREATE STREAM users WITH (KAFKA_TOPIC='users');"); - KafkaCluster kafkaCluster = Mockito.mock(KafkaCluster.class); - when(kafkaCluster.getKsqldbServer()).thenReturn("localhost:8088"); - - StepVerifier.create(ksqlService.executeKsqlCommand(kafkaCluster, Mono.just(command))) - .verifyError(UnprocessableEntityException.class); - - StepVerifier.create(ksqlService.executeKsqlCommand(kafkaCluster, Mono.just(command))) - .verifyErrorMessage("Invalid sql"); - } - - @Test - void shouldSetHostToStrategy() { - String host = "localhost:8088"; - KsqlCommandDTO command = (new KsqlCommandDTO()).ksql("describe streams;"); - KafkaCluster kafkaCluster = Mockito.mock(KafkaCluster.class); - - when(kafkaCluster.getKsqldbServer()).thenReturn(host); - when(ksqlClient.execute(any())).thenReturn(Mono.just(new KsqlCommandResponseDTO())); - - ksqlService.executeKsqlCommand(kafkaCluster, Mono.just(command)).block(); - assertThat(alternativeStrategy.getUri()).isEqualTo(host + "/ksql"); - } - - @Test - void shouldCallClientAndReturnResponse() { - KsqlCommandDTO command = (new KsqlCommandDTO()).ksql("describe streams;"); - KafkaCluster kafkaCluster = Mockito.mock(KafkaCluster.class); - KsqlCommandResponseDTO response = new KsqlCommandResponseDTO().message("success"); - - when(kafkaCluster.getKsqldbServer()).thenReturn("host"); - when(ksqlClient.execute(any())).thenReturn(Mono.just(response)); - - KsqlCommandResponseDTO receivedResponse = - ksqlService.executeKsqlCommand(kafkaCluster, Mono.just(command)).block(); - verify(ksqlClient, times(1)).execute(alternativeStrategy); - assertThat(receivedResponse).isEqualTo(response); - - } -} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/MessagesServiceTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/MessagesServiceTest.java index e866bb28c2b..cb50c0eb818 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/MessagesServiceTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/MessagesServiceTest.java @@ -1,18 +1,33 @@ package com.provectus.kafka.ui.service; +import static com.provectus.kafka.ui.service.MessagesService.execSmartFilterTest; +import static org.assertj.core.api.Assertions.assertThat; + import com.provectus.kafka.ui.AbstractIntegrationTest; import com.provectus.kafka.ui.exception.TopicNotFoundException; +import com.provectus.kafka.ui.model.ConsumerPosition; import com.provectus.kafka.ui.model.CreateTopicMessageDTO; import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.SeekDirectionDTO; +import com.provectus.kafka.ui.model.SeekTypeDTO; +import com.provectus.kafka.ui.model.SmartFilterTestExecutionDTO; +import com.provectus.kafka.ui.model.TopicMessageDTO; +import com.provectus.kafka.ui.model.TopicMessageEventDTO; +import com.provectus.kafka.ui.producer.KafkaTestProducer; +import com.provectus.kafka.ui.serdes.builtin.StringSerde; import java.util.List; +import java.util.Map; import java.util.UUID; +import org.apache.kafka.clients.admin.NewTopic; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import reactor.core.publisher.Flux; import reactor.test.StepVerifier; class MessagesServiceTest extends AbstractIntegrationTest { + private static final String MASKED_TOPICS_PREFIX = "masking-test-"; private static final String NON_EXISTING_TOPIC = UUID.randomUUID().toString(); @Autowired @@ -44,9 +59,77 @@ void sendMessageReturnsExceptionWhenTopicNotFound() { @Test void loadMessagesReturnsExceptionWhenTopicNotFound() { - StepVerifier.create(messagesService.loadMessages(cluster, NON_EXISTING_TOPIC, null, null, null, 1)) + StepVerifier.create(messagesService + .loadMessages(cluster, NON_EXISTING_TOPIC, null, null, null, 1, null, "String", "String")) .expectError(TopicNotFoundException.class) .verify(); } -} \ No newline at end of file + @Test + void maskingAppliedOnConfiguredClusters() throws Exception { + String testTopic = MASKED_TOPICS_PREFIX + UUID.randomUUID(); + try (var producer = KafkaTestProducer.forKafka(kafka)) { + createTopic(new NewTopic(testTopic, 1, (short) 1)); + producer.send(testTopic, "message1"); + producer.send(testTopic, "message2").get(); + + Flux msgsFlux = messagesService.loadMessages( + cluster, + testTopic, + new ConsumerPosition(SeekTypeDTO.BEGINNING, testTopic, null), + null, + null, + 100, + SeekDirectionDTO.FORWARD, + StringSerde.name(), + StringSerde.name() + ).filter(evt -> evt.getType() == TopicMessageEventDTO.TypeEnum.MESSAGE) + .map(TopicMessageEventDTO::getMessage); + + // both messages should be masked + StepVerifier.create(msgsFlux) + .expectNextMatches(msg -> msg.getContent().equals("***")) + .expectNextMatches(msg -> msg.getContent().equals("***")) + .verifyComplete(); + } finally { + deleteTopic(testTopic); + } + } + + @Test + void execSmartFilterTestReturnsExecutionResult() { + var params = new SmartFilterTestExecutionDTO() + .filterCode("key != null && value != null && headers != null && timestampMs != null && offset != null") + .key("1234") + .value("{ \"some\" : \"value\" } ") + .headers(Map.of("h1", "hv1")) + .offset(12345L) + .timestampMs(System.currentTimeMillis()) + .partition(1); + assertThat(execSmartFilterTest(params).getResult()).isTrue(); + + params.setFilterCode("return false"); + assertThat(execSmartFilterTest(params).getResult()).isFalse(); + } + + @Test + void execSmartFilterTestReturnsErrorOnFilterApplyError() { + var result = execSmartFilterTest( + new SmartFilterTestExecutionDTO() + .filterCode("return 1/0") + ); + assertThat(result.getResult()).isNull(); + assertThat(result.getError()).containsIgnoringCase("execution error"); + } + + @Test + void execSmartFilterTestReturnsErrorOnFilterCompilationError() { + var result = execSmartFilterTest( + new SmartFilterTestExecutionDTO() + .filterCode("this is invalid groovy syntax = 1") + ); + assertThat(result.getResult()).isNull(); + assertThat(result.getError()).containsIgnoringCase("Compilation error"); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/OffsetsResetServiceTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/OffsetsResetServiceTest.java index 966e0ec7636..fa73f0bff65 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/OffsetsResetServiceTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/OffsetsResetServiceTest.java @@ -37,24 +37,16 @@ public class OffsetsResetServiceTest extends AbstractIntegrationTest { private static final int PARTITIONS = 5; - private static final KafkaCluster CLUSTER = - KafkaCluster.builder() - .name(LOCAL) - .bootstrapServers(kafka.getBootstrapServers()) - .properties(new Properties()) - .build(); - private final String groupId = "OffsetsResetServiceTestGroup-" + UUID.randomUUID(); private final String topic = "OffsetsResetServiceTestTopic-" + UUID.randomUUID(); + private KafkaCluster cluster; private OffsetsResetService offsetsResetService; @BeforeEach void init() { - AdminClientServiceImpl adminClientService = new AdminClientServiceImpl(); - adminClientService.setClientTimeout(5_000); - offsetsResetService = new OffsetsResetService(adminClientService); - + cluster = applicationContext.getBean(ClustersStorage.class).getClusterByName(LOCAL).get(); + offsetsResetService = new OffsetsResetService(applicationContext.getBean(AdminClientService.class)); createTopic(new NewTopic(topic, PARTITIONS, (short) 1)); createConsumerGroup(); } @@ -76,13 +68,13 @@ private void createConsumerGroup() { void failsIfGroupDoesNotExists() { List> expectedNotFound = List.of( offsetsResetService - .resetToEarliest(CLUSTER, "non-existing-group", topic, null), + .resetToEarliest(cluster, "non-existing-group", topic, null), offsetsResetService - .resetToLatest(CLUSTER, "non-existing-group", topic, null), + .resetToLatest(cluster, "non-existing-group", topic, null), offsetsResetService - .resetToTimestamp(CLUSTER, "non-existing-group", topic, null, System.currentTimeMillis()), + .resetToTimestamp(cluster, "non-existing-group", topic, null, System.currentTimeMillis()), offsetsResetService - .resetToOffsets(CLUSTER, "non-existing-group", topic, Map.of()) + .resetToOffsets(cluster, "non-existing-group", topic, Map.of()) ); for (Mono mono : expectedNotFound) { @@ -101,11 +93,11 @@ void failsIfGroupIsActive() { consumer.poll(Duration.ofMillis(100)); List> expectedValidationError = List.of( - offsetsResetService.resetToEarliest(CLUSTER, groupId, topic, null), - offsetsResetService.resetToLatest(CLUSTER, groupId, topic, null), + offsetsResetService.resetToEarliest(cluster, groupId, topic, null), + offsetsResetService.resetToLatest(cluster, groupId, topic, null), offsetsResetService - .resetToTimestamp(CLUSTER, groupId, topic, null, System.currentTimeMillis()), - offsetsResetService.resetToOffsets(CLUSTER, groupId, topic, Map.of()) + .resetToTimestamp(cluster, groupId, topic, null, System.currentTimeMillis()), + offsetsResetService.resetToOffsets(cluster, groupId, topic, Map.of()) ); for (Mono mono : expectedValidationError) { @@ -121,7 +113,7 @@ void resetToOffsets() { sendMsgsToPartition(Map.of(0, 10, 1, 10, 2, 10)); var expectedOffsets = Map.of(0, 5L, 1, 5L, 2, 5L); - offsetsResetService.resetToOffsets(CLUSTER, groupId, topic, expectedOffsets).block(); + offsetsResetService.resetToOffsets(cluster, groupId, topic, expectedOffsets).block(); assertOffsets(expectedOffsets); } @@ -131,7 +123,7 @@ void resetToOffsetsCommitsEarliestOrLatestOffsetsIfOffsetsBoundsNotValid() { var offsetsWithInValidBounds = Map.of(0, -2L, 1, 5L, 2, 500L); var expectedOffsets = Map.of(0, 0L, 1, 5L, 2, 10L); - offsetsResetService.resetToOffsets(CLUSTER, groupId, topic, offsetsWithInValidBounds).block(); + offsetsResetService.resetToOffsets(cluster, groupId, topic, offsetsWithInValidBounds).block(); assertOffsets(expectedOffsets); } @@ -140,11 +132,11 @@ void resetToEarliest() { sendMsgsToPartition(Map.of(0, 10, 1, 10, 2, 10)); commit(Map.of(0, 5L, 1, 5L, 2, 5L)); - offsetsResetService.resetToEarliest(CLUSTER, groupId, topic, List.of(0, 1)).block(); + offsetsResetService.resetToEarliest(cluster, groupId, topic, List.of(0, 1)).block(); assertOffsets(Map.of(0, 0L, 1, 0L, 2, 5L)); commit(Map.of(0, 5L, 1, 5L, 2, 5L)); - offsetsResetService.resetToEarliest(CLUSTER, groupId, topic, null).block(); + offsetsResetService.resetToEarliest(cluster, groupId, topic, null).block(); assertOffsets(Map.of(0, 0L, 1, 0L, 2, 0L, 3, 0L, 4, 0L)); } @@ -153,11 +145,11 @@ void resetToLatest() { sendMsgsToPartition(Map.of(0, 10, 1, 10, 2, 10, 3, 10, 4, 10)); commit(Map.of(0, 5L, 1, 5L, 2, 5L)); - offsetsResetService.resetToLatest(CLUSTER, groupId, topic, List.of(0, 1)).block(); + offsetsResetService.resetToLatest(cluster, groupId, topic, List.of(0, 1)).block(); assertOffsets(Map.of(0, 10L, 1, 10L, 2, 5L)); commit(Map.of(0, 5L, 1, 5L, 2, 5L)); - offsetsResetService.resetToLatest(CLUSTER, groupId, topic, null).block(); + offsetsResetService.resetToLatest(cluster, groupId, topic, null).block(); assertOffsets(Map.of(0, 10L, 1, 10L, 2, 10L, 3, 10L, 4, 10L)); } @@ -175,7 +167,7 @@ void resetToTimestamp() { new ProducerRecord(topic, 2, 1200L, null, null))); offsetsResetService.resetToTimestamp( - CLUSTER, groupId, topic, List.of(0, 1, 2, 3), 1600L + cluster, groupId, topic, List.of(0, 1, 2, 3), 1600L ).block(); assertOffsets(Map.of(0, 2L, 1, 1L, 2, 3L, 3, 0L)); } @@ -227,7 +219,7 @@ private void assertOffsets(Map expectedOffsets) { private Consumer groupConsumer() { Properties props = new Properties(); props.put(ConsumerConfig.CLIENT_ID_CONFIG, "kafka-ui-" + UUID.randomUUID()); - props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, CLUSTER.getBootstrapServers()); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers()); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, BytesDeserializer.class); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, BytesDeserializer.class); props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ReactiveAdminClientTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ReactiveAdminClientTest.java new file mode 100644 index 00000000000..061c0bea66b --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ReactiveAdminClientTest.java @@ -0,0 +1,303 @@ +package com.provectus.kafka.ui.service; + +import static com.provectus.kafka.ui.service.ReactiveAdminClient.toMonoWithExceptionFilter; +import static java.util.Objects.requireNonNull; +import static org.apache.kafka.clients.admin.ListOffsetsResult.ListOffsetsResultInfo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.ThrowableAssert.ThrowingCallable; + +import com.provectus.kafka.ui.AbstractIntegrationTest; +import com.provectus.kafka.ui.exception.ValidationException; +import com.provectus.kafka.ui.producer.KafkaTestProducer; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Stream; +import lombok.SneakyThrows; +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.clients.admin.AlterConfigOp; +import org.apache.kafka.clients.admin.Config; +import org.apache.kafka.clients.admin.ConfigEntry; +import org.apache.kafka.clients.admin.NewTopic; +import org.apache.kafka.clients.admin.OffsetSpec; +import org.apache.kafka.clients.admin.TopicDescription; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.clients.consumer.OffsetAndMetadata; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.KafkaFuture; +import org.apache.kafka.common.Node; +import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.common.TopicPartitionInfo; +import org.apache.kafka.common.config.ConfigResource; +import org.apache.kafka.common.errors.UnknownTopicOrPartitionException; +import org.apache.kafka.common.internals.KafkaFutureImpl; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.assertj.core.api.ThrowableAssert; +import org.junit.function.ThrowingRunnable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +class ReactiveAdminClientTest extends AbstractIntegrationTest { + + private final List clearings = new ArrayList<>(); + + private AdminClient adminClient; + private ReactiveAdminClient reactiveAdminClient; + + @BeforeEach + void init() { + AdminClientService adminClientService = applicationContext.getBean(AdminClientService.class); + ClustersStorage clustersStorage = applicationContext.getBean(ClustersStorage.class); + reactiveAdminClient = requireNonNull(adminClientService.get(clustersStorage.getClusterByName(LOCAL).get()).block()); + adminClient = reactiveAdminClient.getClient(); + } + + @AfterEach + void tearDown() { + for (ThrowingRunnable clearing : clearings) { + try { + clearing.run(); + } catch (Throwable th) { + //NOOP + } + } + } + + @Test + void testUpdateTopicConfigs() throws Exception { + String topic = UUID.randomUUID().toString(); + createTopics(new NewTopic(topic, 1, (short) 1)); + + var configResource = new ConfigResource(ConfigResource.Type.TOPIC, topic); + + adminClient.incrementalAlterConfigs( + Map.of( + configResource, + List.of( + new AlterConfigOp(new ConfigEntry("compression.type", "gzip"), AlterConfigOp.OpType.SET), + new AlterConfigOp(new ConfigEntry("retention.bytes", "12345678"), AlterConfigOp.OpType.SET) + ) + ) + ).all().get(); + + StepVerifier.create( + reactiveAdminClient.updateTopicConfig( + topic, + Map.of( + "compression.type", "snappy", //changing existing config + "file.delete.delay.ms", "12345" // adding new one + ) + ) + ).expectComplete().verify(); + + Config config = adminClient.describeConfigs(List.of(configResource)).values().get(configResource).get(); + assertThat(config.get("retention.bytes").value()).isNotEqualTo("12345678"); // wes reset to default + assertThat(config.get("compression.type").value()).isEqualTo("snappy"); + assertThat(config.get("file.delete.delay.ms").value()).isEqualTo("12345"); + } + + + @SneakyThrows + void createTopics(NewTopic... topics) { + adminClient.createTopics(List.of(topics)).all().get(); + clearings.add(() -> adminClient.deleteTopics(Stream.of(topics).map(NewTopic::name).toList()).all().get()); + } + + void fillTopic(String topic, int msgsCnt) { + try (var producer = KafkaTestProducer.forKafka(kafka)) { + for (int i = 0; i < msgsCnt; i++) { + producer.send(topic, UUID.randomUUID().toString()); + } + } + } + + @Test + void testToMonoWithExceptionFilter() { + var failedFuture = new KafkaFutureImpl(); + failedFuture.completeExceptionally(new UnknownTopicOrPartitionException()); + + var okFuture = new KafkaFutureImpl(); + okFuture.complete("done"); + + var emptyFuture = new KafkaFutureImpl(); + emptyFuture.complete(null); + + Map> arg = Map.of( + "failure", failedFuture, + "ok", okFuture, + "empty", emptyFuture + ); + StepVerifier.create(toMonoWithExceptionFilter(arg, UnknownTopicOrPartitionException.class)) + .assertNext(result -> assertThat(result).hasSize(1).containsEntry("ok", "done")) + .verifyComplete(); + } + + @Test + void filterPartitionsWithLeaderCheckSkipsPartitionsFromTopicWhereSomePartitionsHaveNoLeader() { + var filteredPartitions = ReactiveAdminClient.filterPartitionsWithLeaderCheck( + List.of( + // contains partitions with no leader + new TopicDescription("noLeaderTopic", false, + List.of( + new TopicPartitionInfo(0, new Node(1, "n1", 9092), List.of(), List.of()), + new TopicPartitionInfo(1, null, List.of(), List.of()))), + // should be skipped by predicate + new TopicDescription("skippingByPredicate", false, + List.of( + new TopicPartitionInfo(0, new Node(1, "n1", 9092), List.of(), List.of()))), + // good topic + new TopicDescription("good", false, + List.of( + new TopicPartitionInfo(0, new Node(1, "n1", 9092), List.of(), List.of()), + new TopicPartitionInfo(1, new Node(2, "n2", 9092), List.of(), List.of())) + )), + p -> !p.topic().equals("skippingByPredicate"), + false + ); + + assertThat(filteredPartitions) + .containsExactlyInAnyOrder( + new TopicPartition("good", 0), + new TopicPartition("good", 1) + ); + } + + @Test + void filterPartitionsWithLeaderCheckThrowExceptionIfThereIsSomePartitionsWithoutLeaderAndFlagSet() { + ThrowingCallable call = () -> ReactiveAdminClient.filterPartitionsWithLeaderCheck( + List.of( + // contains partitions with no leader + new TopicDescription("t1", false, + List.of( + new TopicPartitionInfo(0, new Node(1, "n1", 9092), List.of(), List.of()), + new TopicPartitionInfo(1, null, List.of(), List.of()))), + new TopicDescription("t2", false, + List.of( + new TopicPartitionInfo(0, new Node(1, "n1", 9092), List.of(), List.of())) + )), + p -> true, + // setting failOnNoLeader flag + true + ); + assertThatThrownBy(call).isInstanceOf(ValidationException.class); + } + + @Test + void testListOffsetsUnsafe() { + String topic = UUID.randomUUID().toString(); + createTopics(new NewTopic(topic, 2, (short) 1)); + + // sending messages to have non-zero offsets for tp + try (var producer = KafkaTestProducer.forKafka(kafka)) { + producer.send(new ProducerRecord<>(topic, 1, "k", "v")); + producer.send(new ProducerRecord<>(topic, 1, "k", "v")); + } + + var requestedPartitions = List.of( + new TopicPartition(topic, 0), + new TopicPartition(topic, 1) + ); + + StepVerifier.create(reactiveAdminClient.listOffsetsUnsafe(requestedPartitions, OffsetSpec.earliest())) + .assertNext(offsets -> { + assertThat(offsets) + .hasSize(2) + .containsEntry(new TopicPartition(topic, 0), 0L) + .containsEntry(new TopicPartition(topic, 1), 0L); + }) + .verifyComplete(); + + StepVerifier.create(reactiveAdminClient.listOffsetsUnsafe(requestedPartitions, OffsetSpec.latest())) + .assertNext(offsets -> { + assertThat(offsets) + .hasSize(2) + .containsEntry(new TopicPartition(topic, 0), 0L) + .containsEntry(new TopicPartition(topic, 1), 2L); + }) + .verifyComplete(); + } + + + @Test + void testListConsumerGroupOffsets() throws Exception { + String topic = UUID.randomUUID().toString(); + String anotherTopic = UUID.randomUUID().toString(); + createTopics(new NewTopic(topic, 2, (short) 1), new NewTopic(anotherTopic, 1, (short) 1)); + fillTopic(topic, 10); + + Function> consumerSupplier = groupName -> { + Properties p = new Properties(); + p.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers()); + p.setProperty(ConsumerConfig.GROUP_ID_CONFIG, groupName); + p.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + p.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); + p.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); + p.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); + return new KafkaConsumer(p); + }; + + String fullyPolledConsumer = UUID.randomUUID().toString(); + try (KafkaConsumer c = consumerSupplier.apply(fullyPolledConsumer)) { + c.subscribe(List.of(topic)); + int polled = 0; + while (polled < 10) { + polled += c.poll(Duration.ofMillis(50)).count(); + } + c.commitSync(); + } + + String polled1MsgConsumer = UUID.randomUUID().toString(); + try (KafkaConsumer c = consumerSupplier.apply(polled1MsgConsumer)) { + c.subscribe(List.of(topic)); + c.poll(Duration.ofMillis(100)); + c.commitSync(Map.of(tp(topic, 0), new OffsetAndMetadata(1))); + } + + String noCommitConsumer = UUID.randomUUID().toString(); + try (KafkaConsumer c = consumerSupplier.apply(noCommitConsumer)) { + c.subscribe(List.of(topic)); + c.poll(Duration.ofMillis(100)); + } + + Map endOffsets = adminClient.listOffsets(Map.of( + tp(topic, 0), OffsetSpec.latest(), + tp(topic, 1), OffsetSpec.latest())).all().get(); + + StepVerifier.create( + reactiveAdminClient.listConsumerGroupOffsets( + List.of(fullyPolledConsumer, polled1MsgConsumer, noCommitConsumer), + List.of( + tp(topic, 0), + tp(topic, 1), + tp(anotherTopic, 0)) + ) + ).assertNext(table -> { + + assertThat(table.row(polled1MsgConsumer)) + .containsEntry(tp(topic, 0), 1L) + .hasSize(1); + + assertThat(table.row(noCommitConsumer)) + .isEmpty(); + + assertThat(table.row(fullyPolledConsumer)) + .containsEntry(tp(topic, 0), endOffsets.get(tp(topic, 0)).offset()) + .containsEntry(tp(topic, 1), endOffsets.get(tp(topic, 1)).offset()) + .hasSize(2); + }) + .verifyComplete(); + } + + private static TopicPartition tp(String topic, int partition) { + return new TopicPartition(topic, partition); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/RecordEmitterTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/RecordEmitterTest.java index 05d8da3786c..2a9fa76f136 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/RecordEmitterTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/RecordEmitterTest.java @@ -1,21 +1,26 @@ package com.provectus.kafka.ui.service; -import static com.provectus.kafka.ui.model.SeekDirectionDTO.BACKWARD; -import static com.provectus.kafka.ui.model.SeekDirectionDTO.FORWARD; import static com.provectus.kafka.ui.model.SeekTypeDTO.BEGINNING; +import static com.provectus.kafka.ui.model.SeekTypeDTO.LATEST; import static com.provectus.kafka.ui.model.SeekTypeDTO.OFFSET; import static com.provectus.kafka.ui.model.SeekTypeDTO.TIMESTAMP; import static org.assertj.core.api.Assertions.assertThat; import com.provectus.kafka.ui.AbstractIntegrationTest; -import com.provectus.kafka.ui.emitter.BackwardRecordEmitter; -import com.provectus.kafka.ui.emitter.ForwardRecordEmitter; +import com.provectus.kafka.ui.emitter.BackwardEmitter; +import com.provectus.kafka.ui.emitter.EnhancedConsumer; +import com.provectus.kafka.ui.emitter.ForwardEmitter; +import com.provectus.kafka.ui.emitter.PollingSettings; +import com.provectus.kafka.ui.emitter.PollingThrottler; import com.provectus.kafka.ui.model.ConsumerPosition; +import com.provectus.kafka.ui.model.TopicMessageDTO; import com.provectus.kafka.ui.model.TopicMessageEventDTO; import com.provectus.kafka.ui.producer.KafkaTestProducer; -import com.provectus.kafka.ui.serde.SimpleRecordSerDe; -import com.provectus.kafka.ui.util.OffsetsSeekBackward; -import com.provectus.kafka.ui.util.OffsetsSeekForward; +import com.provectus.kafka.ui.serde.api.Serde; +import com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer; +import com.provectus.kafka.ui.serdes.PropertyResolverImpl; +import com.provectus.kafka.ui.serdes.builtin.StringSerde; +import com.provectus.kafka.ui.util.ApplicationMetrics; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; @@ -26,17 +31,15 @@ import java.util.concurrent.ThreadLocalRandom; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; import lombok.Value; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.admin.NewTopic; import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.common.TopicPartition; import org.apache.kafka.common.header.internals.RecordHeader; -import org.apache.kafka.common.serialization.BytesDeserializer; -import org.apache.kafka.common.utils.Bytes; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -53,6 +56,8 @@ class RecordEmitterTest extends AbstractIntegrationTest { static final String TOPIC = RecordEmitterTest.class.getSimpleName() + "_" + UUID.randomUUID(); static final String EMPTY_TOPIC = TOPIC + "_empty"; static final List SENT_RECORDS = new ArrayList<>(); + static final ConsumerRecordDeserializer RECORD_DESERIALIZER = createRecordsDeserializer(); + static final Predicate NOOP_FILTER = m -> true; @BeforeAll static void generateMsgs() throws Exception { @@ -88,54 +93,75 @@ static void generateMsgs() throws Exception { static void cleanup() { deleteTopic(TOPIC); deleteTopic(EMPTY_TOPIC); + SENT_RECORDS.clear(); + } + + private static ConsumerRecordDeserializer createRecordsDeserializer() { + Serde s = new StringSerde(); + s.configure(PropertyResolverImpl.empty(), PropertyResolverImpl.empty(), PropertyResolverImpl.empty()); + return new ConsumerRecordDeserializer( + StringSerde.name(), + s.deserializer(null, Serde.Target.KEY), + StringSerde.name(), + s.deserializer(null, Serde.Target.VALUE), + StringSerde.name(), + s.deserializer(null, Serde.Target.KEY), + s.deserializer(null, Serde.Target.VALUE), + msg -> msg + ); } @Test void pollNothingOnEmptyTopic() { - var forwardEmitter = new ForwardRecordEmitter( + var forwardEmitter = new ForwardEmitter( this::createConsumer, - new OffsetsSeekForward(EMPTY_TOPIC, - new ConsumerPosition(BEGINNING, Map.of(), FORWARD) - ), new SimpleRecordSerDe() + new ConsumerPosition(BEGINNING, EMPTY_TOPIC, null), + 100, + RECORD_DESERIALIZER, + NOOP_FILTER, + PollingSettings.createDefault() ); - var backwardEmitter = new BackwardRecordEmitter( + var backwardEmitter = new BackwardEmitter( this::createConsumer, - new OffsetsSeekBackward( - EMPTY_TOPIC, - new ConsumerPosition(BEGINNING, Map.of(), BACKWARD), - 100 - ), new SimpleRecordSerDe() + new ConsumerPosition(BEGINNING, EMPTY_TOPIC, null), + 100, + RECORD_DESERIALIZER, + NOOP_FILTER, + PollingSettings.createDefault() ); - StepVerifier.create( - Flux.create(forwardEmitter) - .filter(m -> m.getType().equals(TopicMessageEventDTO.TypeEnum.MESSAGE)) - .take(100) - ).expectNextCount(0).expectComplete().verify(); - - StepVerifier.create( - Flux.create(backwardEmitter) - .filter(m -> m.getType().equals(TopicMessageEventDTO.TypeEnum.MESSAGE)) - .take(100) - ).expectNextCount(0).expectComplete().verify(); + StepVerifier.create(Flux.create(forwardEmitter)) + .expectNextMatches(m -> m.getType().equals(TopicMessageEventDTO.TypeEnum.PHASE)) + .expectNextMatches(m -> m.getType().equals(TopicMessageEventDTO.TypeEnum.DONE)) + .expectComplete() + .verify(); + + StepVerifier.create(Flux.create(backwardEmitter)) + .expectNextMatches(m -> m.getType().equals(TopicMessageEventDTO.TypeEnum.PHASE)) + .expectNextMatches(m -> m.getType().equals(TopicMessageEventDTO.TypeEnum.DONE)) + .expectComplete() + .verify(); } @Test void pollFullTopicFromBeginning() { - var forwardEmitter = new ForwardRecordEmitter( + var forwardEmitter = new ForwardEmitter( this::createConsumer, - new OffsetsSeekForward(TOPIC, - new ConsumerPosition(BEGINNING, Map.of(), FORWARD) - ), new SimpleRecordSerDe() + new ConsumerPosition(BEGINNING, TOPIC, null), + PARTITIONS * MSGS_PER_PARTITION, + RECORD_DESERIALIZER, + NOOP_FILTER, + PollingSettings.createDefault() ); - var backwardEmitter = new BackwardRecordEmitter( + var backwardEmitter = new BackwardEmitter( this::createConsumer, - new OffsetsSeekBackward(TOPIC, - new ConsumerPosition(BEGINNING, Map.of(), BACKWARD), - PARTITIONS * MSGS_PER_PARTITION - ), new SimpleRecordSerDe() + new ConsumerPosition(LATEST, TOPIC, null), + PARTITIONS * MSGS_PER_PARTITION, + RECORD_DESERIALIZER, + NOOP_FILTER, + PollingSettings.createDefault() ); List expectedValues = SENT_RECORDS.stream().map(Record::getValue).collect(Collectors.toList()); @@ -152,19 +178,22 @@ void pollWithOffsets() { targetOffsets.put(new TopicPartition(TOPIC, i), offset); } - var forwardEmitter = new ForwardRecordEmitter( + var forwardEmitter = new ForwardEmitter( this::createConsumer, - new OffsetsSeekForward(TOPIC, - new ConsumerPosition(OFFSET, targetOffsets, FORWARD) - ), new SimpleRecordSerDe() + new ConsumerPosition(OFFSET, TOPIC, targetOffsets), + PARTITIONS * MSGS_PER_PARTITION, + RECORD_DESERIALIZER, + NOOP_FILTER, + PollingSettings.createDefault() ); - var backwardEmitter = new BackwardRecordEmitter( + var backwardEmitter = new BackwardEmitter( this::createConsumer, - new OffsetsSeekBackward(TOPIC, - new ConsumerPosition(OFFSET, targetOffsets, BACKWARD), - PARTITIONS * MSGS_PER_PARTITION - ), new SimpleRecordSerDe() + new ConsumerPosition(OFFSET, TOPIC, targetOffsets), + PARTITIONS * MSGS_PER_PARTITION, + RECORD_DESERIALIZER, + NOOP_FILTER, + PollingSettings.createDefault() ); var expectedValues = SENT_RECORDS.stream() @@ -197,19 +226,22 @@ void pollWithTimestamps() { ); } - var forwardEmitter = new ForwardRecordEmitter( + var forwardEmitter = new ForwardEmitter( this::createConsumer, - new OffsetsSeekForward(TOPIC, - new ConsumerPosition(TIMESTAMP, targetTimestamps, FORWARD) - ), new SimpleRecordSerDe() + new ConsumerPosition(TIMESTAMP, TOPIC, targetTimestamps), + PARTITIONS * MSGS_PER_PARTITION, + RECORD_DESERIALIZER, + NOOP_FILTER, + PollingSettings.createDefault() ); - var backwardEmitter = new BackwardRecordEmitter( + var backwardEmitter = new BackwardEmitter( this::createConsumer, - new OffsetsSeekBackward(TOPIC, - new ConsumerPosition(TIMESTAMP, targetTimestamps, BACKWARD), - PARTITIONS * MSGS_PER_PARTITION - ), new SimpleRecordSerDe() + new ConsumerPosition(TIMESTAMP, TOPIC, targetTimestamps), + PARTITIONS * MSGS_PER_PARTITION, + RECORD_DESERIALIZER, + NOOP_FILTER, + PollingSettings.createDefault() ); var expectedValues = SENT_RECORDS.stream() @@ -235,12 +267,13 @@ void backwardEmitterSeekToEnd() { targetOffsets.put(new TopicPartition(TOPIC, i), (long) MSGS_PER_PARTITION); } - var backwardEmitter = new BackwardRecordEmitter( + var backwardEmitter = new BackwardEmitter( this::createConsumer, - new OffsetsSeekBackward(TOPIC, - new ConsumerPosition(OFFSET, targetOffsets, BACKWARD), - numMessages - ), new SimpleRecordSerDe() + new ConsumerPosition(OFFSET, TOPIC, targetOffsets), + numMessages, + RECORD_DESERIALIZER, + NOOP_FILTER, + PollingSettings.createDefault() ); var expectedValues = SENT_RECORDS.stream() @@ -261,12 +294,13 @@ void backwardEmitterSeekToBegin() { offsets.put(new TopicPartition(TOPIC, i), 0L); } - var backwardEmitter = new BackwardRecordEmitter( + var backwardEmitter = new BackwardEmitter( this::createConsumer, - new OffsetsSeekBackward(TOPIC, - new ConsumerPosition(OFFSET, offsets, BACKWARD), - 100 - ), new SimpleRecordSerDe() + new ConsumerPosition(OFFSET, TOPIC, offsets), + 100, + RECORD_DESERIALIZER, + NOOP_FILTER, + PollingSettings.createDefault() ); expectEmitter(backwardEmitter, @@ -283,7 +317,8 @@ private void expectEmitter(Consumer> emitter, Lis .expectNextCount(expectedValues.size()) .expectRecordedMatches(r -> r.containsAll(expectedValues)) .consumeRecordedWith(r -> log.info("Collected collection: {}", r)), - v -> {} + v -> { + } ); } @@ -304,22 +339,20 @@ private void expectEmitter( assertionsConsumer.accept(step.expectComplete().verifyThenAssertThat()); } - private KafkaConsumer createConsumer() { + private EnhancedConsumer createConsumer() { return createConsumer(Map.of()); } - private KafkaConsumer createConsumer(Map properties) { + private EnhancedConsumer createConsumer(Map properties) { final Map map = Map.of( ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers(), ConsumerConfig.GROUP_ID_CONFIG, UUID.randomUUID().toString(), - ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 20, // to check multiple polls - ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, BytesDeserializer.class, - ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, BytesDeserializer.class + ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 19 // to check multiple polls ); Properties props = new Properties(); props.putAll(map); props.putAll(properties); - return new KafkaConsumer<>(props); + return new EnhancedConsumer(props, PollingThrottler.noop(), ApplicationMetrics.noop()); } @Value diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/SchemaRegistryPaginationTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/SchemaRegistryPaginationTest.java index 67ab2403397..cba0f58fae7 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/SchemaRegistryPaginationTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/SchemaRegistryPaginationTest.java @@ -7,11 +7,15 @@ import static org.mockito.Mockito.when; import com.provectus.kafka.ui.controller.SchemasController; -import com.provectus.kafka.ui.mapper.ClusterMapper; -import com.provectus.kafka.ui.model.InternalSchemaRegistry; import com.provectus.kafka.ui.model.KafkaCluster; import com.provectus.kafka.ui.model.SchemaSubjectDTO; +import com.provectus.kafka.ui.service.audit.AuditService; +import com.provectus.kafka.ui.sr.model.Compatibility; +import com.provectus.kafka.ui.sr.model.SchemaSubject; +import com.provectus.kafka.ui.util.AccessControlServiceMock; +import com.provectus.kafka.ui.util.ReactiveFailover; import java.util.Comparator; +import java.util.List; import java.util.Optional; import java.util.stream.IntStream; import org.junit.jupiter.api.Test; @@ -21,21 +25,26 @@ public class SchemaRegistryPaginationTest { private static final String LOCAL_KAFKA_CLUSTER_NAME = "local"; - private final SchemaRegistryService schemaRegistryService = mock(SchemaRegistryService.class); - private final ClustersStorage clustersStorage = mock(ClustersStorage.class); - private final ClusterMapper clusterMapper = mock(ClusterMapper.class); + private SchemasController controller; - private final SchemasController controller = new SchemasController(clusterMapper, schemaRegistryService); + private void init(List subjects) { + ClustersStorage clustersStorage = mock(ClustersStorage.class); + when(clustersStorage.getClusterByName(isA(String.class))) + .thenReturn(Optional.of(buildKafkaCluster(LOCAL_KAFKA_CLUSTER_NAME))); - private void init(String[] subjects) { + SchemaRegistryService schemaRegistryService = mock(SchemaRegistryService.class); when(schemaRegistryService.getAllSubjectNames(isA(KafkaCluster.class))) .thenReturn(Mono.just(subjects)); when(schemaRegistryService .getAllLatestVersionSchemas(isA(KafkaCluster.class), anyList())).thenCallRealMethod(); - when(clustersStorage.getClusterByName(isA(String.class))) - .thenReturn(Optional.of(buildKafkaCluster(LOCAL_KAFKA_CLUSTER_NAME))); when(schemaRegistryService.getLatestSchemaVersionBySubject(isA(KafkaCluster.class), isA(String.class))) - .thenAnswer(a -> Mono.just(new SchemaSubjectDTO().subject(a.getArgument(1)))); + .thenAnswer(a -> Mono.just( + new SchemaRegistryService.SubjectWithCompatibilityLevel( + new SchemaSubject().subject(a.getArgument(1)), Compatibility.FULL))); + + this.controller = new SchemasController(schemaRegistryService); + this.controller.setAccessControlService(new AccessControlServiceMock().getMock()); + this.controller.setAuditService(mock(AuditService.class)); this.controller.setClustersStorage(clustersStorage); } @@ -45,7 +54,7 @@ void shouldListFirst25andThen10Schemas() { IntStream.rangeClosed(1, 100) .boxed() .map(num -> "subject" + num) - .toArray(String[]::new) + .toList() ); var schemasFirst25 = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME, null, null, null, null).block(); @@ -69,7 +78,7 @@ void shouldListSchemasContaining_1() { IntStream.rangeClosed(1, 100) .boxed() .map(num -> "subject" + num) - .toArray(String[]::new) + .toList() ); var schemasSearch7 = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME, null, null, "1", null).block(); @@ -83,7 +92,7 @@ void shouldCorrectlyHandleNonPositivePageNumberAndPageSize() { IntStream.rangeClosed(1, 100) .boxed() .map(num -> "subject" + num) - .toArray(String[]::new) + .toList() ); var schemas = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME, 0, -1, null, null).block(); @@ -99,7 +108,7 @@ void shouldCalculateCorrectPageCountForNonDivisiblePageSize() { IntStream.rangeClosed(1, 100) .boxed() .map(num -> "subject" + num) - .toArray(String[]::new) + .toList() ); var schemas = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME, @@ -113,7 +122,7 @@ void shouldCalculateCorrectPageCountForNonDivisiblePageSize() { private KafkaCluster buildKafkaCluster(String clusterName) { return KafkaCluster.builder() .name(clusterName) - .schemaRegistry(InternalSchemaRegistry.builder().build()) + .schemaRegistryClient(mock(ReactiveFailover.class)) .build(); } } diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/SendAndReadTests.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/SendAndReadTests.java index c9be9666d21..78c111cdd19 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/SendAndReadTests.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/SendAndReadTests.java @@ -7,11 +7,14 @@ import com.provectus.kafka.ui.model.ConsumerPosition; import com.provectus.kafka.ui.model.CreateTopicMessageDTO; import com.provectus.kafka.ui.model.KafkaCluster; -import com.provectus.kafka.ui.model.MessageFormatDTO; import com.provectus.kafka.ui.model.SeekDirectionDTO; import com.provectus.kafka.ui.model.SeekTypeDTO; import com.provectus.kafka.ui.model.TopicMessageDTO; import com.provectus.kafka.ui.model.TopicMessageEventDTO; +import com.provectus.kafka.ui.serdes.builtin.Int32Serde; +import com.provectus.kafka.ui.serdes.builtin.Int64Serde; +import com.provectus.kafka.ui.serdes.builtin.StringSerde; +import com.provectus.kafka.ui.serdes.builtin.sr.SchemaRegistrySerde; import io.confluent.kafka.schemaregistry.ParsedSchema; import io.confluent.kafka.schemaregistry.avro.AvroSchema; import io.confluent.kafka.schemaregistry.json.JsonSchema; @@ -126,9 +129,6 @@ public class SendAndReadTests extends AbstractIntegrationTest { @Autowired private ClustersStorage clustersStorage; - @Autowired - private ClustersMetricsScheduler clustersMetricsScheduler; - @BeforeEach void init() { targetCluster = clustersStorage.getClusterByName(LOCAL).get(); @@ -140,7 +140,9 @@ void noSchemaStringKeyStringValue() { .withMsgToSend( new CreateTopicMessageDTO() .key("testKey") + .keySerde(StringSerde.name()) .content("testValue") + .valueSerde(StringSerde.name()) ) .doAssert(polled -> { assertThat(polled.getKey()).isEqualTo("testKey"); @@ -149,40 +151,30 @@ void noSchemaStringKeyStringValue() { } @Test - void noSchemaJsonKeyJsonValue() { - new SendAndReadSpec() - .withMsgToSend( - new CreateTopicMessageDTO() - .key("{ \"f1\": 111, \"f2\": \"testStr1\" }") - .content("{ \"f1\": 222, \"f2\": \"testStr2\" }") - ) - .doAssert(polled -> { - assertThat(polled.getKey()).isEqualTo("{ \"f1\": 111, \"f2\": \"testStr1\" }"); - assertThat(polled.getContent()).isEqualTo("{ \"f1\": 222, \"f2\": \"testStr2\" }"); - }); - } - - @Test - void keyIsIntValueIsDoubleShouldBeSerializedAsStrings() { + void keyIsIntValueIsLong() { new SendAndReadSpec() .withMsgToSend( new CreateTopicMessageDTO() .key("123") - .content("234.56") + .keySerde(Int32Serde.name()) + .content("21474836470") + .valueSerde(Int64Serde.name()) ) .doAssert(polled -> { assertThat(polled.getKey()).isEqualTo("123"); - assertThat(polled.getContent()).isEqualTo("234.56"); + assertThat(polled.getContent()).isEqualTo("21474836470"); }); } @Test - void noSchemaKeyIsNull() { + void keyIsNull() { new SendAndReadSpec() .withMsgToSend( new CreateTopicMessageDTO() .key(null) + .keySerde(StringSerde.name()) .content("testValue") + .valueSerde(StringSerde.name()) ) .doAssert(polled -> { assertThat(polled.getKey()).isNull(); @@ -191,12 +183,14 @@ void noSchemaKeyIsNull() { } @Test - void noSchemaValueIsNull() { + void valueIsNull() { new SendAndReadSpec() .withMsgToSend( new CreateTopicMessageDTO() .key("testKey") + .keySerde(StringSerde.name()) .content(null) + .valueSerde(StringSerde.name()) ) .doAssert(polled -> { assertThat(polled.getKey()).isEqualTo("testKey"); @@ -212,7 +206,9 @@ void primitiveAvroSchemas() { .withMsgToSend( new CreateTopicMessageDTO() .key("\"some string\"") + .keySerde(SchemaRegistrySerde.name()) .content("123") + .valueSerde(SchemaRegistrySerde.name()) ) .doAssert(polled -> { assertThat(polled.getKey()).isEqualTo("\"some string\""); @@ -221,14 +217,16 @@ void primitiveAvroSchemas() { } @Test - void nonNullableKvWithAvroSchema() { + void recordAvroSchema() { new SendAndReadSpec() .withKeySchema(AVRO_SCHEMA_1) .withValueSchema(AVRO_SCHEMA_2) .withMsgToSend( new CreateTopicMessageDTO() .key(AVRO_SCHEMA_1_JSON_RECORD) + .keySerde(SchemaRegistrySerde.name()) .content(AVRO_SCHEMA_2_JSON_RECORD) + .valueSerde(SchemaRegistrySerde.name()) ) .doAssert(polled -> { assertJsonEqual(polled.getKey(), AVRO_SCHEMA_1_JSON_RECORD); @@ -236,36 +234,6 @@ void nonNullableKvWithAvroSchema() { }); } - @Test - void keyWithNoSchemaValueWithAvroSchema() { - new SendAndReadSpec() - .withValueSchema(AVRO_SCHEMA_1) - .withMsgToSend( - new CreateTopicMessageDTO() - .key("testKey") - .content(AVRO_SCHEMA_1_JSON_RECORD) - ) - .doAssert(polled -> { - assertThat(polled.getKey()).isEqualTo("testKey"); - assertJsonEqual(polled.getContent(), AVRO_SCHEMA_1_JSON_RECORD); - }); - } - - @Test - void keyWithAvroSchemaValueWithNoSchema() { - new SendAndReadSpec() - .withKeySchema(AVRO_SCHEMA_1) - .withMsgToSend( - new CreateTopicMessageDTO() - .key(AVRO_SCHEMA_1_JSON_RECORD) - .content("testVal") - ) - .doAssert(polled -> { - assertJsonEqual(polled.getKey(), AVRO_SCHEMA_1_JSON_RECORD); - assertThat(polled.getContent()).isEqualTo("testVal"); - }); - } - @Test void keyWithNoSchemaValueWithProtoSchema() { new SendAndReadSpec() @@ -273,7 +241,9 @@ void keyWithNoSchemaValueWithProtoSchema() { .withMsgToSend( new CreateTopicMessageDTO() .key("testKey") + .keySerde(StringSerde.name()) .content(PROTOBUF_SCHEMA_JSON_RECORD) + .valueSerde(SchemaRegistrySerde.name()) ) .doAssert(polled -> { assertThat(polled.getKey()).isEqualTo("testKey"); @@ -289,7 +259,10 @@ void keyWithAvroSchemaValueWithAvroSchemaKeyIsNull() { .withMsgToSend( new CreateTopicMessageDTO() .key(null) + .keySerde(SchemaRegistrySerde.name()) .content(AVRO_SCHEMA_2_JSON_RECORD) + .valueSerde(SchemaRegistrySerde.name()) + ) .doAssert(polled -> { assertThat(polled.getKey()).isNull(); @@ -298,33 +271,19 @@ void keyWithAvroSchemaValueWithAvroSchemaKeyIsNull() { } @Test - void valueWithAvroSchemaShouldThrowExceptionArgIsNotValidJsonObject() { + void valueWithAvroSchemaShouldThrowExceptionIfArgIsNotValidJsonObject() { new SendAndReadSpec() .withValueSchema(AVRO_SCHEMA_2) .withMsgToSend( new CreateTopicMessageDTO() - // f2 has type object instead of string - .content("{ \"f1\": 111, \"f2\": {} }") + .keySerde(StringSerde.name()) + // f2 has type int instead of string + .content("{ \"f1\": 111, \"f2\": 123 }") + .valueSerde(SchemaRegistrySerde.name()) ) .assertSendThrowsException(); } - @Test - void keyWithAvroSchemaValueWithAvroSchemaValueIsNull() { - new SendAndReadSpec() - .withKeySchema(AVRO_SCHEMA_1) - .withValueSchema(AVRO_SCHEMA_2) - .withMsgToSend( - new CreateTopicMessageDTO() - .key(AVRO_SCHEMA_1_JSON_RECORD) - .content(null) - ) - .doAssert(polled -> { - assertJsonEqual(polled.getKey(), AVRO_SCHEMA_1_JSON_RECORD); - assertThat(polled.getContent()).isNull(); - }); - } - @Test void keyWithAvroSchemaValueWithProtoSchema() { new SendAndReadSpec() @@ -333,7 +292,9 @@ void keyWithAvroSchemaValueWithProtoSchema() { .withMsgToSend( new CreateTopicMessageDTO() .key(AVRO_SCHEMA_1_JSON_RECORD) + .keySerde(SchemaRegistrySerde.name()) .content(PROTOBUF_SCHEMA_JSON_RECORD) + .valueSerde(SchemaRegistrySerde.name()) ) .doAssert(polled -> { assertJsonEqual(polled.getKey(), AVRO_SCHEMA_1_JSON_RECORD); @@ -347,8 +308,12 @@ void valueWithProtoSchemaShouldThrowExceptionArgIsNotValidJsonObject() { .withValueSchema(PROTOBUF_SCHEMA) .withMsgToSend( new CreateTopicMessageDTO() + .key(null) + .keySerde(StringSerde.name()) // f2 field has type object instead of int - .content("{ \"f1\" : \"test str\", \"f2\" : {} }")) + .content("{ \"f1\" : \"test str\", \"f2\" : {} }") + .valueSerde(SchemaRegistrySerde.name()) + ) .assertSendThrowsException(); } @@ -360,7 +325,9 @@ void keyWithProtoSchemaValueWithJsonSchema() { .withMsgToSend( new CreateTopicMessageDTO() .key(PROTOBUF_SCHEMA_JSON_RECORD) + .keySerde(SchemaRegistrySerde.name()) .content(JSON_SCHEMA_RECORD) + .valueSerde(SchemaRegistrySerde.name()) ) .doAssert(polled -> { assertJsonEqual(polled.getKey(), PROTOBUF_SCHEMA_JSON_RECORD); @@ -368,29 +335,17 @@ void keyWithProtoSchemaValueWithJsonSchema() { }); } - @Test - void keyWithJsonValueWithJsonSchemaKeyValueIsNull() { - new SendAndReadSpec() - .withKeySchema(JSON_SCHEMA) - .withValueSchema(JSON_SCHEMA) - .withMsgToSend( - new CreateTopicMessageDTO() - .key(JSON_SCHEMA_RECORD) - ) - .doAssert(polled -> { - assertJsonEqual(polled.getKey(), JSON_SCHEMA_RECORD); - assertThat(polled.getContent()).isNull(); - }); - } - @Test void valueWithJsonSchemaThrowsExceptionIfArgIsNotValidJsonObject() { new SendAndReadSpec() .withValueSchema(JSON_SCHEMA) .withMsgToSend( new CreateTopicMessageDTO() + .key(null) + .keySerde(StringSerde.name()) // 'f2' field has has type object instead of string .content("{ \"f1\": 12, \"f2\": {}, \"schema\": \"some txt\" }") + .valueSerde(SchemaRegistrySerde.name()) ) .assertSendThrowsException(); } @@ -403,17 +358,20 @@ void topicMessageMetadataAvro() { .withMsgToSend( new CreateTopicMessageDTO() .key(AVRO_SCHEMA_1_JSON_RECORD) + .keySerde(SchemaRegistrySerde.name()) .content(AVRO_SCHEMA_2_JSON_RECORD) + .valueSerde(SchemaRegistrySerde.name()) ) .doAssert(polled -> { assertJsonEqual(polled.getKey(), AVRO_SCHEMA_1_JSON_RECORD); assertJsonEqual(polled.getContent(), AVRO_SCHEMA_2_JSON_RECORD); assertThat(polled.getKeySize()).isEqualTo(15L); assertThat(polled.getValueSize()).isEqualTo(15L); - assertThat(polled.getKeyFormat()).isEqualTo(MessageFormatDTO.AVRO); - assertThat(polled.getValueFormat()).isEqualTo(MessageFormatDTO.AVRO); - assertThat(polled.getKeySchemaId()).isNotEmpty(); - assertThat(polled.getValueSchemaId()).isNotEmpty(); + assertThat(polled.getKeyDeserializeProperties().get("schemaId")).isNotNull(); + assertThat(polled.getValueDeserializeProperties().get("schemaId")).isNotNull(); + assertThat(polled.getKeyDeserializeProperties().get("type")).isEqualTo("AVRO"); + assertThat(polled.getValueDeserializeProperties().get("schemaId")).isNotNull(); + assertThat(polled.getValueDeserializeProperties().get("type")).isEqualTo("AVRO"); }); } @@ -425,17 +383,19 @@ void topicMessageMetadataProtobuf() { .withMsgToSend( new CreateTopicMessageDTO() .key(PROTOBUF_SCHEMA_JSON_RECORD) + .keySerde(SchemaRegistrySerde.name()) .content(PROTOBUF_SCHEMA_JSON_RECORD) + .valueSerde(SchemaRegistrySerde.name()) ) .doAssert(polled -> { assertJsonEqual(polled.getKey(), PROTOBUF_SCHEMA_JSON_RECORD); assertJsonEqual(polled.getContent(), PROTOBUF_SCHEMA_JSON_RECORD); assertThat(polled.getKeySize()).isEqualTo(18L); assertThat(polled.getValueSize()).isEqualTo(18L); - assertThat(polled.getKeyFormat()).isEqualTo(MessageFormatDTO.PROTOBUF); - assertThat(polled.getValueFormat()).isEqualTo(MessageFormatDTO.PROTOBUF); - assertThat(polled.getKeySchemaId()).isNotEmpty(); - assertThat(polled.getValueSchemaId()).isNotEmpty(); + assertThat(polled.getValueDeserializeProperties().get("schemaId")).isNotNull(); + assertThat(polled.getKeyDeserializeProperties().get("type")).isEqualTo("PROTOBUF"); + assertThat(polled.getValueDeserializeProperties().get("schemaId")).isNotNull(); + assertThat(polled.getValueDeserializeProperties().get("type")).isEqualTo("PROTOBUF"); }); } @@ -447,19 +407,21 @@ void topicMessageMetadataJson() { .withMsgToSend( new CreateTopicMessageDTO() .key(JSON_SCHEMA_RECORD) + .keySerde(SchemaRegistrySerde.name()) .content(JSON_SCHEMA_RECORD) + .valueSerde(SchemaRegistrySerde.name()) .headers(Map.of("header1", "value1")) ) .doAssert(polled -> { assertJsonEqual(polled.getKey(), JSON_SCHEMA_RECORD); assertJsonEqual(polled.getContent(), JSON_SCHEMA_RECORD); - assertThat(polled.getKeyFormat()).isEqualTo(MessageFormatDTO.JSON); - assertThat(polled.getValueFormat()).isEqualTo(MessageFormatDTO.JSON); - assertThat(polled.getKeySchemaId()).isNotEmpty(); - assertThat(polled.getValueSchemaId()).isNotEmpty(); assertThat(polled.getKeySize()).isEqualTo(57L); assertThat(polled.getValueSize()).isEqualTo(57L); assertThat(polled.getHeadersSize()).isEqualTo(13L); + assertThat(polled.getValueDeserializeProperties().get("schemaId")).isNotNull(); + assertThat(polled.getKeyDeserializeProperties().get("type")).isEqualTo("JSON"); + assertThat(polled.getValueDeserializeProperties().get("schemaId")).isNotNull(); + assertThat(polled.getValueDeserializeProperties().get("type")).isEqualTo("JSON"); }); } @@ -469,7 +431,9 @@ void noKeyAndNoContentPresentTest() { .withMsgToSend( new CreateTopicMessageDTO() .key(null) + .keySerde(StringSerde.name()) // any serde .content(null) + .valueSerde(StringSerde.name()) // any serde ) .doAssert(polled -> { assertThat(polled.getKey()).isNull(); @@ -514,10 +478,6 @@ private String createTopicAndCreateSchemas() { if (valueSchema != null) { schemaRegistry.schemaRegistryClient().register(topic + "-value", valueSchema); } - - // need to update to see new topic & schemas - clustersMetricsScheduler.updateMetrics(); - return topic; } @@ -542,12 +502,15 @@ public void doAssert(Consumer msgAssert) { topic, new ConsumerPosition( SeekTypeDTO.BEGINNING, - Map.of(new TopicPartition(topic, 0), 0L), - SeekDirectionDTO.FORWARD + topic, + Map.of(new TopicPartition(topic, 0), 0L) ), null, null, - 1 + 1, + SeekDirectionDTO.FORWARD, + msgToSend.getKeySerde().get(), + msgToSend.getValueSerde().get() ).filter(e -> e.getType().equals(TopicMessageEventDTO.TypeEnum.MESSAGE)) .map(TopicMessageEventDTO::getMessage) .blockLast(Duration.ofSeconds(5000)); diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/TopicsServicePaginationTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/TopicsServicePaginationTest.java index 76b1e8c4cb6..3a6cebc8347 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/TopicsServicePaginationTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/TopicsServicePaginationTest.java @@ -11,13 +11,16 @@ import com.provectus.kafka.ui.mapper.ClusterMapperImpl; import com.provectus.kafka.ui.model.InternalLogDirStats; import com.provectus.kafka.ui.model.InternalPartitionsOffsets; -import com.provectus.kafka.ui.model.InternalSchemaRegistry; import com.provectus.kafka.ui.model.InternalTopic; import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.Metrics; import com.provectus.kafka.ui.model.SortOrderDTO; import com.provectus.kafka.ui.model.TopicColumnsToSortDTO; import com.provectus.kafka.ui.model.TopicDTO; -import com.provectus.kafka.ui.util.JmxClusterUtil; +import com.provectus.kafka.ui.service.analyze.TopicAnalysisService; +import com.provectus.kafka.ui.service.audit.AuditService; +import com.provectus.kafka.ui.service.rbac.AccessControlService; +import com.provectus.kafka.ui.util.AccessControlServiceMock; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -40,8 +43,10 @@ class TopicsServicePaginationTest { private final TopicsService topicsService = mock(TopicsService.class); private final ClustersStorage clustersStorage = mock(ClustersStorage.class); private final ClusterMapper clusterMapper = new ClusterMapperImpl(); + private final AccessControlService accessControlService = new AccessControlServiceMock().getMock(); - private final TopicsController topicsController = new TopicsController(topicsService, clusterMapper); + private final TopicsController topicsController = + new TopicsController(topicsService, mock(TopicAnalysisService.class), clusterMapper); private void init(Map topicsInCache) { @@ -54,7 +59,9 @@ private void init(Map topicsInCache) { List lst = a.getArgument(1); return Mono.just(lst.stream().map(topicsInCache::get).collect(Collectors.toList())); }); - this.topicsController.setClustersStorage(clustersStorage); + topicsController.setAccessControlService(accessControlService); + topicsController.setAuditService(mock(AuditService.class)); + topicsController.setClustersStorage(clustersStorage); } @Test @@ -64,7 +71,7 @@ public void shouldListFirst25Topics() { .map(Objects::toString) .map(name -> new TopicDescription(name, false, List.of())) .map(topicDescription -> InternalTopic.from(topicDescription, List.of(), null, - JmxClusterUtil.JmxMetrics.empty(), InternalLogDirStats.empty())) + Metrics.empty(), InternalLogDirStats.empty(), "_")) .collect(Collectors.toMap(InternalTopic::getName, Function.identity())) ); @@ -81,7 +88,6 @@ public void shouldListFirst25Topics() { private KafkaCluster buildKafkaCluster(String clusterName) { return KafkaCluster.builder() .name(clusterName) - .schemaRegistry(InternalSchemaRegistry.builder().build()) .build(); } @@ -91,7 +97,7 @@ public void shouldListFirst25TopicsSortedByNameDescendingOrder() { .map(Objects::toString) .map(name -> new TopicDescription(name, false, List.of())) .map(topicDescription -> InternalTopic.from(topicDescription, List.of(), null, - JmxClusterUtil.JmxMetrics.empty(), InternalLogDirStats.empty())) + Metrics.empty(), InternalLogDirStats.empty(), "_")) .collect(Collectors.toMap(InternalTopic::getName, Function.identity())); init(internalTopics); @@ -118,7 +124,7 @@ public void shouldCalculateCorrectPageCountForNonDivisiblePageSize() { .map(Objects::toString) .map(name -> new TopicDescription(name, false, List.of())) .map(topicDescription -> InternalTopic.from(topicDescription, List.of(), null, - JmxClusterUtil.JmxMetrics.empty(), InternalLogDirStats.empty())) + Metrics.empty(), InternalLogDirStats.empty(), "_")) .collect(Collectors.toMap(InternalTopic::getName, Function.identity())) ); @@ -127,7 +133,7 @@ public void shouldCalculateCorrectPageCountForNonDivisiblePageSize() { assertThat(topics.getBody().getPageCount()).isEqualTo(4); assertThat(topics.getBody().getTopics()).hasSize(1); - assertThat(topics.getBody().getTopics().get(0).getName().equals("99")); + assertThat(topics.getBody().getTopics().get(0).getName()).isEqualTo("99"); } @Test @@ -137,7 +143,7 @@ public void shouldCorrectlyHandleNonPositivePageNumberAndPageSize() { .map(Objects::toString) .map(name -> new TopicDescription(name, false, List.of())) .map(topicDescription -> InternalTopic.from(topicDescription, List.of(), null, - JmxClusterUtil.JmxMetrics.empty(), InternalLogDirStats.empty())) + Metrics.empty(), InternalLogDirStats.empty(), "_")) .collect(Collectors.toMap(InternalTopic::getName, Function.identity())) ); @@ -156,7 +162,7 @@ public void shouldListBotInternalAndNonInternalTopics() { .map(Objects::toString) .map(name -> new TopicDescription(name, Integer.parseInt(name) % 10 == 0, List.of())) .map(topicDescription -> InternalTopic.from(topicDescription, List.of(), null, - JmxClusterUtil.JmxMetrics.empty(), InternalLogDirStats.empty())) + Metrics.empty(), InternalLogDirStats.empty(), "_")) .collect(Collectors.toMap(InternalTopic::getName, Function.identity())) ); @@ -177,7 +183,7 @@ public void shouldListOnlyNonInternalTopics() { .map(Objects::toString) .map(name -> new TopicDescription(name, Integer.parseInt(name) % 5 == 0, List.of())) .map(topicDescription -> InternalTopic.from(topicDescription, List.of(), null, - JmxClusterUtil.JmxMetrics.empty(), InternalLogDirStats.empty())) + Metrics.empty(), InternalLogDirStats.empty(), "_")) .collect(Collectors.toMap(InternalTopic::getName, Function.identity())) ); @@ -198,7 +204,7 @@ public void shouldListOnlyTopicsContainingOne() { .map(Objects::toString) .map(name -> new TopicDescription(name, false, List.of())) .map(topicDescription -> InternalTopic.from(topicDescription, List.of(), null, - JmxClusterUtil.JmxMetrics.empty(), InternalLogDirStats.empty())) + Metrics.empty(), InternalLogDirStats.empty(), "_")) .collect(Collectors.toMap(InternalTopic::getName, Function.identity())) ); @@ -220,7 +226,7 @@ public void shouldListTopicsOrderedByPartitionsCount() { new TopicPartitionInfo(p, null, List.of(), List.of())) .collect(Collectors.toList()))) .map(topicDescription -> InternalTopic.from(topicDescription, List.of(), InternalPartitionsOffsets.empty(), - JmxClusterUtil.JmxMetrics.empty(), InternalLogDirStats.empty())) + Metrics.empty(), InternalLogDirStats.empty(), "_")) .collect(Collectors.toMap(InternalTopic::getName, Function.identity())); init(internalTopics); diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/acl/AclCsvTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/acl/AclCsvTest.java new file mode 100644 index 00000000000..08ca4d15073 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/acl/AclCsvTest.java @@ -0,0 +1,70 @@ +package com.provectus.kafka.ui.service.acl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.provectus.kafka.ui.exception.ValidationException; +import java.util.Collection; +import java.util.List; +import org.apache.kafka.common.acl.AccessControlEntry; +import org.apache.kafka.common.acl.AclBinding; +import org.apache.kafka.common.acl.AclOperation; +import org.apache.kafka.common.acl.AclPermissionType; +import org.apache.kafka.common.resource.PatternType; +import org.apache.kafka.common.resource.ResourcePattern; +import org.apache.kafka.common.resource.ResourceType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class AclCsvTest { + + private static final List TEST_BINDINGS = List.of( + new AclBinding( + new ResourcePattern(ResourceType.TOPIC, "*", PatternType.LITERAL), + new AccessControlEntry("User:test1", "*", AclOperation.READ, AclPermissionType.ALLOW)), + new AclBinding( + new ResourcePattern(ResourceType.GROUP, "group1", PatternType.PREFIXED), + new AccessControlEntry("User:test2", "localhost", AclOperation.DESCRIBE, AclPermissionType.DENY)) + ); + + @ParameterizedTest + @ValueSource(strings = { + "Principal,ResourceType, PatternType, ResourceName,Operation,PermissionType,Host\n" + + "User:test1,TOPIC,LITERAL,*,READ,ALLOW,*\n" + + "User:test2,GROUP,PREFIXED,group1,DESCRIBE,DENY,localhost", + + //without header + "User:test1,TOPIC,LITERAL,*,READ,ALLOW,*\n" + + "\n" + + "User:test2,GROUP,PREFIXED,group1,DESCRIBE,DENY,localhost" + + "\n" + }) + void parsesValidInputCsv(String csvString) { + Collection parsed = AclCsv.parseCsv(csvString); + assertThat(parsed).containsExactlyInAnyOrderElementsOf(TEST_BINDINGS); + } + + @ParameterizedTest + @ValueSource(strings = { + // columns > 7 + "User:test1,TOPIC,LITERAL,*,READ,ALLOW,*,1,2,3,4", + // columns < 7 + "User:test1,TOPIC,LITERAL,*", + // enum values are illegal + "User:test1,ILLEGAL,LITERAL,*,READ,ALLOW,*", + "User:test1,TOPIC,LITERAL,*,READ,ILLEGAL,*" + }) + void throwsExceptionForInvalidInputCsv(String csvString) { + assertThatThrownBy(() -> AclCsv.parseCsv(csvString)) + .isInstanceOf(ValidationException.class); + } + + @Test + void transformAndParseUseSameFormat() { + String csv = AclCsv.transformToCsvString(TEST_BINDINGS); + Collection parsedBindings = AclCsv.parseCsv(csv); + assertThat(parsedBindings).containsExactlyInAnyOrderElementsOf(TEST_BINDINGS); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/acl/AclsServiceTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/acl/AclsServiceTest.java new file mode 100644 index 00000000000..340aad7091c --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/acl/AclsServiceTest.java @@ -0,0 +1,290 @@ +package com.provectus.kafka.ui.service.acl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.provectus.kafka.ui.model.CreateConsumerAclDTO; +import com.provectus.kafka.ui.model.CreateProducerAclDTO; +import com.provectus.kafka.ui.model.CreateStreamAppAclDTO; +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.service.AdminClientService; +import com.provectus.kafka.ui.service.ReactiveAdminClient; +import java.util.Collection; +import java.util.List; +import java.util.UUID; +import org.apache.kafka.common.acl.AccessControlEntry; +import org.apache.kafka.common.acl.AclBinding; +import org.apache.kafka.common.acl.AclOperation; +import org.apache.kafka.common.acl.AclPermissionType; +import org.apache.kafka.common.resource.PatternType; +import org.apache.kafka.common.resource.Resource; +import org.apache.kafka.common.resource.ResourcePattern; +import org.apache.kafka.common.resource.ResourcePatternFilter; +import org.apache.kafka.common.resource.ResourceType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import reactor.core.publisher.Mono; + +class AclsServiceTest { + + private static final KafkaCluster CLUSTER = KafkaCluster.builder().build(); + + private final ReactiveAdminClient adminClientMock = mock(ReactiveAdminClient.class); + private final AdminClientService adminClientService = mock(AdminClientService.class); + + private final AclsService aclsService = new AclsService(adminClientService); + + @BeforeEach + void initMocks() { + when(adminClientService.get(CLUSTER)).thenReturn(Mono.just(adminClientMock)); + } + + @Test + void testSyncAclWithAclCsv() { + var existingBinding1 = new AclBinding( + new ResourcePattern(ResourceType.TOPIC, "*", PatternType.LITERAL), + new AccessControlEntry("User:test1", "*", AclOperation.READ, AclPermissionType.ALLOW)); + + var existingBinding2 = new AclBinding( + new ResourcePattern(ResourceType.GROUP, "group1", PatternType.PREFIXED), + new AccessControlEntry("User:test2", "localhost", AclOperation.DESCRIBE, AclPermissionType.DENY)); + + var newBindingToBeAdded = new AclBinding( + new ResourcePattern(ResourceType.GROUP, "groupNew", PatternType.PREFIXED), + new AccessControlEntry("User:test3", "localhost", AclOperation.DESCRIBE, AclPermissionType.DENY)); + + when(adminClientMock.listAcls(ResourcePatternFilter.ANY)) + .thenReturn(Mono.just(List.of(existingBinding1, existingBinding2))); + + ArgumentCaptor> createdCaptor = ArgumentCaptor.forClass(Collection.class); + when(adminClientMock.createAcls(createdCaptor.capture())) + .thenReturn(Mono.empty()); + + ArgumentCaptor> deletedCaptor = ArgumentCaptor.forClass(Collection.class); + when(adminClientMock.deleteAcls(deletedCaptor.capture())) + .thenReturn(Mono.empty()); + + aclsService.syncAclWithAclCsv( + CLUSTER, + "Principal,ResourceType, PatternType, ResourceName,Operation,PermissionType,Host\n" + + "User:test1,TOPIC,LITERAL,*,READ,ALLOW,*\n" + + "User:test3,GROUP,PREFIXED,groupNew,DESCRIBE,DENY,localhost" + ).block(); + + Collection createdBindings = createdCaptor.getValue(); + assertThat(createdBindings) + .hasSize(1) + .contains(newBindingToBeAdded); + + Collection deletedBindings = deletedCaptor.getValue(); + assertThat(deletedBindings) + .hasSize(1) + .contains(existingBinding2); + } + + + @Test + void createsConsumerDependantAcls() { + ArgumentCaptor> createdCaptor = ArgumentCaptor.forClass(Collection.class); + when(adminClientMock.createAcls(createdCaptor.capture())) + .thenReturn(Mono.empty()); + + var principal = UUID.randomUUID().toString(); + var host = UUID.randomUUID().toString(); + + aclsService.createConsumerAcl( + CLUSTER, + new CreateConsumerAclDTO() + .principal(principal) + .host(host) + .consumerGroups(List.of("cg1", "cg2")) + .topics(List.of("t1", "t2")) + ).block(); + + //Read, Describe on topics, Read on consumerGroups + Collection createdBindings = createdCaptor.getValue(); + assertThat(createdBindings) + .hasSize(6) + .contains(new AclBinding( + new ResourcePattern(ResourceType.TOPIC, "t1", PatternType.LITERAL), + new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.TOPIC, "t1", PatternType.LITERAL), + new AccessControlEntry(principal, host, AclOperation.DESCRIBE, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.TOPIC, "t2", PatternType.LITERAL), + new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.TOPIC, "t2", PatternType.LITERAL), + new AccessControlEntry(principal, host, AclOperation.DESCRIBE, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.GROUP, "cg1", PatternType.LITERAL), + new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.GROUP, "cg2", PatternType.LITERAL), + new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW))); + } + + @Test + void createsConsumerDependantAclsWhenTopicsAndGroupsSpecifiedByPrefix() { + ArgumentCaptor> createdCaptor = ArgumentCaptor.forClass(Collection.class); + when(adminClientMock.createAcls(createdCaptor.capture())) + .thenReturn(Mono.empty()); + + var principal = UUID.randomUUID().toString(); + var host = UUID.randomUUID().toString(); + + aclsService.createConsumerAcl( + CLUSTER, + new CreateConsumerAclDTO() + .principal(principal) + .host(host) + .consumerGroupsPrefix("cgPref") + .topicsPrefix("topicPref") + ).block(); + + //Read, Describe on topics, Read on consumerGroups + Collection createdBindings = createdCaptor.getValue(); + assertThat(createdBindings) + .hasSize(3) + .contains(new AclBinding( + new ResourcePattern(ResourceType.TOPIC, "topicPref", PatternType.PREFIXED), + new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.TOPIC, "topicPref", PatternType.PREFIXED), + new AccessControlEntry(principal, host, AclOperation.DESCRIBE, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.GROUP, "cgPref", PatternType.PREFIXED), + new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW))); + } + + @Test + void createsProducerDependantAcls() { + ArgumentCaptor> createdCaptor = ArgumentCaptor.forClass(Collection.class); + when(adminClientMock.createAcls(createdCaptor.capture())) + .thenReturn(Mono.empty()); + + var principal = UUID.randomUUID().toString(); + var host = UUID.randomUUID().toString(); + + aclsService.createProducerAcl( + CLUSTER, + new CreateProducerAclDTO() + .principal(principal) + .host(host) + .topics(List.of("t1")) + .idempotent(true) + .transactionalId("txId1") + ).block(); + + //Write, Describe, Create permission on topics, Write, Describe on transactionalIds + //IDEMPOTENT_WRITE on cluster if idempotent is enabled (true) + Collection createdBindings = createdCaptor.getValue(); + assertThat(createdBindings) + .hasSize(6) + .contains(new AclBinding( + new ResourcePattern(ResourceType.TOPIC, "t1", PatternType.LITERAL), + new AccessControlEntry(principal, host, AclOperation.WRITE, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.TOPIC, "t1", PatternType.LITERAL), + new AccessControlEntry(principal, host, AclOperation.DESCRIBE, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.TOPIC, "t1", PatternType.LITERAL), + new AccessControlEntry(principal, host, AclOperation.CREATE, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.TRANSACTIONAL_ID, "txId1", PatternType.LITERAL), + new AccessControlEntry(principal, host, AclOperation.WRITE, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.TRANSACTIONAL_ID, "txId1", PatternType.LITERAL), + new AccessControlEntry(principal, host, AclOperation.DESCRIBE, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.CLUSTER, Resource.CLUSTER_NAME, PatternType.LITERAL), + new AccessControlEntry(principal, host, AclOperation.IDEMPOTENT_WRITE, AclPermissionType.ALLOW))); + } + + + @Test + void createsProducerDependantAclsWhenTopicsAndTxIdSpecifiedByPrefix() { + ArgumentCaptor> createdCaptor = ArgumentCaptor.forClass(Collection.class); + when(adminClientMock.createAcls(createdCaptor.capture())) + .thenReturn(Mono.empty()); + + var principal = UUID.randomUUID().toString(); + var host = UUID.randomUUID().toString(); + + aclsService.createProducerAcl( + CLUSTER, + new CreateProducerAclDTO() + .principal(principal) + .host(host) + .topicsPrefix("topicPref") + .transactionsIdPrefix("txIdPref") + .idempotent(false) + ).block(); + + //Write, Describe, Create permission on topics, Write, Describe on transactionalIds + //IDEMPOTENT_WRITE on cluster if idempotent is enabled (false) + Collection createdBindings = createdCaptor.getValue(); + assertThat(createdBindings) + .hasSize(5) + .contains(new AclBinding( + new ResourcePattern(ResourceType.TOPIC, "topicPref", PatternType.PREFIXED), + new AccessControlEntry(principal, host, AclOperation.WRITE, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.TOPIC, "topicPref", PatternType.PREFIXED), + new AccessControlEntry(principal, host, AclOperation.DESCRIBE, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.TOPIC, "topicPref", PatternType.PREFIXED), + new AccessControlEntry(principal, host, AclOperation.CREATE, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.TRANSACTIONAL_ID, "txIdPref", PatternType.PREFIXED), + new AccessControlEntry(principal, host, AclOperation.WRITE, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.TRANSACTIONAL_ID, "txIdPref", PatternType.PREFIXED), + new AccessControlEntry(principal, host, AclOperation.DESCRIBE, AclPermissionType.ALLOW))); + } + + + @Test + void createsStreamAppDependantAcls() { + ArgumentCaptor> createdCaptor = ArgumentCaptor.forClass(Collection.class); + when(adminClientMock.createAcls(createdCaptor.capture())) + .thenReturn(Mono.empty()); + + var principal = UUID.randomUUID().toString(); + var host = UUID.randomUUID().toString(); + + aclsService.createStreamAppAcl( + CLUSTER, + new CreateStreamAppAclDTO() + .principal(principal) + .host(host) + .inputTopics(List.of("t1")) + .outputTopics(List.of("t2", "t3")) + .applicationId("appId1") + ).block(); + + // Read on input topics, Write on output topics + // ALL on applicationId-prefixed Groups and Topics + Collection createdBindings = createdCaptor.getValue(); + assertThat(createdBindings) + .hasSize(5) + .contains(new AclBinding( + new ResourcePattern(ResourceType.TOPIC, "t1", PatternType.LITERAL), + new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.TOPIC, "t2", PatternType.LITERAL), + new AccessControlEntry(principal, host, AclOperation.WRITE, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.TOPIC, "t3", PatternType.LITERAL), + new AccessControlEntry(principal, host, AclOperation.WRITE, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.GROUP, "appId1", PatternType.PREFIXED), + new AccessControlEntry(principal, host, AclOperation.ALL, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.TOPIC, "appId1", PatternType.PREFIXED), + new AccessControlEntry(principal, host, AclOperation.ALL, AclPermissionType.ALLOW))); + } +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisServiceTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisServiceTest.java new file mode 100644 index 00000000000..7d02219e9c1 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisServiceTest.java @@ -0,0 +1,62 @@ +package com.provectus.kafka.ui.service.analyze; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.provectus.kafka.ui.AbstractIntegrationTest; +import com.provectus.kafka.ui.producer.KafkaTestProducer; +import com.provectus.kafka.ui.service.ClustersStorage; +import java.time.Duration; +import java.util.UUID; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.kafka.clients.admin.NewTopic; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.testcontainers.shaded.org.awaitility.Awaitility; + + +class TopicAnalysisServiceTest extends AbstractIntegrationTest { + + @Autowired + private ClustersStorage clustersStorage; + + @Autowired + private TopicAnalysisService topicAnalysisService; + + @Test + void savesResultWhenAnalysisIsCompleted() { + String topic = "analyze_test_" + UUID.randomUUID(); + createTopic(new NewTopic(topic, 2, (short) 1)); + fillTopic(topic, 1_000); + + var cluster = clustersStorage.getClusterByName(LOCAL).get(); + topicAnalysisService.analyze(cluster, topic).block(); + + Awaitility.await() + .atMost(Duration.ofSeconds(20)) + .untilAsserted(() -> { + assertThat(topicAnalysisService.getTopicAnalysis(cluster, topic)) + .hasValueSatisfying(state -> { + assertThat(state.getProgress()).isNull(); + assertThat(state.getResult()).isNotNull(); + var completedAnalyze = state.getResult(); + assertThat(completedAnalyze.getTotalStats().getTotalMsgs()).isEqualTo(1_000); + assertThat(completedAnalyze.getPartitionStats().size()).isEqualTo(2); + }); + }); + } + + private void fillTopic(String topic, int cnt) { + try (var producer = KafkaTestProducer.forKafka(kafka)) { + for (int i = 0; i < cnt; i++) { + producer.send( + new ProducerRecord<>( + topic, + RandomStringUtils.randomAlphabetic(5), + RandomStringUtils.randomAlphabetic(10))); + } + } + } + + +} \ No newline at end of file diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/audit/AuditIntegrationTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/audit/AuditIntegrationTest.java new file mode 100644 index 00000000000..8e17bca965f --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/audit/AuditIntegrationTest.java @@ -0,0 +1,87 @@ +package com.provectus.kafka.ui.service.audit; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.provectus.kafka.ui.AbstractIntegrationTest; +import com.provectus.kafka.ui.model.TopicCreationDTO; +import com.provectus.kafka.ui.model.rbac.Resource; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.UUID; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.common.serialization.BytesDeserializer; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.testcontainers.shaded.org.awaitility.Awaitility; + +public class AuditIntegrationTest extends AbstractIntegrationTest { + + @Autowired + private WebTestClient webTestClient; + + @Test + void auditRecordWrittenIntoKafkaWhenNewTopicCreated() { + String newTopicName = "test_audit_" + UUID.randomUUID(); + + webTestClient.post() + .uri("/api/clusters/{clusterName}/topics", LOCAL) + .bodyValue( + new TopicCreationDTO() + .replicationFactor(1) + .partitions(1) + .name(newTopicName) + ) + .exchange() + .expectStatus() + .isOk(); + + try (var consumer = createConsumer()) { + var jsonMapper = new JsonMapper(); + consumer.subscribe(List.of("__kui-audit-log")); + Awaitility.await() + .pollInSameThread() + .atMost(Duration.ofSeconds(15)) + .untilAsserted(() -> { + var polled = consumer.poll(Duration.ofSeconds(1)); + assertThat(polled).anySatisfy(kafkaRecord -> { + try { + AuditRecord record = jsonMapper.readValue(kafkaRecord.value(), AuditRecord.class); + assertThat(record.operation()).isEqualTo("createTopic"); + assertThat(record.resources()).map(AuditRecord.AuditResource::type).contains(Resource.TOPIC); + assertThat(record.result().success()).isTrue(); + assertThat(record.timestamp()).isNotBlank(); + assertThat(record.clusterName()).isEqualTo(LOCAL); + assertThat(record.operationParams()) + .isEqualTo(Map.of( + "name", newTopicName, + "partitions", 1, + "replicationFactor", 1, + "configs", Map.of() + )); + } catch (JsonProcessingException e) { + Assertions.fail(); + } + }); + }); + } + } + + private KafkaConsumer createConsumer() { + Properties props = new Properties(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers()); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, BytesDeserializer.class); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + props.put(ConsumerConfig.GROUP_ID_CONFIG, AuditIntegrationTest.class.getName()); + return new KafkaConsumer<>(props); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/audit/AuditServiceTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/audit/AuditServiceTest.java new file mode 100644 index 00000000000..49781bcd3e4 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/audit/AuditServiceTest.java @@ -0,0 +1,165 @@ +package com.provectus.kafka.ui.service.audit; + +import static com.provectus.kafka.ui.service.audit.AuditService.createAuditWriter; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyMap; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.provectus.kafka.ui.config.ClustersProperties; +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.rbac.AccessContext; +import com.provectus.kafka.ui.service.ReactiveAdminClient; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Signal; + +class AuditServiceTest { + + @Test + void isAuditTopicChecksIfAuditIsEnabledForCluster() { + Map writers = Map.of( + "c1", new AuditWriter("с1", true, "c1topic", null, null), + "c2", new AuditWriter("c2", false, "c2topic", mock(KafkaProducer.class), null) + ); + + var auditService = new AuditService(writers); + assertThat(auditService.isAuditTopic(KafkaCluster.builder().name("notExist").build(), "some")) + .isFalse(); + assertThat(auditService.isAuditTopic(KafkaCluster.builder().name("c1").build(), "c1topic")) + .isFalse(); + assertThat(auditService.isAuditTopic(KafkaCluster.builder().name("c2").build(), "c2topic")) + .isTrue(); + } + + @Test + void auditCallsWriterMethodDependingOnSignal() { + var auditWriter = mock(AuditWriter.class); + var auditService = new AuditService(Map.of("test", auditWriter)); + + var cxt = AccessContext.builder().cluster("test").build(); + + auditService.audit(cxt, Signal.complete()); + verify(auditWriter).write(any(), any(), eq(null)); + + var th = new Exception("testError"); + auditService.audit(cxt, Signal.error(th)); + verify(auditWriter).write(any(), any(), eq(th)); + } + + @Nested + class CreateAuditWriter { + + private final ReactiveAdminClient adminClientMock = mock(ReactiveAdminClient.class); + private final Supplier> producerSupplierMock = mock(Supplier.class); + + private final ClustersProperties.Cluster clustersProperties = new ClustersProperties.Cluster(); + + private final KafkaCluster cluster = KafkaCluster + .builder() + .name("test") + .originalProperties(clustersProperties) + .build(); + + + @BeforeEach + void init() { + when(producerSupplierMock.get()) + .thenReturn(mock(KafkaProducer.class)); + } + + @Test + void logOnlyAlterOpsByDefault() { + var auditProps = new ClustersProperties.AuditProperties(); + auditProps.setConsoleAuditEnabled(true); + clustersProperties.setAudit(auditProps); + + var maybeWriter = createAuditWriter(cluster, () -> adminClientMock, producerSupplierMock); + assertThat(maybeWriter) + .hasValueSatisfying(w -> assertThat(w.logAlterOperationsOnly()).isTrue()); + } + + @Test + void noWriterIfNoAuditPropsSet() { + var maybeWriter = createAuditWriter(cluster, () -> adminClientMock, producerSupplierMock); + assertThat(maybeWriter).isEmpty(); + } + + @Test + void setsLoggerIfConsoleLoggingEnabled() { + var auditProps = new ClustersProperties.AuditProperties(); + auditProps.setConsoleAuditEnabled(true); + clustersProperties.setAudit(auditProps); + + var maybeWriter = createAuditWriter(cluster, () -> adminClientMock, producerSupplierMock); + assertThat(maybeWriter).isPresent(); + + var writer = maybeWriter.get(); + assertThat(writer.consoleLogger()).isNotNull(); + } + + @Nested + class WhenTopicAuditEnabled { + + @BeforeEach + void setTopicWriteProperties() { + var auditProps = new ClustersProperties.AuditProperties(); + auditProps.setTopicAuditEnabled(true); + auditProps.setTopic("test_audit_topic"); + auditProps.setAuditTopicsPartitions(3); + auditProps.setAuditTopicProperties(Map.of("p1", "v1")); + clustersProperties.setAudit(auditProps); + } + + @Test + void createsProducerIfTopicExists() { + when(adminClientMock.listTopics(true)) + .thenReturn(Mono.just(Set.of("test_audit_topic"))); + + var maybeWriter = createAuditWriter(cluster, () -> adminClientMock, producerSupplierMock); + assertThat(maybeWriter).isPresent(); + + //checking there was no topic creation request + verify(adminClientMock, times(0)) + .createTopic(any(), anyInt(), anyInt(), anyMap()); + + var writer = maybeWriter.get(); + assertThat(writer.producer()).isNotNull(); + assertThat(writer.targetTopic()).isEqualTo("test_audit_topic"); + } + + @Test + void createsProducerAndTopicIfItIsNotExist() { + when(adminClientMock.listTopics(true)) + .thenReturn(Mono.just(Set.of())); + + when(adminClientMock.createTopic(eq("test_audit_topic"), eq(3), eq(null), anyMap())) + .thenReturn(Mono.empty()); + + var maybeWriter = createAuditWriter(cluster, () -> adminClientMock, producerSupplierMock); + assertThat(maybeWriter).isPresent(); + + //verifying topic created + verify(adminClientMock).createTopic(eq("test_audit_topic"), eq(3), eq(null), anyMap()); + + var writer = maybeWriter.get(); + assertThat(writer.producer()).isNotNull(); + assertThat(writer.targetTopic()).isEqualTo("test_audit_topic"); + } + + } + } + + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/audit/AuditWriterTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/audit/AuditWriterTest.java new file mode 100644 index 00000000000..5bcee45ac8e --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/audit/AuditWriterTest.java @@ -0,0 +1,86 @@ +package com.provectus.kafka.ui.service.audit; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import com.provectus.kafka.ui.config.auth.AuthenticatedUser; +import com.provectus.kafka.ui.model.rbac.AccessContext; +import com.provectus.kafka.ui.model.rbac.AccessContext.AccessContextBuilder; +import com.provectus.kafka.ui.model.rbac.permission.AclAction; +import com.provectus.kafka.ui.model.rbac.permission.ClusterConfigAction; +import com.provectus.kafka.ui.model.rbac.permission.ConnectAction; +import com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction; +import com.provectus.kafka.ui.model.rbac.permission.SchemaAction; +import com.provectus.kafka.ui.model.rbac.permission.TopicAction; +import java.util.List; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; +import org.slf4j.Logger; + +class AuditWriterTest { + + final KafkaProducer producerMock = Mockito.mock(KafkaProducer.class); + final Logger loggerMock = Mockito.mock(Logger.class); + final AuthenticatedUser user = new AuthenticatedUser("someone", List.of()); + + @Nested + class AlterOperationsOnlyWriter { + + final AuditWriter alterOnlyWriter = new AuditWriter("test", true, "test-topic", producerMock, loggerMock); + + @ParameterizedTest + @MethodSource + void onlyLogsWhenAlterOperationIsPresentForOneOfResources(AccessContext ctxWithAlterOperation) { + alterOnlyWriter.write(ctxWithAlterOperation, user, null); + verify(producerMock).send(any(), any()); + verify(loggerMock).info(any()); + } + + static Stream onlyLogsWhenAlterOperationIsPresentForOneOfResources() { + Stream> topicEditActions = + TopicAction.ALTER_ACTIONS.stream().map(a -> c -> c.topic("test").topicActions(a)); + Stream> clusterConfigEditActions = + ClusterConfigAction.ALTER_ACTIONS.stream().map(a -> c -> c.clusterConfigActions(a)); + Stream> aclEditActions = + AclAction.ALTER_ACTIONS.stream().map(a -> c -> c.aclActions(a)); + Stream> cgEditActions = + ConsumerGroupAction.ALTER_ACTIONS.stream().map(a -> c -> c.consumerGroup("cg").consumerGroupActions(a)); + Stream> schemaEditActions = + SchemaAction.ALTER_ACTIONS.stream().map(a -> c -> c.schema("sc").schemaActions(a)); + Stream> connEditActions = + ConnectAction.ALTER_ACTIONS.stream().map(a -> c -> c.connect("conn").connectActions(a)); + return Stream.of( + topicEditActions, clusterConfigEditActions, aclEditActions, + cgEditActions, connEditActions, schemaEditActions + ) + .flatMap(c -> c) + .map(setter -> setter.apply(AccessContext.builder().cluster("test").operationName("test")).build()); + } + + @ParameterizedTest + @MethodSource + void doesNothingIfNoResourceHasAlterAction(AccessContext readOnlyCxt) { + alterOnlyWriter.write(readOnlyCxt, user, null); + verifyNoInteractions(producerMock); + verifyNoInteractions(loggerMock); + } + + static Stream doesNothingIfNoResourceHasAlterAction() { + return Stream.>of( + c -> c.topic("test").topicActions(TopicAction.VIEW), + c -> c.clusterConfigActions(ClusterConfigAction.VIEW), + c -> c.aclActions(AclAction.VIEW), + c -> c.consumerGroup("cg").consumerGroupActions(ConsumerGroupAction.VIEW), + c -> c.schema("sc").schemaActions(SchemaAction.VIEW), + c -> c.connect("conn").connectActions(ConnectAction.VIEW) + ).map(setter -> setter.apply(AccessContext.builder().cluster("test").operationName("test")).build()); + } + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/ConnectorsExporterTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/ConnectorsExporterTest.java new file mode 100644 index 00000000000..e06a16388ee --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/ConnectorsExporterTest.java @@ -0,0 +1,111 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.provectus.kafka.ui.connect.model.ConnectorTopics; +import com.provectus.kafka.ui.model.ConnectDTO; +import com.provectus.kafka.ui.model.ConnectorDTO; +import com.provectus.kafka.ui.model.ConnectorTypeDTO; +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.service.KafkaConnectService; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.opendatadiscovery.client.model.DataEntity; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +class ConnectorsExporterTest { + + private static final KafkaCluster CLUSTER = KafkaCluster.builder() + .name("test cluster") + .bootstrapServers("localhost:9092") + .build(); + + private final KafkaConnectService kafkaConnectService = mock(KafkaConnectService.class); + private final ConnectorsExporter exporter = new ConnectorsExporter(kafkaConnectService); + + @Test + void exportsConnectorsAsDataTransformers() { + ConnectDTO connect = new ConnectDTO(); + connect.setName("testConnect"); + connect.setAddress("http://kconnect:8083"); + + ConnectorDTO sinkConnector = new ConnectorDTO(); + sinkConnector.setName("testSink"); + sinkConnector.setType(ConnectorTypeDTO.SINK); + sinkConnector.setConnect(connect.getName()); + sinkConnector.setConfig( + Map.of( + "connector.class", "FileStreamSink", + "file", "filePathHere", + "topic", "inputTopic" + ) + ); + + ConnectorDTO sourceConnector = new ConnectorDTO(); + sourceConnector.setName("testSource"); + sourceConnector.setConnect(connect.getName()); + sourceConnector.setType(ConnectorTypeDTO.SOURCE); + sourceConnector.setConfig( + Map.of( + "connector.class", "FileStreamSource", + "file", "filePathHere", + "topic", "outputTopic" + ) + ); + + when(kafkaConnectService.getConnects(CLUSTER)) + .thenReturn(Flux.just(connect)); + + when(kafkaConnectService.getConnectorNamesWithErrorsSuppress(CLUSTER, connect.getName())) + .thenReturn(Flux.just(sinkConnector.getName(), sourceConnector.getName())); + + when(kafkaConnectService.getConnector(CLUSTER, connect.getName(), sinkConnector.getName())) + .thenReturn(Mono.just(sinkConnector)); + + when(kafkaConnectService.getConnector(CLUSTER, connect.getName(), sourceConnector.getName())) + .thenReturn(Mono.just(sourceConnector)); + + when(kafkaConnectService.getConnectorTopics(CLUSTER, connect.getName(), sourceConnector.getName())) + .thenReturn(Mono.just(new ConnectorTopics().topics(List.of("outputTopic")))); + + when(kafkaConnectService.getConnectorTopics(CLUSTER, connect.getName(), sinkConnector.getName())) + .thenReturn(Mono.just(new ConnectorTopics().topics(List.of("inputTopic")))); + + StepVerifier.create(exporter.export(CLUSTER)) + .assertNext(dataEntityList -> { + assertThat(dataEntityList.getDataSourceOddrn()) + .isEqualTo("//kafkaconnect/host/kconnect:8083"); + + assertThat(dataEntityList.getItems()) + .hasSize(2); + + assertThat(dataEntityList.getItems()) + .filteredOn(DataEntity::getOddrn, "//kafkaconnect/host/kconnect:8083/connectors/testSink") + .singleElement() + .satisfies(sink -> { + assertThat(sink.getMetadata().get(0).getMetadata()) + .containsOnlyKeys("type", "connector.class", "file", "topic"); + assertThat(sink.getDataTransformer().getInputs()).contains( + "//kafka/cluster/localhost:9092/topics/inputTopic"); + }); + + assertThat(dataEntityList.getItems()) + .filteredOn(DataEntity::getOddrn, "//kafkaconnect/host/kconnect:8083/connectors/testSource") + .singleElement() + .satisfies(source -> { + assertThat(source.getMetadata().get(0).getMetadata()) + .containsOnlyKeys("type", "connector.class", "file", "topic"); + assertThat(source.getDataTransformer().getOutputs()).contains( + "//kafka/cluster/localhost:9092/topics/outputTopic"); + }); + + }) + .verifyComplete(); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/SchemaReferencesResolverTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/SchemaReferencesResolverTest.java new file mode 100644 index 00000000000..d24524473a7 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/SchemaReferencesResolverTest.java @@ -0,0 +1,86 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import com.provectus.kafka.ui.sr.api.KafkaSrClientApi; +import com.provectus.kafka.ui.sr.model.SchemaReference; +import com.provectus.kafka.ui.sr.model.SchemaSubject; +import java.util.List; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +class SchemaReferencesResolverTest { + + private final KafkaSrClientApi srClientMock = mock(KafkaSrClientApi.class); + + private final SchemaReferencesResolver schemaReferencesResolver = new SchemaReferencesResolver(srClientMock); + + @Test + void resolvesRefsUsingSrClient() { + mockSrCall("sub1", 1, + new SchemaSubject() + .schema("schema1")); + + mockSrCall("sub2", 1, + new SchemaSubject() + .schema("schema2") + .references( + List.of( + new SchemaReference().name("ref2_1").subject("sub2_1").version(2), + new SchemaReference().name("ref2_2").subject("sub1").version(1)))); + + mockSrCall("sub2_1", 2, + new SchemaSubject() + .schema("schema2_1") + .references( + List.of( + new SchemaReference().name("ref2_1_1").subject("sub2_1_1").version(3), + new SchemaReference().name("ref1").subject("should_not_be_called").version(1) + )) + ); + + mockSrCall("sub2_1_1", 3, + new SchemaSubject() + .schema("schema2_1_1")); + + var resolvedRefsMono = schemaReferencesResolver.resolve( + List.of( + new SchemaReference().name("ref1").subject("sub1").version(1), + new SchemaReference().name("ref2").subject("sub2").version(1))); + + StepVerifier.create(resolvedRefsMono) + .assertNext(refs -> + assertThat(refs) + .containsExactlyEntriesOf( + // checking map should be ordered + ImmutableMap.builder() + .put("ref1", "schema1") + .put("ref2_1_1", "schema2_1_1") + .put("ref2_1", "schema2_1") + .put("ref2_2", "schema1") + .put("ref2", "schema2") + .build())) + .verifyComplete(); + } + + @Test + void returnsEmptyMapOnEmptyInputs() { + StepVerifier.create(schemaReferencesResolver.resolve(null)) + .assertNext(map -> assertThat(map).isEmpty()) + .verifyComplete(); + + StepVerifier.create(schemaReferencesResolver.resolve(List.of())) + .assertNext(map -> assertThat(map).isEmpty()) + .verifyComplete(); + } + + private void mockSrCall(String subject, int version, SchemaSubject subjectToReturn) { + when(srClientMock.getSubjectVersion(subject, version + "", true)) + .thenReturn(Mono.just(subjectToReturn)); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/TopicsExporterTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/TopicsExporterTest.java new file mode 100644 index 00000000000..cb4103467be --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/TopicsExporterTest.java @@ -0,0 +1,169 @@ +package com.provectus.kafka.ui.service.integration.odd; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.Statistics; +import com.provectus.kafka.ui.service.StatisticsCache; +import com.provectus.kafka.ui.sr.api.KafkaSrClientApi; +import com.provectus.kafka.ui.sr.model.SchemaSubject; +import com.provectus.kafka.ui.sr.model.SchemaType; +import com.provectus.kafka.ui.util.ReactiveFailover; +import java.util.List; +import java.util.Map; +import org.apache.kafka.clients.admin.ConfigEntry; +import org.apache.kafka.clients.admin.TopicDescription; +import org.apache.kafka.common.Node; +import org.apache.kafka.common.TopicPartitionInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opendatadiscovery.client.model.DataEntity; +import org.opendatadiscovery.client.model.DataEntityType; +import org.springframework.http.HttpHeaders; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +class TopicsExporterTest { + + private final KafkaSrClientApi schemaRegistryClientMock = mock(KafkaSrClientApi.class); + + private final KafkaCluster cluster = KafkaCluster.builder() + .name("testCluster") + .bootstrapServers("localhost:9092,localhost:19092") + .schemaRegistryClient(ReactiveFailover.createNoop(schemaRegistryClientMock)) + .build(); + + private Statistics stats; + + private TopicsExporter topicsExporter; + + @BeforeEach + void init() { + var statisticsCacheMock = mock(StatisticsCache.class); + when(statisticsCacheMock.get(cluster)).thenAnswer(invocationOnMock -> stats); + + topicsExporter = new TopicsExporter( + topic -> !topic.startsWith("_"), + statisticsCacheMock + ); + } + + @Test + void doesNotExportTopicsWhichDontFitFiltrationRule() { + when(schemaRegistryClientMock.getSubjectVersion(anyString(), anyString(), anyBoolean())) + .thenReturn(Mono.error(WebClientResponseException.create(404, "NF", new HttpHeaders(), null, null, null))); + stats = Statistics.empty() + .toBuilder() + .topicDescriptions( + Map.of( + "_hidden", new TopicDescription("_hidden", false, List.of( + new TopicPartitionInfo(0, null, List.of(), List.of()) + )), + "visible", new TopicDescription("visible", false, List.of( + new TopicPartitionInfo(0, null, List.of(), List.of()) + )) + ) + ) + .build(); + + StepVerifier.create(topicsExporter.export(cluster)) + .assertNext(entityList -> { + assertThat(entityList.getDataSourceOddrn()) + .isNotEmpty(); + + assertThat(entityList.getItems()) + .hasSize(1) + .allSatisfy(e -> e.getOddrn().contains("visible")); + }) + .verifyComplete(); + } + + @Test + void doesExportTopicData() { + when(schemaRegistryClientMock.getSubjectVersion("testTopic-value", "latest", false)) + .thenReturn(Mono.just( + new SchemaSubject() + .schema("\"string\"") + .schemaType(SchemaType.AVRO) + )); + + when(schemaRegistryClientMock.getSubjectVersion("testTopic-key", "latest", false)) + .thenReturn(Mono.just( + new SchemaSubject() + .schema("\"int\"") + .schemaType(SchemaType.AVRO) + )); + + stats = Statistics.empty() + .toBuilder() + .topicDescriptions( + Map.of( + "testTopic", + new TopicDescription( + "testTopic", + false, + List.of( + new TopicPartitionInfo( + 0, + null, + List.of( + new Node(1, "host1", 9092), + new Node(2, "host2", 9092) + ), + List.of()) + )) + ) + ) + .topicConfigs( + Map.of( + "testTopic", List.of( + new ConfigEntry( + "custom.config", + "100500", + ConfigEntry.ConfigSource.DYNAMIC_TOPIC_CONFIG, + false, + false, + List.of(), + ConfigEntry.ConfigType.INT, + null + ) + ) + ) + ) + .build(); + + StepVerifier.create(topicsExporter.export(cluster)) + .assertNext(entityList -> { + assertThat(entityList.getItems()) + .hasSize(1); + + DataEntity topicEntity = entityList.getItems().get(0); + assertThat(topicEntity.getName()).isNotEmpty(); + assertThat(topicEntity.getOddrn()) + .isEqualTo("//kafka/cluster/localhost:19092,localhost:9092/topics/testTopic"); + assertThat(topicEntity.getType()).isEqualTo(DataEntityType.KAFKA_TOPIC); + assertThat(topicEntity.getMetadata()) + .hasSize(1) + .singleElement() + .satisfies(e -> + assertThat(e.getMetadata()) + .containsExactlyInAnyOrderEntriesOf( + Map.of( + "partitions", 1, + "replication_factor", 2, + "custom.config", "100500"))); + + assertThat(topicEntity.getDataset()).isNotNull(); + assertThat(topicEntity.getDataset().getFieldList()) + .hasSize(4); // 2 field for key, 2 for value + }) + .verifyComplete(); + } + + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/AvroExtractorTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/AvroExtractorTest.java new file mode 100644 index 00000000000..cd1baf77987 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/AvroExtractorTest.java @@ -0,0 +1,271 @@ +package com.provectus.kafka.ui.service.integration.odd.schema; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.confluent.kafka.schemaregistry.avro.AvroSchema; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.opendatadiscovery.client.model.DataSetField; +import org.opendatadiscovery.client.model.DataSetFieldType; +import org.opendatadiscovery.oddrn.model.KafkaPath; + +class AvroExtractorTest { + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void test(boolean isKey) { + var list = AvroExtractor.extract( + new AvroSchema(""" + { + "type": "record", + "name": "Message", + "namespace": "com.provectus.kafka", + "fields": + [ + { + "name": "f1", + "type": + { + "type": "array", + "items": + { + "type": "record", + "name": "ArrElement", + "fields": + [ + { + "name": "longmap", + "type": + { + "type": "map", + "values": "long" + } + } + ] + } + } + }, + { + "name": "f2", + "type": + { + "type": "record", + "name": "InnerMessage", + "fields": + [ + { + "name": "text", + "doc": "string field here", + "type": "string" + }, + { + "name": "innerMsgRef", + "type": "InnerMessage" + }, + { + "name": "nullable_union", + "type": + [ + "null", + "string", + "int" + ], + "default": null + }, + { + "name": "order_enum", + "type": + { + "type": "enum", + "name": "Suit", + "symbols": + [ + "SPADES", + "HEARTS" + ] + } + }, + { + "name": "str_list", + "type": + { + "type": "array", + "items": "string" + } + } + ] + } + } + ] + } + """), + + KafkaPath.builder() + .cluster("localhost:9092") + .topic("someTopic") + .build(), + isKey + ); + + String baseOddrn = "//kafka/cluster/localhost:9092/topics/someTopic/columns/" + (isKey ? "key" : "value"); + + assertThat(list).contains( + DataSetFieldsExtractors.rootField( + KafkaPath.builder().cluster("localhost:9092").topic("someTopic").build(), + isKey + ), + new DataSetField() + .name("f1") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/f1") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.LIST) + .logicalType("array") + .isNullable(false) + ), + new DataSetField() + .name("ArrElement") + .parentFieldOddrn(baseOddrn + "/f1") + .oddrn(baseOddrn + "/f1/items/ArrElement") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRUCT) + .logicalType("com.provectus.kafka.ArrElement") + .isNullable(false) + ), + new DataSetField() + .name("longmap") + .parentFieldOddrn(baseOddrn + "/f1/items/ArrElement") + .oddrn(baseOddrn + "/f1/items/ArrElement/fields/longmap") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.MAP) + .logicalType("map") + .isNullable(false) + ), + new DataSetField() + .name("key") + .parentFieldOddrn(baseOddrn + "/f1/items/ArrElement/fields/longmap") + .oddrn(baseOddrn + "/f1/items/ArrElement/fields/longmap/key") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRING) + .logicalType("string") + .isNullable(false) + ), + new DataSetField() + .name("value") + .parentFieldOddrn(baseOddrn + "/f1/items/ArrElement/fields/longmap") + .oddrn(baseOddrn + "/f1/items/ArrElement/fields/longmap/value") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.INTEGER) + .logicalType("long") + .isNullable(false) + ), + new DataSetField() + .name("f2") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/f2") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRUCT) + .logicalType("com.provectus.kafka.InnerMessage") + .isNullable(false) + ), + new DataSetField() + .name("text") + .parentFieldOddrn(baseOddrn + "/f2") + .oddrn(baseOddrn + "/f2/fields/text") + .description("string field here") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRING) + .logicalType("string") + .isNullable(false) + ), + new DataSetField() + .name("innerMsgRef") + .parentFieldOddrn(baseOddrn + "/f2") + .oddrn(baseOddrn + "/f2/fields/innerMsgRef") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRUCT) + .logicalType("com.provectus.kafka.InnerMessage") + .isNullable(false) + ), + new DataSetField() + .name("nullable_union") + .parentFieldOddrn(baseOddrn + "/f2") + .oddrn(baseOddrn + "/f2/fields/nullable_union") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.UNION) + .logicalType("union") + .isNullable(true) + ), + new DataSetField() + .name("string") + .parentFieldOddrn(baseOddrn + "/f2/fields/nullable_union") + .oddrn(baseOddrn + "/f2/fields/nullable_union/values/string") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRING) + .logicalType("string") + .isNullable(true) + ), + new DataSetField() + .name("int") + .parentFieldOddrn(baseOddrn + "/f2/fields/nullable_union") + .oddrn(baseOddrn + "/f2/fields/nullable_union/values/int") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.INTEGER) + .logicalType("int") + .isNullable(true) + ), + new DataSetField() + .name("int") + .parentFieldOddrn(baseOddrn + "/f2/fields/nullable_union") + .oddrn(baseOddrn + "/f2/fields/nullable_union/values/int") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.INTEGER) + .logicalType("int") + .isNullable(true) + ), + new DataSetField() + .name("order_enum") + .parentFieldOddrn(baseOddrn + "/f2") + .oddrn(baseOddrn + "/f2/fields/order_enum") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRING) + .logicalType("enum") + .isNullable(false) + ), + new DataSetField() + .name("str_list") + .parentFieldOddrn(baseOddrn + "/f2") + .oddrn(baseOddrn + "/f2/fields/str_list") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.LIST) + .logicalType("array") + .isNullable(false) + ), + new DataSetField() + .name("string") + .parentFieldOddrn(baseOddrn + "/f2/fields/str_list") + .oddrn(baseOddrn + "/f2/fields/str_list/items/string") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRING) + .logicalType("string") + .isNullable(false) + ) + ); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/JsonSchemaExtractorTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/JsonSchemaExtractorTest.java new file mode 100644 index 00000000000..30a1e6229cc --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/JsonSchemaExtractorTest.java @@ -0,0 +1,145 @@ +package com.provectus.kafka.ui.service.integration.odd.schema; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.confluent.kafka.schemaregistry.json.JsonSchema; +import java.net.URI; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.opendatadiscovery.client.model.DataSetField; +import org.opendatadiscovery.client.model.DataSetFieldType; +import org.opendatadiscovery.client.model.MetadataExtension; +import org.opendatadiscovery.oddrn.model.KafkaPath; + +class JsonSchemaExtractorTest { + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void test(boolean isKey) { + String jsonSchema = """ + { + "$id": "http://example.com/test.TestMsg", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": [ "int32_field" ], + "properties": + { + "int32_field": { "type": "integer", "title": "field title" }, + "lst_s_field": { "type": "array", "items": { "type": "string" }, "description": "field descr" }, + "untyped_struct_field": { "type": "object", "properties": {} }, + "union_field": { "type": [ "number", "object", "null" ] }, + "struct_field": { + "type": "object", + "properties": { + "bool_field": { "type": "boolean" } + } + } + } + } + """; + var fields = JsonSchemaExtractor.extract( + new JsonSchema(jsonSchema), + KafkaPath.builder() + .cluster("localhost:9092") + .topic("someTopic") + .build(), + isKey + ); + + String baseOddrn = "//kafka/cluster/localhost:9092/topics/someTopic/columns/" + (isKey ? "key" : "value"); + + assertThat(fields).contains( + DataSetFieldsExtractors.rootField( + KafkaPath.builder().cluster("localhost:9092").topic("someTopic").build(), + isKey + ), + new DataSetField() + .name("int32_field") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/int32_field") + .description("field title") + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.NUMBER) + .logicalType("Number") + .isNullable(false)), + new DataSetField() + .name("lst_s_field") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/lst_s_field") + .description("field descr") + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.LIST) + .logicalType("array") + .isNullable(true)), + new DataSetField() + .name("String") + .parentFieldOddrn(baseOddrn + "/lst_s_field") + .oddrn(baseOddrn + "/lst_s_field/items/String") + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRING) + .logicalType("String") + .isNullable(false)), + new DataSetField() + .name("untyped_struct_field") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/untyped_struct_field") + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRUCT) + .logicalType("Object") + .isNullable(true)), + new DataSetField() + .name("union_field") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/union_field/anyOf") + .metadata(List.of(new MetadataExtension() + .schemaUrl(URI.create("wontbeused.oops")) + .metadata(Map.of("criterion", "anyOf")))) + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.UNION) + .logicalType("anyOf") + .isNullable(true)), + new DataSetField() + .name("Number") + .parentFieldOddrn(baseOddrn + "/union_field/anyOf") + .oddrn(baseOddrn + "/union_field/anyOf/values/Number") + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.NUMBER) + .logicalType("Number") + .isNullable(true)), + new DataSetField() + .name("Object") + .parentFieldOddrn(baseOddrn + "/union_field/anyOf") + .oddrn(baseOddrn + "/union_field/anyOf/values/Object") + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRUCT) + .logicalType("Object") + .isNullable(true)), + new DataSetField() + .name("Null") + .parentFieldOddrn(baseOddrn + "/union_field/anyOf") + .oddrn(baseOddrn + "/union_field/anyOf/values/Null") + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.UNKNOWN) + .logicalType("Null") + .isNullable(true)), + new DataSetField() + .name("struct_field") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/struct_field") + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRUCT) + .logicalType("Object") + .isNullable(true)), + new DataSetField() + .name("bool_field") + .parentFieldOddrn(baseOddrn + "/struct_field") + .oddrn(baseOddrn + "/struct_field/fields/bool_field") + .type(new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.BOOLEAN) + .logicalType("Boolean") + .isNullable(true)) + ); + } +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/ProtoExtractorTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/ProtoExtractorTest.java new file mode 100644 index 00000000000..8d6344d7cca --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/ProtoExtractorTest.java @@ -0,0 +1,186 @@ +package com.provectus.kafka.ui.service.integration.odd.schema; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.opendatadiscovery.client.model.DataSetField; +import org.opendatadiscovery.client.model.DataSetFieldType; +import org.opendatadiscovery.oddrn.model.KafkaPath; + +class ProtoExtractorTest { + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void test(boolean isKey) { + String protoSchema = """ + syntax = "proto3"; + package test; + + import "google/protobuf/timestamp.proto"; + import "google/protobuf/duration.proto"; + import "google/protobuf/struct.proto"; + import "google/protobuf/wrappers.proto"; + + message TestMsg { + map mapField = 100; + int32 int32_field = 2; + bool bool_field = 3; + SampleEnum enum_field = 4; + + enum SampleEnum { + ENUM_V1 = 0; + ENUM_V2 = 1; + } + + google.protobuf.Timestamp ts_field = 5; + google.protobuf.Duration duration_field = 8; + + oneof some_oneof1 { + google.protobuf.Value one_of_v1 = 9; + google.protobuf.Value one_of_v2 = 10; + } + // wrapper field: + google.protobuf.Int64Value int64_w_field = 11; + + //embedded msg + EmbeddedMsg emb = 19; + + message EmbeddedMsg { + int32 emb_f1 = 1; + TestMsg outer_ref = 2; + } + }"""; + + var list = ProtoExtractor.extract( + new ProtobufSchema(protoSchema), + KafkaPath.builder() + .cluster("localhost:9092") + .topic("someTopic") + .build(), + isKey + ); + + String baseOddrn = "//kafka/cluster/localhost:9092/topics/someTopic/columns/" + (isKey ? "key" : "value"); + + assertThat(list) + .contains( + DataSetFieldsExtractors.rootField( + KafkaPath.builder().cluster("localhost:9092").topic("someTopic").build(), + isKey + ), + new DataSetField() + .name("mapField") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/mapField") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.LIST) + .logicalType("repeated") + .isNullable(true) + ), + new DataSetField() + .name("int32_field") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/int32_field") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.INTEGER) + .logicalType("int32") + .isNullable(true) + ), + new DataSetField() + .name("enum_field") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/enum_field") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRING) + .logicalType("enum") + .isNullable(true) + ), + new DataSetField() + .name("ts_field") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/ts_field") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.DATETIME) + .logicalType("google.protobuf.Timestamp") + .isNullable(true) + ), + new DataSetField() + .name("duration_field") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/duration_field") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.DURATION) + .logicalType("google.protobuf.Duration") + .isNullable(true) + ), + new DataSetField() + .name("one_of_v1") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/one_of_v1") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.UNKNOWN) + .logicalType("google.protobuf.Value") + .isNullable(true) + ), + new DataSetField() + .name("one_of_v2") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/one_of_v2") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.UNKNOWN) + .logicalType("google.protobuf.Value") + .isNullable(true) + ), + new DataSetField() + .name("int64_w_field") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/int64_w_field") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.INTEGER) + .logicalType("google.protobuf.Int64Value") + .isNullable(true) + ), + new DataSetField() + .name("emb") + .parentFieldOddrn(baseOddrn) + .oddrn(baseOddrn + "/emb") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRUCT) + .logicalType("test.TestMsg.EmbeddedMsg") + .isNullable(true) + ), + new DataSetField() + .name("emb_f1") + .parentFieldOddrn(baseOddrn + "/emb") + .oddrn(baseOddrn + "/emb/fields/emb_f1") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.INTEGER) + .logicalType("int32") + .isNullable(true) + ), + new DataSetField() + .name("outer_ref") + .parentFieldOddrn(baseOddrn + "/emb") + .oddrn(baseOddrn + "/emb/fields/outer_ref") + .type( + new DataSetFieldType() + .type(DataSetFieldType.TypeEnum.STRUCT) + .logicalType("test.TestMsg") + .isNullable(true) + ) + ); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlApiClientTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlApiClientTest.java index d09e32f948c..f266e07c6d5 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlApiClientTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlApiClientTest.java @@ -3,29 +3,22 @@ import static org.assertj.core.api.Assertions.assertThat; import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.DoubleNode; +import com.fasterxml.jackson.databind.node.DecimalNode; import com.fasterxml.jackson.databind.node.IntNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.TextNode; import com.provectus.kafka.ui.AbstractIntegrationTest; -import com.provectus.kafka.ui.container.KsqlDbContainer; -import com.provectus.kafka.ui.model.KafkaCluster; +import java.math.BigDecimal; import java.time.Duration; -import java.util.List; import java.util.Map; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.testcontainers.shaded.org.awaitility.Awaitility; -import org.testcontainers.utility.DockerImageName; import reactor.test.StepVerifier; class KsqlApiClientTest extends AbstractIntegrationTest { - private static final KsqlDbContainer KSQL_DB = new KsqlDbContainer( - DockerImageName.parse("confluentinc/ksqldb-server").withTag("0.24.0")) - .withKafka(kafka); - @BeforeAll static void startContainer() { KSQL_DB.start(); @@ -39,7 +32,7 @@ static void stopContainer() { // Tutorial is here: https://ksqldb.io/quickstart.html @Test void ksqTutorialQueriesWork() { - var client = new KsqlApiClient(KafkaCluster.builder().ksqldbServer(KSQL_DB.url()).build()); + var client = ksqlClient(); execCommandSync(client, "CREATE STREAM riderLocations (profileId VARCHAR, latitude DOUBLE, longitude DOUBLE) " + "WITH (kafka_topic='locations', value_format='json', partitions=1);", @@ -73,7 +66,7 @@ void ksqTutorialQueriesWork() { private void assertLastKsqTutorialQueryResult(KsqlApiClient client) { // expected results: //{"header":"Schema","columnNames":[...],"values":null} - //{"header":"Row","columnNames":null,"values":[[0.0,["4ab5cbad","8b6eae59","4a7c7b41"],3]]} + //{"header":"Row","columnNames":null,"values":[[0,["4ab5cbad","8b6eae59","4a7c7b41"],3]]} //{"header":"Row","columnNames":null,"values":[[10.0,["18f4ea86"],1]]} StepVerifier.create( client.execute( @@ -87,34 +80,26 @@ private void assertLastKsqTutorialQueryResult(KsqlApiClient client) { assertThat(header.getValues()).isNull(); }) .assertNext(row -> { - assertThat(row).isEqualTo( - KsqlApiClient.KsqlResponseTable.builder() - .header("Row") - .columnNames(null) - .values(List.of(List.of( - new DoubleNode(0.0), - new ArrayNode(JsonNodeFactory.instance) - .add(new TextNode("4ab5cbad")) - .add(new TextNode("8b6eae59")) - .add(new TextNode("4a7c7b41")), - new IntNode(3) - ))) - .build() - ); + var distance = (DecimalNode) row.getValues().get(0).get(0); + var riders = (ArrayNode) row.getValues().get(0).get(1); + var count = (IntNode) row.getValues().get(0).get(2); + + assertThat(distance).isEqualTo(new DecimalNode(new BigDecimal(0))); + assertThat(riders).isEqualTo(new ArrayNode(JsonNodeFactory.instance) + .add(new TextNode("4ab5cbad")) + .add(new TextNode("8b6eae59")) + .add(new TextNode("4a7c7b41"))); + assertThat(count).isEqualTo(new IntNode(3)); }) .assertNext(row -> { - assertThat(row).isEqualTo( - KsqlApiClient.KsqlResponseTable.builder() - .header("Row") - .columnNames(null) - .values(List.of(List.of( - new DoubleNode(10.0), - new ArrayNode(JsonNodeFactory.instance) - .add(new TextNode("18f4ea86")), - new IntNode(1) - ))) - .build() - ); + var distance = (DecimalNode) row.getValues().get(0).get(0); + var riders = (ArrayNode) row.getValues().get(0).get(1); + var count = (IntNode) row.getValues().get(0).get(2); + + assertThat(distance).isEqualTo(new DecimalNode(new BigDecimal(10))); + assertThat(riders).isEqualTo(new ArrayNode(JsonNodeFactory.instance) + .add(new TextNode("18f4ea86"))); + assertThat(count).isEqualTo(new IntNode(1)); }) .verifyComplete(); } @@ -125,5 +110,9 @@ private void execCommandSync(KsqlApiClient client, String... ksqls) { } } + private KsqlApiClient ksqlClient() { + return new KsqlApiClient(KSQL_DB.url(), null, null, null, null); + } + -} \ No newline at end of file +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2Test.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2Test.java index 22c87c9aecc..0e1717430ce 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2Test.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2Test.java @@ -3,24 +3,20 @@ import static org.assertj.core.api.Assertions.assertThat; import com.provectus.kafka.ui.AbstractIntegrationTest; -import com.provectus.kafka.ui.container.KsqlDbContainer; import com.provectus.kafka.ui.model.KafkaCluster; import com.provectus.kafka.ui.model.KsqlStreamDescriptionDTO; import com.provectus.kafka.ui.model.KsqlTableDescriptionDTO; +import com.provectus.kafka.ui.util.ReactiveFailover; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.testcontainers.utility.DockerImageName; class KsqlServiceV2Test extends AbstractIntegrationTest { - private static final KsqlDbContainer KSQL_DB = new KsqlDbContainer( - DockerImageName.parse("confluentinc/ksqldb-server").withTag("0.24.0")) - .withKafka(kafka); - private static final Set STREAMS_TO_DELETE = new CopyOnWriteArraySet<>(); private static final Set TABLES_TO_DELETE = new CopyOnWriteArraySet<>(); @@ -31,14 +27,12 @@ static void init() { @AfterAll static void cleanup() { - var client = new KsqlApiClient(KafkaCluster.builder().ksqldbServer(KSQL_DB.url()).build()); - TABLES_TO_DELETE.forEach(t -> - client.execute(String.format("DROP TABLE IF EXISTS %s DELETE TOPIC;", t), Map.of()) + ksqlClient().execute(String.format("DROP TABLE IF EXISTS %s DELETE TOPIC;", t), Map.of()) .blockLast()); STREAMS_TO_DELETE.forEach(s -> - client.execute(String.format("DROP STREAM IF EXISTS %s DELETE TOPIC;", s), Map.of()) + ksqlClient().execute(String.format("DROP STREAM IF EXISTS %s DELETE TOPIC;", s), Map.of()) .blockLast()); KSQL_DB.stop(); @@ -48,11 +42,10 @@ static void cleanup() { @Test void listStreamsReturnsAllKsqlStreams() { - var cluster = KafkaCluster.builder().ksqldbServer(KSQL_DB.url()).build(); var streamName = "stream_" + System.currentTimeMillis(); STREAMS_TO_DELETE.add(streamName); - new KsqlApiClient(cluster) + ksqlClient() .execute( String.format("CREATE STREAM %s ( " + " c1 BIGINT KEY, " @@ -65,7 +58,7 @@ void listStreamsReturnsAllKsqlStreams() { Map.of()) .blockLast(); - var streams = ksqlService.listStreams(cluster).collectList().block(); + var streams = ksqlService.listStreams(cluster()).collectList().block(); assertThat(streams).contains( new KsqlStreamDescriptionDTO() .name(streamName.toUpperCase()) @@ -77,11 +70,10 @@ void listStreamsReturnsAllKsqlStreams() { @Test void listTablesReturnsAllKsqlTables() { - var cluster = KafkaCluster.builder().ksqldbServer(KSQL_DB.url()).build(); var tableName = "table_" + System.currentTimeMillis(); TABLES_TO_DELETE.add(tableName); - new KsqlApiClient(cluster) + ksqlClient() .execute( String.format("CREATE TABLE %s ( " + " c1 BIGINT PRIMARY KEY, " @@ -94,7 +86,7 @@ void listTablesReturnsAllKsqlTables() { Map.of()) .blockLast(); - var tables = ksqlService.listTables(cluster).collectList().block(); + var tables = ksqlService.listTables(cluster()).collectList().block(); assertThat(tables).contains( new KsqlTableDescriptionDTO() .name(tableName.toUpperCase()) @@ -105,4 +97,15 @@ void listTablesReturnsAllKsqlTables() { ); } -} \ No newline at end of file + private static KafkaCluster cluster() { + return KafkaCluster.builder() + .ksqlClient(ReactiveFailover.create( + List.of(ksqlClient()), th -> true, "", ReactiveFailover.DEFAULT_RETRY_GRACE_PERIOD_MS)) + .build(); + } + + private static KsqlApiClient ksqlClient() { + return new KsqlApiClient(KSQL_DB.url(), null, null, null, null); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/response/ResponseParserTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/response/ResponseParserTest.java new file mode 100644 index 00000000000..02552449336 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/response/ResponseParserTest.java @@ -0,0 +1,25 @@ +package com.provectus.kafka.ui.service.ksql.response; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class ResponseParserTest { + + @Test + void parsesSelectHeaderIntoColumnNames() { + assertThat(ResponseParser.parseSelectHeadersString("`inQuotes` INT, notInQuotes INT")) + .containsExactly("`inQuotes` INT", "notInQuotes INT"); + + assertThat(ResponseParser.parseSelectHeadersString("`name with comma,` INT, name2 STRING")) + .containsExactly("`name with comma,` INT", "name2 STRING"); + + assertThat(ResponseParser.parseSelectHeadersString( + "`topLvl` INT, `struct` STRUCT<`nested1` STRING, anotherName STRUCT>")) + .containsExactly( + "`topLvl` INT", + "`struct` STRUCT<`nested1` STRING, anotherName STRUCT>" + ); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/DataMaskingTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/DataMaskingTest.java new file mode 100644 index 00000000000..4fb51af0a21 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/DataMaskingTest.java @@ -0,0 +1,89 @@ +package com.provectus.kafka.ui.service.masking; + +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.node.ContainerNode; +import com.provectus.kafka.ui.config.ClustersProperties; +import com.provectus.kafka.ui.serde.api.Serde; +import com.provectus.kafka.ui.service.masking.policies.MaskingPolicy; +import java.util.List; +import java.util.regex.Pattern; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class DataMaskingTest { + + private static final String TOPIC = "test_topic"; + + private DataMasking masking; + + private MaskingPolicy policy1; + private MaskingPolicy policy2; + private MaskingPolicy policy3; + + @BeforeEach + void init() { + policy1 = spy(createMaskPolicy()); + policy2 = spy(createMaskPolicy()); + policy3 = spy(createMaskPolicy()); + + masking = new DataMasking( + List.of( + new DataMasking.Mask(Pattern.compile(TOPIC), null, policy1), + new DataMasking.Mask(null, Pattern.compile(TOPIC), policy2), + new DataMasking.Mask(null, Pattern.compile(TOPIC + "|otherTopic"), policy3))); + } + + private MaskingPolicy createMaskPolicy() { + var props = new ClustersProperties.Masking(); + props.setType(ClustersProperties.Masking.Type.REMOVE); + return MaskingPolicy.create(props); + } + + @ParameterizedTest + @ValueSource(strings = { + "{\"some\": \"json\"}", + "[ {\"json\": \"array\"} ]" + }) + @SneakyThrows + void appliesMasksToJsonContainerArgsBasedOnTopicPatterns(String jsonObjOrArr) { + var parsedJson = (ContainerNode) new JsonMapper().readTree(jsonObjOrArr); + + masking.getMaskingFunction(TOPIC, Serde.Target.KEY).apply(jsonObjOrArr); + verify(policy1).applyToJsonContainer(eq(parsedJson)); + verifyNoInteractions(policy2, policy3); + + reset(policy1, policy2, policy3); + + masking.getMaskingFunction(TOPIC, Serde.Target.VALUE).apply(jsonObjOrArr); + verify(policy2).applyToJsonContainer(eq(parsedJson)); + verify(policy3).applyToJsonContainer(eq(policy2.applyToJsonContainer(parsedJson))); + verifyNoInteractions(policy1); + } + + @ParameterizedTest + @ValueSource(strings = { + "non json str", + "234", + "null" + }) + void appliesFirstFoundMaskToStringArgsBasedOnTopicPatterns(String nonJsonObjOrArrString) { + masking.getMaskingFunction(TOPIC, Serde.Target.KEY).apply(nonJsonObjOrArrString); + verify(policy1).applyToString(eq(nonJsonObjOrArrString)); + verifyNoInteractions(policy2, policy3); + + reset(policy1, policy2, policy3); + + masking.getMaskingFunction(TOPIC, Serde.Target.VALUE).apply(nonJsonObjOrArrString); + verify(policy2).applyToString(eq(nonJsonObjOrArrString)); + verifyNoInteractions(policy1, policy3); + } + +} \ No newline at end of file diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/FieldsSelectorTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/FieldsSelectorTest.java new file mode 100644 index 00000000000..497a9365d75 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/FieldsSelectorTest.java @@ -0,0 +1,53 @@ +package com.provectus.kafka.ui.service.masking.policies; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.provectus.kafka.ui.config.ClustersProperties; +import com.provectus.kafka.ui.exception.ValidationException; +import java.util.List; +import org.junit.jupiter.api.Test; + +class FieldsSelectorTest { + + @Test + void selectsFieldsDueToProvidedPattern() { + var properties = new ClustersProperties.Masking(); + properties.setFieldsNamePattern("f1|f2"); + + var selector = FieldsSelector.create(properties); + assertThat(selector.shouldBeMasked("f1")).isTrue(); + assertThat(selector.shouldBeMasked("f2")).isTrue(); + assertThat(selector.shouldBeMasked("doesNotMatchPattern")).isFalse(); + } + + @Test + void selectsFieldsDueToProvidedFieldNames() { + var properties = new ClustersProperties.Masking(); + properties.setFields(List.of("f1", "f2")); + + var selector = FieldsSelector.create(properties); + assertThat(selector.shouldBeMasked("f1")).isTrue(); + assertThat(selector.shouldBeMasked("f2")).isTrue(); + assertThat(selector.shouldBeMasked("notInAList")).isFalse(); + } + + @Test + void selectAllFieldsIfNoPatternAndNoNamesProvided() { + var properties = new ClustersProperties.Masking(); + + var selector = FieldsSelector.create(properties); + assertThat(selector.shouldBeMasked("anyPropertyName")).isTrue(); + } + + @Test + void throwsExceptionIfBothFieldListAndPatternProvided() { + var properties = new ClustersProperties.Masking(); + properties.setFieldsNamePattern("f1|f2"); + properties.setFields(List.of("f3", "f4")); + + assertThatThrownBy(() -> FieldsSelector.create(properties)) + .isInstanceOf(ValidationException.class); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/MaskTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/MaskTest.java new file mode 100644 index 00000000000..b33a26f3000 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/MaskTest.java @@ -0,0 +1,68 @@ +package com.provectus.kafka.ui.service.masking.policies; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.node.ContainerNode; +import java.util.List; +import java.util.stream.Stream; +import lombok.SneakyThrows; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +class MaskTest { + + private static final FieldsSelector FIELDS_SELECTOR = fieldName -> List.of("id", "name").contains(fieldName); + private static final List PATTERN = List.of("X", "x", "n", "-"); + + @ParameterizedTest + @MethodSource + void testApplyToJsonContainer(FieldsSelector selector, ContainerNode original, ContainerNode expected) { + Mask policy = new Mask(selector, PATTERN); + assertThat(policy.applyToJsonContainer(original)).isEqualTo(expected); + } + + private static Stream testApplyToJsonContainer() { + return Stream.of( + Arguments.of( + FIELDS_SELECTOR, + parse("{ \"id\": 123, \"name\": { \"first\": \"James\", \"surname\": \"Bond777!\"}}"), + parse("{ \"id\": \"nnn\", \"name\": { \"first\": \"Xxxxx\", \"surname\": \"Xxxxnnn-\"}}") + ), + Arguments.of( + FIELDS_SELECTOR, + parse("[{ \"id\": 123, \"f2\": 234}, { \"name\": \"1.2\", \"f2\": 345} ]"), + parse("[{ \"id\": \"nnn\", \"f2\": 234}, { \"name\": \"n-n\", \"f2\": 345} ]") + ), + Arguments.of( + FIELDS_SELECTOR, + parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"Bond777!\"}}"), + parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"Xxxxnnn-\"}}") + ), + Arguments.of( + (FieldsSelector) (fieldName -> true), + parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"Bond777!\"}}"), + parse("{ \"outer\": { \"f1\": \"Xxxxx\", \"name\": \"Xxxxnnn-\"}}") + ) + ); + } + + @ParameterizedTest + @CsvSource({ + "Some string?!1, Xxxx xxxxxx--n", + "1.24343, n-nnnnn", + "null, xxxx" + }) + void testApplyToString(String original, String expected) { + Mask policy = new Mask(fieldName -> true, PATTERN); + assertThat(policy.applyToString(original)).isEqualTo(expected); + } + + @SneakyThrows + private static JsonNode parse(String str) { + return new JsonMapper().readTree(str); + } +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/RemoveTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/RemoveTest.java new file mode 100644 index 00000000000..9393ea1c626 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/RemoveTest.java @@ -0,0 +1,72 @@ +package com.provectus.kafka.ui.service.masking.policies; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.node.ContainerNode; +import java.util.List; +import java.util.stream.Stream; +import lombok.SneakyThrows; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +class RemoveTest { + + private static final FieldsSelector FIELDS_SELECTOR = fieldName -> List.of("id", "name").contains(fieldName); + + @ParameterizedTest + @MethodSource + void testApplyToJsonContainer(FieldsSelector fieldsSelector, ContainerNode original, ContainerNode expected) { + var policy = new Remove(fieldsSelector); + assertThat(policy.applyToJsonContainer(original)).isEqualTo(expected); + } + + private static Stream testApplyToJsonContainer() { + return Stream.of( + Arguments.of( + FIELDS_SELECTOR, + parse("{ \"id\": 123, \"name\": { \"first\": \"James\", \"surname\": \"Bond777!\"}}"), + parse("{}") + ), + Arguments.of( + FIELDS_SELECTOR, + parse("[{ \"id\": 123, \"f2\": 234}, { \"name\": \"1.2\", \"f2\": 345} ]"), + parse("[{ \"f2\": 234}, { \"f2\": 345} ]") + ), + Arguments.of( + FIELDS_SELECTOR, + parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"Bond777!\"}}"), + parse("{ \"outer\": { \"f1\": \"James\"}}") + ), + Arguments.of( + (FieldsSelector) (fieldName -> true), + parse("{ \"outer\": { \"f1\": \"v1\", \"f2\": \"v2\", \"inner\" : {\"if1\": \"iv1\"}}}"), + parse("{}") + ), + Arguments.of( + (FieldsSelector) (fieldName -> true), + parse("[{ \"f1\": 123}, { \"f2\": \"1.2\"} ]"), + parse("[{}, {}]") + ) + ); + } + + @SneakyThrows + private static JsonNode parse(String str) { + return new JsonMapper().readTree(str); + } + + @ParameterizedTest + @CsvSource({ + "Some string?!1, null", + "1.24343, null", + "null, null" + }) + void testApplyToString(String original, String expected) { + var policy = new Remove(fieldName -> true); + assertThat(policy.applyToString(original)).isEqualTo(expected); + } +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/ReplaceTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/ReplaceTest.java new file mode 100644 index 00000000000..9f2fcd90c4c --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/ReplaceTest.java @@ -0,0 +1,68 @@ +package com.provectus.kafka.ui.service.masking.policies; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.node.ContainerNode; +import java.util.List; +import java.util.stream.Stream; +import lombok.SneakyThrows; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +class ReplaceTest { + + private static final FieldsSelector FIELDS_SELECTOR = fieldName -> List.of("id", "name").contains(fieldName); + private static final String REPLACEMENT_STRING = "***"; + + @ParameterizedTest + @MethodSource + void testApplyToJsonContainer(FieldsSelector fieldsSelector, ContainerNode original, ContainerNode expected) { + var policy = new Replace(fieldsSelector, REPLACEMENT_STRING); + assertThat(policy.applyToJsonContainer(original)).isEqualTo(expected); + } + + private static Stream testApplyToJsonContainer() { + return Stream.of( + Arguments.of( + FIELDS_SELECTOR, + parse("{ \"id\": 123, \"name\": { \"first\": \"James\", \"surname\": \"Bond777!\"}}"), + parse("{ \"id\": \"***\", \"name\": { \"first\": \"***\", \"surname\": \"***\"}}") + ), + Arguments.of( + FIELDS_SELECTOR, + parse("[{ \"id\": 123, \"f2\": 234}, { \"name\": \"1.2\", \"f2\": 345} ]"), + parse("[{ \"id\": \"***\", \"f2\": 234}, { \"name\": \"***\", \"f2\": 345} ]") + ), + Arguments.of( + FIELDS_SELECTOR, + parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"Bond777!\"}}"), + parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"***\"}}") + ), + Arguments.of( + (FieldsSelector) (fieldName -> true), + parse("{ \"outer\": { \"f1\": \"v1\", \"f2\": \"v2\", \"inner\" : {\"if1\": \"iv1\"}}}"), + parse("{ \"outer\": { \"f1\": \"***\", \"f2\": \"***\", \"inner\" : {\"if1\": \"***\"}}}}") + ) + ); + } + + @SneakyThrows + private static JsonNode parse(String str) { + return new JsonMapper().readTree(str); + } + + @ParameterizedTest + @CsvSource({ + "Some string?!1, ***", + "1.24343, ***", + "null, ***" + }) + void testApplyToString(String original, String expected) { + var policy = new Replace(fieldName -> true, REPLACEMENT_STRING); + assertThat(policy.applyToString(original)).isEqualTo(expected); + } +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/metrics/JmxMetricsFormatterTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/metrics/JmxMetricsFormatterTest.java new file mode 100644 index 00000000000..1a4ff5134e6 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/metrics/JmxMetricsFormatterTest.java @@ -0,0 +1,77 @@ +package com.provectus.kafka.ui.service.metrics; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import javax.management.MBeanAttributeInfo; +import javax.management.ObjectName; +import org.assertj.core.data.Offset; +import org.junit.jupiter.api.Test; + +class JmxMetricsFormatterTest { + + /** + * Original format is here. + */ + @Test + void convertsJmxMetricsAccordingToJmxExporterFormat() throws Exception { + List metrics = JmxMetricsFormatter.constructMetricsList( + new ObjectName( + "kafka.server:type=Some.BrokerTopic-Metrics,name=BytesOutPer-Sec,topic=test,some-lbl=123"), + new MBeanAttributeInfo[] { + createMbeanInfo("FifteenMinuteRate"), + createMbeanInfo("Mean"), + createMbeanInfo("Calls-count"), + createMbeanInfo("SkipValue"), + }, + new Object[] { + 123.0, + 100.0, + 10L, + "string values not supported" + } + ); + + assertThat(metrics).hasSize(3); + + assertMetricsEqual( + RawMetric.create( + "kafka_server_Some_BrokerTopic_Metrics_FifteenMinuteRate", + Map.of("name", "BytesOutPer-Sec", "topic", "test", "some_lbl", "123"), + BigDecimal.valueOf(123.0) + ), + metrics.get(0) + ); + + assertMetricsEqual( + RawMetric.create( + "kafka_server_Some_BrokerTopic_Metrics_Mean", + Map.of("name", "BytesOutPer-Sec", "topic", "test", "some_lbl", "123"), + BigDecimal.valueOf(100.0) + ), + metrics.get(1) + ); + + assertMetricsEqual( + RawMetric.create( + "kafka_server_Some_BrokerTopic_Metrics_Calls_count", + Map.of("name", "BytesOutPer-Sec", "topic", "test", "some_lbl", "123"), + BigDecimal.valueOf(10) + ), + metrics.get(2) + ); + } + + private static MBeanAttributeInfo createMbeanInfo(String name) { + return new MBeanAttributeInfo(name, "sometype-notused", null, true, true, false, null); + } + + private void assertMetricsEqual(RawMetric expected, RawMetric actual) { + assertThat(actual.name()).isEqualTo(expected.name()); + assertThat(actual.labels()).isEqualTo(expected.labels()); + assertThat(actual.value()).isCloseTo(expected.value(), Offset.offset(new BigDecimal("0.001"))); + } + +} \ No newline at end of file diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/metrics/PrometheusEndpointMetricsParserTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/metrics/PrometheusEndpointMetricsParserTest.java new file mode 100644 index 00000000000..294215c8b18 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/metrics/PrometheusEndpointMetricsParserTest.java @@ -0,0 +1,30 @@ +package com.provectus.kafka.ui.service.metrics; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.Test; + +class PrometheusEndpointMetricsParserTest { + + @Test + void test() { + String metricsString = + "kafka_server_BrokerTopicMetrics_FifteenMinuteRate" + + "{name=\"BytesOutPerSec\",topic=\"__confluent.support.metrics\",} 123.1234"; + + Optional parsedOpt = PrometheusEndpointMetricsParser.parse(metricsString); + + assertThat(parsedOpt).hasValueSatisfying(metric -> { + assertThat(metric.name()).isEqualTo("kafka_server_BrokerTopicMetrics_FifteenMinuteRate"); + assertThat(metric.value()).isEqualTo("123.1234"); + assertThat(metric.labels()).containsExactlyEntriesOf( + Map.of( + "name", "BytesOutPerSec", + "topic", "__confluent.support.metrics" + )); + }); + } + +} \ No newline at end of file diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/metrics/PrometheusMetricsRetrieverTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/metrics/PrometheusMetricsRetrieverTest.java new file mode 100644 index 00000000000..9cc0494039d --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/metrics/PrometheusMetricsRetrieverTest.java @@ -0,0 +1,97 @@ +package com.provectus.kafka.ui.service.metrics; + +import com.provectus.kafka.ui.model.MetricsConfig; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.test.StepVerifier; + +class PrometheusMetricsRetrieverTest { + + private final PrometheusMetricsRetriever retriever = new PrometheusMetricsRetriever(); + + private final MockWebServer mockWebServer = new MockWebServer(); + + @BeforeEach + void startMockServer() throws IOException { + mockWebServer.start(); + } + + @AfterEach + void stopMockServer() throws IOException { + mockWebServer.close(); + } + + @Test + void callsMetricsEndpointAndConvertsResponceToRawMetric() { + var url = mockWebServer.url("/metrics"); + mockWebServer.enqueue(prepareResponse()); + + MetricsConfig metricsConfig = prepareMetricsConfig(url.port(), null, null); + + StepVerifier.create(retriever.retrieve(WebClient.create(), url.host(), metricsConfig)) + .expectNextSequence(expectedRawMetrics()) + // third metric should not be present, since it has "NaN" value + .verifyComplete(); + } + + @Test + void callsSecureMetricsEndpointAndConvertsResponceToRawMetric() { + var url = mockWebServer.url("/metrics"); + mockWebServer.enqueue(prepareResponse()); + + + MetricsConfig metricsConfig = prepareMetricsConfig(url.port(), "username", "password"); + + StepVerifier.create(retriever.retrieve(WebClient.create(), url.host(), metricsConfig)) + .expectNextSequence(expectedRawMetrics()) + // third metric should not be present, since it has "NaN" value + .verifyComplete(); + } + + MockResponse prepareResponse() { + // body copied from real jmx exporter + return new MockResponse().setBody( + "# HELP kafka_server_KafkaRequestHandlerPool_FifteenMinuteRate Attribute exposed for management \n" + + "# TYPE kafka_server_KafkaRequestHandlerPool_FifteenMinuteRate untyped\n" + + "kafka_server_KafkaRequestHandlerPool_FifteenMinuteRate{name=\"RequestHandlerAvgIdlePercent\",} 0.898\n" + + "# HELP kafka_server_socket_server_metrics_request_size_avg The average size of requests sent. \n" + + "# TYPE kafka_server_socket_server_metrics_request_size_avg untyped\n" + + "kafka_server_socket_server_metrics_request_size_avg{listener=\"PLAIN\",networkProcessor=\"1\",} 101.1\n" + + "kafka_server_socket_server_metrics_request_size_avg{listener=\"PLAIN2\",networkProcessor=\"5\",} NaN" + ); + } + + MetricsConfig prepareMetricsConfig(Integer port, String username, String password) { + return MetricsConfig.builder() + .ssl(false) + .port(port) + .type(MetricsConfig.PROMETHEUS_METRICS_TYPE) + .username(username) + .password(password) + .build(); + } + + List expectedRawMetrics() { + + var firstMetric = RawMetric.create( + "kafka_server_KafkaRequestHandlerPool_FifteenMinuteRate", + Map.of("name", "RequestHandlerAvgIdlePercent"), + new BigDecimal("0.898") + ); + + var secondMetric = RawMetric.create( + "kafka_server_socket_server_metrics_request_size_avg", + Map.of("listener", "PLAIN", "networkProcessor", "1"), + new BigDecimal("101.1") + ); + return List.of(firstMetric, secondMetric); + } +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/metrics/WellKnownMetricsTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/metrics/WellKnownMetricsTest.java new file mode 100644 index 00000000000..c1c9c04058a --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/metrics/WellKnownMetricsTest.java @@ -0,0 +1,93 @@ +package com.provectus.kafka.ui.service.metrics; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.provectus.kafka.ui.model.Metrics; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import org.apache.kafka.common.Node; +import org.junit.jupiter.api.Test; + +class WellKnownMetricsTest { + + private final WellKnownMetrics wellKnownMetrics = new WellKnownMetrics(); + + @Test + void bytesIoTopicMetricsPopulated() { + populateWith( + new Node(0, "host", 123), + "kafka_server_BrokerTopicMetrics_FifteenMinuteRate{name=\"BytesInPerSec\",topic=\"test-topic\",} 1.0", + "kafka_server_BrokerTopicMetrics_FifteenMinuteRate{name=\"BytesOutPerSec\",topic=\"test-topic\",} 2.0", + "kafka_server_brokertopicmetrics_fifteenminuterate{name=\"bytesinpersec\",topic=\"test-topic\",} 1.0", + "kafka_server_brokertopicmetrics_fifteenminuterate{name=\"bytesoutpersec\",topic=\"test-topic\",} 2.0", + "some_unknown_prefix_brokertopicmetrics_fifteenminuterate{name=\"bytesinpersec\",topic=\"test-topic\",} 1.0", + "some_unknown_prefix_brokertopicmetrics_fifteenminuterate{name=\"bytesoutpersec\",topic=\"test-topic\",} 2.0" + ); + assertThat(wellKnownMetrics.bytesInFifteenMinuteRate) + .containsEntry("test-topic", new BigDecimal("3.0")); + assertThat(wellKnownMetrics.bytesOutFifteenMinuteRate) + .containsEntry("test-topic", new BigDecimal("6.0")); + } + + @Test + void bytesIoBrokerMetricsPopulated() { + populateWith( + new Node(1, "host1", 123), + "kafka_server_BrokerTopicMetrics_FifteenMinuteRate{name=\"BytesInPerSec\",} 1.0", + "kafka_server_BrokerTopicMetrics_FifteenMinuteRate{name=\"BytesOutPerSec\",} 2.0" + ); + populateWith( + new Node(2, "host2", 345), + "some_unknown_prefix_brokertopicmetrics_fifteenminuterate{name=\"bytesinpersec\",} 10.0", + "some_unknown_prefix_brokertopicmetrics_fifteenminuterate{name=\"bytesoutpersec\",} 20.0" + ); + + assertThat(wellKnownMetrics.brokerBytesInFifteenMinuteRate) + .hasSize(2) + .containsEntry(1, new BigDecimal("1.0")) + .containsEntry(2, new BigDecimal("10.0")); + + assertThat(wellKnownMetrics.brokerBytesOutFifteenMinuteRate) + .hasSize(2) + .containsEntry(1, new BigDecimal("2.0")) + .containsEntry(2, new BigDecimal("20.0")); + } + + @Test + void appliesInnerStateToMetricsBuilder() { + //filling per topic io rates + wellKnownMetrics.bytesInFifteenMinuteRate.put("topic", new BigDecimal(1)); + wellKnownMetrics.bytesOutFifteenMinuteRate.put("topic", new BigDecimal(2)); + + //filling per broker io rates + wellKnownMetrics.brokerBytesInFifteenMinuteRate.put(1, new BigDecimal(1)); + wellKnownMetrics.brokerBytesOutFifteenMinuteRate.put(1, new BigDecimal(2)); + wellKnownMetrics.brokerBytesInFifteenMinuteRate.put(2, new BigDecimal(10)); + wellKnownMetrics.brokerBytesOutFifteenMinuteRate.put(2, new BigDecimal(20)); + + Metrics.MetricsBuilder builder = Metrics.builder(); + wellKnownMetrics.apply(builder); + var metrics = builder.build(); + + // checking per topic io rates + assertThat(metrics.getTopicBytesInPerSec()).containsExactlyEntriesOf(wellKnownMetrics.bytesInFifteenMinuteRate); + assertThat(metrics.getTopicBytesOutPerSec()).containsExactlyEntriesOf(wellKnownMetrics.bytesOutFifteenMinuteRate); + + // checking per broker io rates + assertThat(metrics.getBrokerBytesInPerSec()).containsExactlyInAnyOrderEntriesOf( + Map.of(1, new BigDecimal(1), 2, new BigDecimal(10))); + assertThat(metrics.getBrokerBytesOutPerSec()).containsExactlyInAnyOrderEntriesOf( + Map.of(1, new BigDecimal(2), 2, new BigDecimal(20))); + } + + private void populateWith(Node n, String... prometheusMetric) { + Arrays.stream(prometheusMetric) + .map(PrometheusEndpointMetricsParser::parse) + .filter(Optional::isPresent) + .map(Optional::get) + .forEach(m -> wellKnownMetrics.populate(n, m)); + } + +} \ No newline at end of file diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/CreateStrategyTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/CreateStrategyTest.java deleted file mode 100644 index 257fb36d35d..00000000000 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/CreateStrategyTest.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.provectus.kafka.ui.strategy.ksql.statement; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.provectus.kafka.ui.exception.UnprocessableEntityException; -import com.provectus.kafka.ui.model.KsqlCommandResponseDTO; -import lombok.SneakyThrows; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class CreateStrategyTest { - private final ObjectMapper mapper = new ObjectMapper(); - private CreateStrategy strategy; - - @BeforeEach - void setUp() { - strategy = new CreateStrategy(); - } - - @Test - void shouldReturnUri() { - strategy.host("ksqldb-server:8088"); - assertThat(strategy.getUri()).isEqualTo("ksqldb-server:8088/ksql"); - } - - @Test - void shouldReturnTrueInTest() { - assertTrue(strategy.test("CREATE STREAM stream WITH (KAFKA_TOPIC='topic');")); - assertTrue(strategy.test("CREATE STREAM stream" - + " AS SELECT users.id AS userid FROM users EMIT CHANGES;" - )); - assertTrue(strategy.test( - "CREATE TABLE table (id VARCHAR) WITH (KAFKA_TOPIC='table');" - )); - assertTrue(strategy.test( - "CREATE TABLE pageviews_regions WITH (KEY_FORMAT='JSON')" - + " AS SELECT gender, COUNT(*) AS numbers" - + " FROM pageviews EMIT CHANGES;" - )); - } - - @Test - void shouldReturnFalseInTest() { - assertFalse(strategy.test("show streams;")); - assertFalse(strategy.test("show tables;")); - assertFalse(strategy.test("CREATE TABLE test;")); - assertFalse(strategy.test("CREATE STREAM test;")); - } - - @Test - void shouldSerializeResponse() { - String message = "updated successful"; - JsonNode node = getResponseWithMessage(message); - KsqlCommandResponseDTO serializedResponse = strategy.serializeResponse(node); - assertThat(serializedResponse.getMessage()).isEqualTo(message); - - } - - @Test - void shouldSerializeWithException() { - JsonNode commandStatusNode = mapper.createObjectNode().put("commandStatus", "nodeWithMessage"); - JsonNode node = mapper.createArrayNode().add(mapper.valueToTree(commandStatusNode)); - Exception exception = assertThrows( - UnprocessableEntityException.class, - () -> strategy.serializeResponse(node) - ); - - assertThat(exception.getMessage()).isEqualTo("KSQL DB response mapping error"); - } - - @SneakyThrows - private JsonNode getResponseWithMessage(String message) { - JsonNode nodeWithMessage = mapper.createObjectNode().put("message", message); - JsonNode commandStatusNode = mapper.createObjectNode().set("commandStatus", nodeWithMessage); - return mapper.createArrayNode().add(mapper.valueToTree(commandStatusNode)); - } -} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/DescribeStrategyTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/DescribeStrategyTest.java deleted file mode 100644 index 51cb0c742a4..00000000000 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/DescribeStrategyTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.provectus.kafka.ui.strategy.ksql.statement; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.provectus.kafka.ui.exception.UnprocessableEntityException; -import com.provectus.kafka.ui.model.KsqlCommandResponseDTO; -import com.provectus.kafka.ui.model.TableDTO; -import java.util.List; -import lombok.SneakyThrows; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class DescribeStrategyTest { - private final ObjectMapper mapper = new ObjectMapper(); - private DescribeStrategy strategy; - - @BeforeEach - void setUp() { - strategy = new DescribeStrategy(); - } - - @Test - void shouldReturnUri() { - strategy.host("ksqldb-server:8088"); - assertThat(strategy.getUri()).isEqualTo("ksqldb-server:8088/ksql"); - } - - @Test - void shouldReturnTrueInTest() { - assertTrue(strategy.test("DESCRIBE users;")); - assertTrue(strategy.test("DESCRIBE EXTENDED users;")); - } - - @Test - void shouldReturnFalseInTest() { - assertFalse(strategy.test("list streams;")); - assertFalse(strategy.test("show tables;")); - } - - @Test - void shouldSerializeResponse() { - JsonNode node = getResponseWithObjectNode(); - KsqlCommandResponseDTO serializedResponse = strategy.serializeResponse(node); - TableDTO table = serializedResponse.getData(); - assertThat(table.getHeaders()).isEqualTo(List.of("key", "value")); - assertThat(table.getRows()).isEqualTo(List.of(List.of("name", "kafka"))); - } - - @Test - void shouldSerializeWithException() { - JsonNode sourceDescriptionNode = - mapper.createObjectNode().put("sourceDescription", "nodeWithMessage"); - JsonNode node = mapper.createArrayNode().add(mapper.valueToTree(sourceDescriptionNode)); - Exception exception = assertThrows( - UnprocessableEntityException.class, - () -> strategy.serializeResponse(node) - ); - - assertThat(exception.getMessage()).isEqualTo("KSQL DB response mapping error"); - } - - @SneakyThrows - private JsonNode getResponseWithObjectNode() { - JsonNode nodeWithMessage = mapper.createObjectNode().put("name", "kafka"); - JsonNode nodeWithResponse = mapper.createObjectNode().set("sourceDescription", nodeWithMessage); - return mapper.createArrayNode().add(mapper.valueToTree(nodeWithResponse)); - } -} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/DropStrategyTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/DropStrategyTest.java deleted file mode 100644 index 5f2b8fcc844..00000000000 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/DropStrategyTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.provectus.kafka.ui.strategy.ksql.statement; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.provectus.kafka.ui.exception.UnprocessableEntityException; -import com.provectus.kafka.ui.model.KsqlCommandResponseDTO; -import lombok.SneakyThrows; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class DropStrategyTest { - private final ObjectMapper mapper = new ObjectMapper(); - private DropStrategy strategy; - - @BeforeEach - void setUp() { - strategy = new DropStrategy(); - } - - @Test - void shouldReturnUri() { - strategy.host("ksqldb-server:8088"); - assertThat(strategy.getUri()).isEqualTo("ksqldb-server:8088/ksql"); - } - - @Test - void shouldReturnTrueInTest() { - assertTrue(strategy.test("drop table table1;")); - assertTrue(strategy.test("drop stream stream2;")); - } - - @Test - void shouldReturnFalseInTest() { - assertFalse(strategy.test("show streams;")); - assertFalse(strategy.test("show tables;")); - assertFalse(strategy.test("create table test;")); - assertFalse(strategy.test("create stream test;")); - } - - @Test - void shouldSerializeResponse() { - String message = "updated successful"; - JsonNode node = getResponseWithMessage(message); - KsqlCommandResponseDTO serializedResponse = strategy.serializeResponse(node); - assertThat(serializedResponse.getMessage()).isEqualTo(message); - - } - - @Test - void shouldSerializeWithException() { - JsonNode commandStatusNode = mapper.createObjectNode().put("commandStatus", "nodeWithMessage"); - JsonNode node = mapper.createArrayNode().add(mapper.valueToTree(commandStatusNode)); - Exception exception = assertThrows( - UnprocessableEntityException.class, - () -> strategy.serializeResponse(node) - ); - - assertThat(exception.getMessage()).isEqualTo("KSQL DB response mapping error"); - } - - @SneakyThrows - private JsonNode getResponseWithMessage(String message) { - JsonNode nodeWithMessage = mapper.createObjectNode().put("message", message); - JsonNode commandStatusNode = mapper.createObjectNode().set("commandStatus", nodeWithMessage); - return mapper.createArrayNode().add(mapper.valueToTree(commandStatusNode)); - } -} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/ExplainStrategyTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/ExplainStrategyTest.java deleted file mode 100644 index 2582abedbf7..00000000000 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/ExplainStrategyTest.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.provectus.kafka.ui.strategy.ksql.statement; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.provectus.kafka.ui.exception.UnprocessableEntityException; -import com.provectus.kafka.ui.model.KsqlCommandResponseDTO; -import com.provectus.kafka.ui.model.TableDTO; -import java.util.List; -import lombok.SneakyThrows; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class ExplainStrategyTest { - private final ObjectMapper mapper = new ObjectMapper(); - private ExplainStrategy strategy; - - @BeforeEach - void setUp() { - strategy = new ExplainStrategy(); - } - - @Test - void shouldReturnUri() { - strategy.host("ksqldb-server:8088"); - assertThat(strategy.getUri()).isEqualTo("ksqldb-server:8088/ksql"); - } - - @Test - void shouldReturnTrueInTest() { - assertTrue(strategy.test("explain users_query_id;")); - } - - @Test - void shouldReturnFalseInTest() { - assertFalse(strategy.test("show queries;")); - } - - @Test - void shouldSerializeResponse() { - JsonNode node = getResponseWithObjectNode(); - KsqlCommandResponseDTO serializedResponse = strategy.serializeResponse(node); - TableDTO table = serializedResponse.getData(); - assertThat(table.getHeaders()).isEqualTo(List.of("key", "value")); - assertThat(table.getRows()).isEqualTo(List.of(List.of("name", "kafka"))); - } - - @Test - void shouldSerializeWithException() { - JsonNode sourceDescriptionNode = - mapper.createObjectNode().put("sourceDescription", "nodeWithMessage"); - JsonNode node = mapper.createArrayNode().add(mapper.valueToTree(sourceDescriptionNode)); - Exception exception = assertThrows( - UnprocessableEntityException.class, - () -> strategy.serializeResponse(node) - ); - - assertThat(exception.getMessage()).isEqualTo("KSQL DB response mapping error"); - } - - @SneakyThrows - private JsonNode getResponseWithObjectNode() { - JsonNode nodeWithMessage = mapper.createObjectNode().put("name", "kafka"); - JsonNode nodeWithResponse = mapper.createObjectNode().set("queryDescription", nodeWithMessage); - return mapper.createArrayNode().add(mapper.valueToTree(nodeWithResponse)); - } -} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/SelectStrategyTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/SelectStrategyTest.java deleted file mode 100644 index efeb87d5847..00000000000 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/SelectStrategyTest.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.provectus.kafka.ui.strategy.ksql.statement; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.provectus.kafka.ui.exception.UnprocessableEntityException; -import com.provectus.kafka.ui.model.KsqlCommandResponseDTO; -import com.provectus.kafka.ui.model.TableDTO; -import java.util.List; -import lombok.SneakyThrows; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class SelectStrategyTest { - private final ObjectMapper mapper = new ObjectMapper(); - private SelectStrategy strategy; - - @BeforeEach - void setUp() { - strategy = new SelectStrategy(); - } - - @Test - void shouldReturnUri() { - strategy.host("ksqldb-server:8088"); - assertThat(strategy.getUri()).isEqualTo("ksqldb-server:8088/query"); - } - - @Test - void shouldReturnTrueInTest() { - assertTrue(strategy.test("select * from users;")); - } - - @Test - void shouldReturnFalseInTest() { - assertFalse(strategy.test("show streams;")); - assertFalse(strategy.test("select *;")); - } - - @Test - void shouldSerializeResponse() { - JsonNode node = getResponseWithData(); - KsqlCommandResponseDTO serializedResponse = strategy.serializeResponse(node); - TableDTO table = serializedResponse.getData(); - assertThat(table.getHeaders()).isEqualTo(List.of("header1", "header2")); - assertThat(table.getRows()).isEqualTo(List.of(List.of("value1", "value2"))); - } - - @Test - void shouldSerializeWithException() { - JsonNode node = mapper.createObjectNode(); - Exception exception = assertThrows( - UnprocessableEntityException.class, - () -> strategy.serializeResponse(node) - ); - - assertThat(exception.getMessage()).isEqualTo("KSQL DB response mapping error"); - } - - @SneakyThrows - private JsonNode getResponseWithData() { - JsonNode headerNode = mapper.createObjectNode().set( - "header", mapper.createObjectNode().put("schema", "header1, header2") - ); - JsonNode row = mapper.createObjectNode().set( - "row", mapper.createObjectNode().set( - "columns", mapper.createArrayNode().add("value1").add("value2") - ) - ); - return mapper.createArrayNode().add(headerNode).add(row); - } -} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/ShowStrategyTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/ShowStrategyTest.java deleted file mode 100644 index 3b12afa71ae..00000000000 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/ShowStrategyTest.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.provectus.kafka.ui.strategy.ksql.statement; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.provectus.kafka.ui.exception.UnprocessableEntityException; -import com.provectus.kafka.ui.model.KsqlCommandResponseDTO; -import com.provectus.kafka.ui.model.TableDTO; -import java.util.List; -import lombok.SneakyThrows; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DynamicTest; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestFactory; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class ShowStrategyTest { - private final ObjectMapper mapper = new ObjectMapper(); - private ShowStrategy strategy; - - @BeforeEach - void setUp() { - strategy = new ShowStrategy(); - } - - @Test - void shouldReturnUri() { - strategy.host("ksqldb-server:8088"); - assertThat(strategy.getUri()).isEqualTo("ksqldb-server:8088/ksql"); - } - - @Test - void shouldReturnTrueInTest() { - assertTrue(strategy.test("SHOW STREAMS;")); - assertTrue(strategy.test("SHOW TABLES;")); - assertTrue(strategy.test("SHOW TOPICS;")); - assertTrue(strategy.test("SHOW QUERIES;")); - assertTrue(strategy.test("SHOW PROPERTIES;")); - assertTrue(strategy.test("SHOW FUNCTIONS;")); - assertTrue(strategy.test("LIST STREAMS;")); - assertTrue(strategy.test("LIST TABLES;")); - assertTrue(strategy.test("LIST TOPICS;")); - assertTrue(strategy.test("LIST FUNCTIONS;")); - } - - @Test - void shouldReturnFalseInTest() { - assertFalse(strategy.test("LIST QUERIES;")); - assertFalse(strategy.test("LIST PROPERTIES;")); - } - - @TestFactory - public Iterable shouldSerialize() { - return List.of( - shouldSerializeGenerate("streams", "show streams;"), - shouldSerializeGenerate("tables", "show tables;"), - shouldSerializeGenerate("topics", "show topics;"), - shouldSerializeGenerate("properties", "show properties;"), - shouldSerializeGenerate("functions", "show functions;"), - shouldSerializeGenerate("queries", "show queries;") - ); - } - - public DynamicTest shouldSerializeGenerate(final String key, final String sql) { - return DynamicTest.dynamicTest("Should serialize " + key, - () -> { - JsonNode node = getResponseWithData(key); - strategy.test(sql); - KsqlCommandResponseDTO serializedResponse = strategy.serializeResponse(node); - TableDTO table = serializedResponse.getData(); - assertThat(table.getHeaders()).isEqualTo(List.of("header")); - assertThat(table.getRows()).isEqualTo(List.of(List.of("value"))); - } - ); - } - - @Test - void shouldSerializeWithException() { - JsonNode node = getResponseWithData("streams"); - strategy.test("show tables;"); - Exception exception = assertThrows( - UnprocessableEntityException.class, - () -> strategy.serializeResponse(node) - ); - - assertThat(exception.getMessage()).isEqualTo("KSQL DB response mapping error"); - } - - @SneakyThrows - private JsonNode getResponseWithData(String key) { - JsonNode nodeWithDataItem = mapper.createObjectNode().put("header", "value"); - JsonNode nodeWithData = mapper.createArrayNode().add(nodeWithDataItem); - JsonNode nodeWithResponse = mapper.createObjectNode().set(key, nodeWithData); - return mapper.createArrayNode().add(mapper.valueToTree(nodeWithResponse)); - } -} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/TerminateStrategyTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/TerminateStrategyTest.java deleted file mode 100644 index 2f3b8756a1e..00000000000 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/strategy/ksql/statement/TerminateStrategyTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.provectus.kafka.ui.strategy.ksql.statement; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.provectus.kafka.ui.exception.UnprocessableEntityException; -import com.provectus.kafka.ui.model.KsqlCommandResponseDTO; -import lombok.SneakyThrows; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class TerminateStrategyTest { - private final ObjectMapper mapper = new ObjectMapper(); - private TerminateStrategy strategy; - - @BeforeEach - void setUp() { - strategy = new TerminateStrategy(); - } - - @Test - void shouldReturnUri() { - strategy.host("ksqldb-server:8088"); - assertThat(strategy.getUri()).isEqualTo("ksqldb-server:8088/ksql"); - } - - @Test - void shouldReturnTrueInTest() { - assertTrue(strategy.test("terminate query_id;")); - } - - @Test - void shouldReturnFalseInTest() { - assertFalse(strategy.test("show streams;")); - assertFalse(strategy.test("create table test;")); - } - - @Test - void shouldSerializeResponse() { - String message = "query terminated."; - JsonNode node = getResponseWithMessage(message); - KsqlCommandResponseDTO serializedResponse = strategy.serializeResponse(node); - assertThat(serializedResponse.getMessage()).isEqualTo(message); - - } - - @Test - void shouldSerializeWithException() { - JsonNode commandStatusNode = mapper.createObjectNode().put("commandStatus", "nodeWithMessage"); - JsonNode node = mapper.createArrayNode().add(mapper.valueToTree(commandStatusNode)); - Exception exception = assertThrows( - UnprocessableEntityException.class, - () -> strategy.serializeResponse(node) - ); - - assertThat(exception.getMessage()).isEqualTo("KSQL DB response mapping error"); - } - - @SneakyThrows - private JsonNode getResponseWithMessage(String message) { - JsonNode nodeWithMessage = mapper.createObjectNode().put("message", message); - JsonNode commandStatusNode = mapper.createObjectNode().set("commandStatus", nodeWithMessage); - return mapper.createArrayNode().add(mapper.valueToTree(commandStatusNode)); - } -} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/AccessControlServiceMock.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/AccessControlServiceMock.java new file mode 100644 index 00000000000..36bf78707c5 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/AccessControlServiceMock.java @@ -0,0 +1,23 @@ +package com.provectus.kafka.ui.util; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +import com.provectus.kafka.ui.service.rbac.AccessControlService; +import org.mockito.Mockito; +import reactor.core.publisher.Mono; + +public class AccessControlServiceMock { + + public AccessControlService getMock() { + AccessControlService mock = Mockito.mock(AccessControlService.class); + + when(mock.validateAccess(any())).thenReturn(Mono.empty()); + when(mock.isSchemaAccessible(anyString(), anyString())).thenReturn(Mono.just(true)); + + when(mock.filterViewableTopics(any(), any())).then(invocation -> Mono.just(invocation.getArgument(0))); + + return mock; + } +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/DynamicConfigOperationsTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/DynamicConfigOperationsTest.java new file mode 100644 index 00000000000..7355a9666fd --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/DynamicConfigOperationsTest.java @@ -0,0 +1,128 @@ +package com.provectus.kafka.ui.util; + +import static com.provectus.kafka.ui.util.DynamicConfigOperations.DYNAMIC_CONFIG_ENABLED_ENV_PROPERTY; +import static com.provectus.kafka.ui.util.DynamicConfigOperations.DYNAMIC_CONFIG_PATH_ENV_PROPERTY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.provectus.kafka.ui.config.ClustersProperties; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; + +class DynamicConfigOperationsTest { + + private static final String SAMPLE_YAML_CONFIG = """ + kafka: + clusters: + - name: test + bootstrapServers: localhost:9092 + """; + + private final ConfigurableApplicationContext ctxMock = mock(ConfigurableApplicationContext.class); + private final ConfigurableEnvironment envMock = mock(ConfigurableEnvironment.class); + + private final DynamicConfigOperations ops = new DynamicConfigOperations(ctxMock); + + @TempDir + private Path tmpDir; + + @BeforeEach + void initMocks() { + when(ctxMock.getEnvironment()).thenReturn(envMock); + } + + @Test + void initializerAddsDynamicPropertySourceIfAllEnvVarsAreSet() throws Exception { + Path propsFilePath = tmpDir.resolve("props.yaml"); + Files.writeString(propsFilePath, SAMPLE_YAML_CONFIG, StandardOpenOption.CREATE); + + MutablePropertySources propertySources = new MutablePropertySources(); + propertySources.addFirst(new MapPropertySource("test", Map.of("testK", "testV"))); + + when(envMock.getPropertySources()).thenReturn(propertySources); + mockEnvWithVars(Map.of( + DYNAMIC_CONFIG_ENABLED_ENV_PROPERTY, "true", + DYNAMIC_CONFIG_PATH_ENV_PROPERTY, propsFilePath.toString() + )); + + DynamicConfigOperations.dynamicConfigPropertiesInitializer().initialize(ctxMock); + + assertThat(propertySources.size()).isEqualTo(2); + assertThat(propertySources.stream()) + .element(0) + .extracting(PropertySource::getName) + .isEqualTo("dynamicProperties"); + } + + @ParameterizedTest + @CsvSource({ + "false, /tmp/conf.yaml", + "true, ", + ", /tmp/conf.yaml", + ",", + "true, /tmp/conf.yaml", //vars set, but file doesn't exist + }) + void initializerDoNothingIfAnyOfEnvVarsNotSet(@Nullable String enabledVar, @Nullable String pathVar) { + var vars = new HashMap(); // using HashMap to keep null values + vars.put(DYNAMIC_CONFIG_ENABLED_ENV_PROPERTY, enabledVar); + vars.put(DYNAMIC_CONFIG_PATH_ENV_PROPERTY, pathVar); + mockEnvWithVars(vars); + + DynamicConfigOperations.dynamicConfigPropertiesInitializer().initialize(ctxMock); + verify(envMock, times(0)).getPropertySources(); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void persistRewritesOrCreateConfigFile(boolean exists) throws Exception { + Path propsFilePath = tmpDir.resolve("props.yaml"); + if (exists) { + Files.writeString(propsFilePath, SAMPLE_YAML_CONFIG, StandardOpenOption.CREATE); + } + + mockEnvWithVars(Map.of( + DYNAMIC_CONFIG_ENABLED_ENV_PROPERTY, "true", + DYNAMIC_CONFIG_PATH_ENV_PROPERTY, propsFilePath.toString() + )); + + var overrideProps = new ClustersProperties(); + var cluster = new ClustersProperties.Cluster(); + cluster.setName("newName"); + overrideProps.setClusters(List.of(cluster)); + + ops.persist( + DynamicConfigOperations.PropertiesStructure.builder() + .kafka(overrideProps) + .build() + ); + + assertThat(ops.loadDynamicPropertySource()) + .get() + .extracting(ps -> ps.getProperty("kafka.clusters[0].name")) + .isEqualTo("newName"); + } + + private void mockEnvWithVars(Map envVars) { + envVars.forEach((k, v) -> when(envMock.getProperty(k)).thenReturn((String) v)); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/GithubReleaseInfoTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/GithubReleaseInfoTest.java new file mode 100644 index 00000000000..6ec4bb78638 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/GithubReleaseInfoTest.java @@ -0,0 +1,54 @@ +package com.provectus.kafka.ui.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.time.Duration; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +class GithubReleaseInfoTest { + + private final MockWebServer mockWebServer = new MockWebServer(); + + @BeforeEach + void startMockServer() throws IOException { + mockWebServer.start(); + } + + @AfterEach + void stopMockServer() throws IOException { + mockWebServer.close(); + } + + @Test + void test() { + mockWebServer.enqueue(new MockResponse() + .addHeader("content-type: application/json") + .setBody(""" + { + "published_at": "2023-03-09T16:11:31Z", + "tag_name": "v0.6.0", + "html_url": "https://github.com/provectus/kafka-ui/releases/tag/v0.6.0", + "some_unused_prop": "ololo" + } + """)); + var url = mockWebServer.url("repos/provectus/kafka-ui/releases/latest").toString(); + + var infoHolder = new GithubReleaseInfo(url); + infoHolder.refresh().block(); + + var i = infoHolder.get(); + assertThat(i.html_url()) + .isEqualTo("https://github.com/provectus/kafka-ui/releases/tag/v0.6.0"); + assertThat(i.published_at()) + .isEqualTo("2023-03-09T16:11:31Z"); + assertThat(i.tag_name()) + .isEqualTo("v0.6.0"); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/NumberUtilTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/NumberUtilTest.java deleted file mode 100644 index 1c85d655350..00000000000 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/NumberUtilTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.provectus.kafka.ui.util; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -class NumberUtilTest { - - @Test - void shouldReturnFalseWhenNonNumeric() { - Assertions.assertFalse(NumberUtil.isNumeric(Double.POSITIVE_INFINITY)); - Assertions.assertFalse(NumberUtil.isNumeric(Double.NEGATIVE_INFINITY)); - Assertions.assertFalse(NumberUtil.isNumeric(Double.NaN)); - Assertions.assertFalse(NumberUtil.isNumeric(null)); - Assertions.assertFalse(NumberUtil.isNumeric(" ")); - Assertions.assertFalse(NumberUtil.isNumeric(new Object())); - Assertions.assertFalse(NumberUtil.isNumeric("1231asd")); - } - - @Test - void shouldReturnTrueWhenNumeric() { - Assertions.assertTrue(NumberUtil.isNumeric("123.45")); - Assertions.assertTrue(NumberUtil.isNumeric(123.45)); - Assertions.assertTrue(NumberUtil.isNumeric(123)); - Assertions.assertTrue(NumberUtil.isNumeric(-123.45)); - Assertions.assertTrue(NumberUtil.isNumeric(-1e-10)); - Assertions.assertTrue(NumberUtil.isNumeric(1e-10)); - } -} \ No newline at end of file diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/OffsetsSeekTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/OffsetsSeekTest.java deleted file mode 100644 index 54c2064c1c2..00000000000 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/OffsetsSeekTest.java +++ /dev/null @@ -1,196 +0,0 @@ -package com.provectus.kafka.ui.util; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.provectus.kafka.ui.model.ConsumerPosition; -import com.provectus.kafka.ui.model.SeekDirectionDTO; -import com.provectus.kafka.ui.model.SeekTypeDTO; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.consumer.MockConsumer; -import org.apache.kafka.clients.consumer.OffsetResetStrategy; -import org.apache.kafka.common.PartitionInfo; -import org.apache.kafka.common.TopicPartition; -import org.apache.kafka.common.utils.Bytes; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -class OffsetsSeekTest { - - final String topic = "test"; - final TopicPartition tp0 = new TopicPartition(topic, 0); //offsets: start 0, end 0 - final TopicPartition tp1 = new TopicPartition(topic, 1); //offsets: start 10, end 10 - final TopicPartition tp2 = new TopicPartition(topic, 2); //offsets: start 0, end 20 - final TopicPartition tp3 = new TopicPartition(topic, 3); //offsets: start 25, end 30 - - MockConsumer consumer = new MockConsumer<>(OffsetResetStrategy.EARLIEST); - - @BeforeEach - void initConsumer() { - consumer = new MockConsumer<>(OffsetResetStrategy.EARLIEST); - consumer.updatePartitions( - topic, - Stream.of(tp0, tp1, tp2, tp3) - .map(tp -> new PartitionInfo(topic, tp.partition(), null, null, null, null)) - .collect(Collectors.toList())); - consumer.updateBeginningOffsets(Map.of( - tp0, 0L, - tp1, 10L, - tp2, 0L, - tp3, 25L - )); - consumer.updateEndOffsets(Map.of( - tp0, 0L, - tp1, 10L, - tp2, 20L, - tp3, 30L - )); - } - - @Test - void forwardSeekToBeginningAllPartitions() { - var seek = new OffsetsSeekForward( - topic, - new ConsumerPosition( - SeekTypeDTO.BEGINNING, - Map.of(tp0, 0L, tp1, 0L), - SeekDirectionDTO.FORWARD - ) - ); - - seek.assignAndSeek(consumer); - assertThat(consumer.assignment()).containsExactlyInAnyOrder(tp0, tp1); - assertThat(consumer.position(tp0)).isZero(); - assertThat(consumer.position(tp1)).isEqualTo(10L); - } - - @Test - void backwardSeekToBeginningAllPartitions() { - var seek = new OffsetsSeekBackward( - topic, - new ConsumerPosition( - SeekTypeDTO.BEGINNING, - Map.of(tp2, 0L, tp3, 0L), - SeekDirectionDTO.BACKWARD - ), - 10 - ); - - seek.assignAndSeek(consumer); - assertThat(consumer.assignment()).containsExactlyInAnyOrder(tp2, tp3); - assertThat(consumer.position(tp2)).isEqualTo(20L); - assertThat(consumer.position(tp3)).isEqualTo(30L); - } - - @Test - void forwardSeekToBeginningWithPartitionsList() { - var seek = new OffsetsSeekForward( - topic, - new ConsumerPosition(SeekTypeDTO.BEGINNING, Map.of(), SeekDirectionDTO.FORWARD)); - seek.assignAndSeek(consumer); - assertThat(consumer.assignment()).containsExactlyInAnyOrder(tp0, tp1, tp2, tp3); - assertThat(consumer.position(tp0)).isZero(); - assertThat(consumer.position(tp1)).isEqualTo(10L); - assertThat(consumer.position(tp2)).isZero(); - assertThat(consumer.position(tp3)).isEqualTo(25L); - } - - @Test - void backwardSeekToBeginningWithPartitionsList() { - var seek = new OffsetsSeekBackward( - topic, - new ConsumerPosition(SeekTypeDTO.BEGINNING, Map.of(), SeekDirectionDTO.BACKWARD), - 10 - ); - seek.assignAndSeek(consumer); - assertThat(consumer.assignment()).containsExactlyInAnyOrder(tp0, tp1, tp2, tp3); - assertThat(consumer.position(tp0)).isZero(); - assertThat(consumer.position(tp1)).isEqualTo(10L); - assertThat(consumer.position(tp2)).isEqualTo(20L); - assertThat(consumer.position(tp3)).isEqualTo(30L); - } - - - @Test - void forwardSeekToOffset() { - var seek = new OffsetsSeekForward( - topic, - new ConsumerPosition( - SeekTypeDTO.OFFSET, - Map.of(tp0, 0L, tp1, 1L, tp2, 2L), - SeekDirectionDTO.FORWARD - ) - ); - seek.assignAndSeek(consumer); - assertThat(consumer.assignment()).containsExactlyInAnyOrder(tp2); - assertThat(consumer.position(tp2)).isEqualTo(2L); - } - - @Test - void backwardSeekToOffset() { - var seek = new OffsetsSeekBackward( - topic, - new ConsumerPosition( - SeekTypeDTO.OFFSET, - Map.of(tp0, 0L, tp1, 1L, tp2, 20L), - SeekDirectionDTO.BACKWARD - ), - 2 - ); - seek.assignAndSeek(consumer); - assertThat(consumer.assignment()).containsExactlyInAnyOrder(tp2); - assertThat(consumer.position(tp2)).isEqualTo(20L); - } - - @Test - void backwardSeekToOffsetOnlyOnePartition() { - var seek = new OffsetsSeekBackward( - topic, - new ConsumerPosition( - SeekTypeDTO.OFFSET, - Map.of(tp2, 20L), - SeekDirectionDTO.BACKWARD - ), - 20 - ); - seek.assignAndSeek(consumer); - assertThat(consumer.assignment()).containsExactlyInAnyOrder(tp2); - assertThat(consumer.position(tp2)).isEqualTo(20L); - } - - - @Nested - class WaitingOffsetsTest { - - OffsetsSeekForward.WaitingOffsets offsets; - - @BeforeEach - void assignAndCreateOffsets() { - consumer.assign(List.of(tp0, tp1, tp2, tp3)); - offsets = new OffsetsSeek.WaitingOffsets(topic, consumer, List.of(tp0, tp1, tp2, tp3)); - } - - @Test - void collectsSignificantOffsetsMinus1ForAssignedPartitions() { - // offsets for partition 0 & 1 should be skipped because they - // effectively contains no data (start offset = end offset) - assertThat(offsets.getEndOffsets()).containsExactlyInAnyOrderEntriesOf( - Map.of(2, 19L, 3, 29L) - ); - } - - @Test - void returnTrueWhenOffsetsReachedReached() { - assertThat(offsets.endReached()).isFalse(); - offsets.markPolled(new ConsumerRecord<>(topic, 2, 19, null, null)); - assertThat(offsets.endReached()).isFalse(); - offsets.markPolled(new ConsumerRecord<>(topic, 3, 29, null, null)); - assertThat(offsets.endReached()).isTrue(); - } - } - -} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/PollingThrottlerTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/PollingThrottlerTest.java new file mode 100644 index 00000000000..2efe7562df9 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/PollingThrottlerTest.java @@ -0,0 +1,40 @@ +package com.provectus.kafka.ui.util; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.Percentage.withPercentage; + +import com.google.common.base.Stopwatch; +import com.google.common.util.concurrent.RateLimiter; +import com.provectus.kafka.ui.emitter.PollingThrottler; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class PollingThrottlerTest { + + @Test + void testTrafficThrottled() { + var throttler = new PollingThrottler("test", RateLimiter.create(1000)); + long polledBytes = 0; + var stopwatch = Stopwatch.createStarted(); + while (stopwatch.elapsed(TimeUnit.SECONDS) < 1) { + int newPolled = ThreadLocalRandom.current().nextInt(10); + throttler.throttleAfterPoll(newPolled); + polledBytes += newPolled; + } + assertThat(polledBytes).isCloseTo(1000, withPercentage(3.0)); + } + + @Test + void noopThrottlerDoNotLimitPolling() { + var noopThrottler = PollingThrottler.noop(); + var stopwatch = Stopwatch.createStarted(); + // emulating that we polled 1GB + for (int i = 0; i < 1024; i++) { + noopThrottler.throttleAfterPoll(1024 * 1024); + } + // checking that were are able to "poll" 1GB in less than a second + assertThat(stopwatch.elapsed().getSeconds()).isLessThan(1); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/ReactiveFailoverTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/ReactiveFailoverTest.java new file mode 100644 index 00000000000..245dab9722a --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/ReactiveFailoverTest.java @@ -0,0 +1,233 @@ +package com.provectus.kafka.ui.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.base.Preconditions; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +class ReactiveFailoverTest { + + private static final String NO_AVAILABLE_PUBLISHERS_MSG = "no active publishers!"; + private static final Predicate FAILING_EXCEPTION_FILTER = th -> th.getMessage().contains("fail!"); + private static final Supplier FAILING_EXCEPTION_SUPPLIER = () -> new IllegalStateException("fail!"); + private static final Duration RETRY_PERIOD = Duration.ofMillis(300); + + private final List publishers = Stream.generate(Publisher::new).limit(3).toList(); + + private final ReactiveFailover failover = ReactiveFailover.create( + publishers, + FAILING_EXCEPTION_FILTER, + NO_AVAILABLE_PUBLISHERS_MSG, + RETRY_PERIOD + ); + + @Test + void testMonoFailoverCycle() throws InterruptedException { + // starting with first publisher: + // 0 -> ok : ok + monoCheck( + Map.of( + 0, okMono() + ), + List.of(0), + step -> step.expectNextCount(1).verifyComplete() + ); + + // 0 -> fail, 1 -> ok : ok + monoCheck( + Map.of( + 0, failingMono(), + 1, okMono() + ), + List.of(0, 1), + step -> step.expectNextCount(1).verifyComplete() + ); + + // 0.failed, 1.failed, 2 -> ok : ok + monoCheck( + Map.of( + 1, failingMono(), + 2, okMono() + ), + List.of(1, 2), + step -> step.expectNextCount(1).verifyComplete() + ); + + // 0.failed, 1.failed, 2 -> fail : failing exception + monoCheck( + Map.of( + 2, failingMono() + ), + List.of(2), + step -> step.verifyErrorMessage(FAILING_EXCEPTION_SUPPLIER.get().getMessage()) + ); + + // 0.failed, 1.failed, 2.failed : No alive publisher exception + monoCheck( + Map.of(), + List.of(), + step -> step.verifyErrorMessage(NO_AVAILABLE_PUBLISHERS_MSG) + ); + + // resetting retry: all publishers became alive: 0.ok, 1.ok, 2.ok + Thread.sleep(RETRY_PERIOD.toMillis() + 1); + + // starting with last errored publisher: + // 2 -> fail, 0 -> fail, 1 -> ok : ok + monoCheck( + Map.of( + 2, failingMono(), + 0, failingMono(), + 1, okMono() + ), + List.of(2, 0, 1), + step -> step.expectNextCount(1).verifyComplete() + ); + + // 1 -> ok : ok + monoCheck( + Map.of( + 1, okMono() + ), + List.of(1), + step -> step.expectNextCount(1).verifyComplete() + ); + } + + @Test + void testFluxFailoverCycle() throws InterruptedException { + // starting with first publisher: + // 0 -> ok : ok + fluxCheck( + Map.of( + 0, okFlux() + ), + List.of(0), + step -> step.expectNextCount(1).verifyComplete() + ); + + // 0 -> fail, 1 -> ok : ok + fluxCheck( + Map.of( + 0, failingFlux(), + 1, okFlux() + ), + List.of(0, 1), + step -> step.expectNextCount(1).verifyComplete() + ); + + // 0.failed, 1.failed, 2 -> ok : ok + fluxCheck( + Map.of( + 1, failingFlux(), + 2, okFlux() + ), + List.of(1, 2), + step -> step.expectNextCount(1).verifyComplete() + ); + + // 0.failed, 1.failed, 2 -> fail : failing exception + fluxCheck( + Map.of( + 2, failingFlux() + ), + List.of(2), + step -> step.verifyErrorMessage(FAILING_EXCEPTION_SUPPLIER.get().getMessage()) + ); + + // 0.failed, 1.failed, 2.failed : No alive publisher exception + fluxCheck( + Map.of(), + List.of(), + step -> step.verifyErrorMessage(NO_AVAILABLE_PUBLISHERS_MSG) + ); + + // resetting retry: all publishers became alive: 0.ok, 1.ok, 2.ok + Thread.sleep(RETRY_PERIOD.toMillis() + 1); + + // starting with last errored publisher: + // 2 -> fail, 0 -> fail, 1 -> ok : ok + fluxCheck( + Map.of( + 2, failingFlux(), + 0, failingFlux(), + 1, okFlux() + ), + List.of(2, 0, 1), + step -> step.expectNextCount(1).verifyComplete() + ); + + // 1 -> ok : ok + fluxCheck( + Map.of( + 1, okFlux() + ), + List.of(1), + step -> step.expectNextCount(1).verifyComplete() + ); + } + + private void monoCheck(Map> mock, + List publishersToBeCalled, // for checking calls order + Consumer> stepVerifier) { + AtomicInteger calledCount = new AtomicInteger(); + var mono = failover.mono(publisher -> { + int calledPublisherIdx = publishers.indexOf(publisher); + assertThat(calledPublisherIdx).isEqualTo(publishersToBeCalled.get(calledCount.getAndIncrement())); + return Preconditions.checkNotNull( + mock.get(calledPublisherIdx), + "Mono result not set for publisher %d", calledPublisherIdx + ); + }); + stepVerifier.accept(StepVerifier.create(mono)); + assertThat(calledCount.get()).isEqualTo(publishersToBeCalled.size()); + } + + + private void fluxCheck(Map> mock, + List publishersToBeCalled, // for checking calls order + Consumer> stepVerifier) { + AtomicInteger calledCount = new AtomicInteger(); + var flux = failover.flux(publisher -> { + int calledPublisherIdx = publishers.indexOf(publisher); + assertThat(calledPublisherIdx).isEqualTo(publishersToBeCalled.get(calledCount.getAndIncrement())); + return Preconditions.checkNotNull( + mock.get(calledPublisherIdx), + "Mono result not set for publisher %d", calledPublisherIdx + ); + }); + stepVerifier.accept(StepVerifier.create(flux)); + assertThat(calledCount.get()).isEqualTo(publishersToBeCalled.size()); + } + + private Flux okFlux() { + return Flux.just("ok"); + } + + private Flux failingFlux() { + return Flux.error(FAILING_EXCEPTION_SUPPLIER); + } + + private Mono okMono() { + return Mono.just("ok"); + } + + private Mono failingMono() { + return Mono.error(FAILING_EXCEPTION_SUPPLIER); + } + + public static class Publisher { + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/AvroJsonSchemaConverterTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/AvroJsonSchemaConverterTest.java index d78426d48a1..24d4daf7d9d 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/AvroJsonSchemaConverterTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/AvroJsonSchemaConverterTest.java @@ -1,27 +1,29 @@ package com.provectus.kafka.ui.util.jsonschema; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.fge.jsonschema.core.exceptions.ProcessingException; -import com.github.fge.jsonschema.core.report.ProcessingReport; -import com.github.fge.jsonschema.main.JsonSchemaFactory; -import io.confluent.kafka.schemaregistry.avro.AvroSchemaUtils; -import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import lombok.SneakyThrows; import org.apache.avro.Schema; -import org.apache.avro.generic.GenericData; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -public class AvroJsonSchemaConverterTest { - @Test - public void avroConvertTest() throws URISyntaxException, JsonProcessingException { - final AvroJsonSchemaConverter converter = new AvroJsonSchemaConverter(); - URI basePath = new URI("http://example.com/"); +class AvroJsonSchemaConverterTest { + + private AvroJsonSchemaConverter converter; + private URI basePath; + + @BeforeEach + void init() throws URISyntaxException { + converter = new AvroJsonSchemaConverter(); + basePath = new URI("http://example.com/"); + } - Schema recordSchema = (new Schema.Parser()).parse( - " {" + @Test + void avroConvertTest() { + String avroSchema = + " {" + " \"type\": \"record\"," + " \"name\": \"Message\"," + " \"namespace\": \"com.provectus.kafka\"," @@ -76,45 +78,59 @@ public void avroConvertTest() throws URISyntaxException, JsonProcessingException + " }" + " }" + " ]" - + " }" - ); + + " }"; + String expectedJsonSchema = "{ " + + " \"$id\" : \"http://example.com/Message\", " + + " \"$schema\" : \"https://json-schema.org/draft/2020-12/schema\", " + + " \"type\" : \"object\", " + + " \"properties\" : { " + + " \"record\" : { \"$ref\" : \"#/definitions/com.provectus.kafka.InnerMessage\" } " + + " }, " + + " \"required\" : [ \"record\" ], " + + " \"definitions\" : { " + + " \"com.provectus.kafka.Message\" : { \"$ref\" : \"#\" }, " + + " \"com.provectus.kafka.InnerMessage\" : { " + + " \"type\" : \"object\", " + + " \"properties\" : { " + + " \"long_text\" : { " + + " \"oneOf\" : [ { " + + " \"type\" : \"null\" " + + " }, { " + + " \"type\" : \"object\", " + + " \"properties\" : { " + + " \"string\" : { " + + " \"type\" : \"string\" " + + " } " + + " } " + + " } ] " + + " }, " + + " \"array\" : { " + + " \"type\" : \"array\", " + + " \"items\" : { \"type\" : \"string\" } " + + " }, " + + " \"id\" : { \"type\" : \"integer\" }, " + + " \"text\" : { \"type\" : \"string\" }, " + + " \"map\" : { " + + " \"type\" : \"object\", " + + " \"additionalProperties\" : { \"type\" : \"integer\" } " + + " }, " + + " \"order\" : { " + + " \"enum\" : [ \"SPADES\", \"HEARTS\", \"DIAMONDS\", \"CLUBS\" ], " + + " \"type\" : \"string\" " + + " } " + + " }, " + + " \"required\" : [ \"id\", \"text\", \"order\", \"array\", \"map\" ] " + + " } " + + " } " + + "}"; - String expected = "{\"$id\":\"http://example.com/Message\"," - + "\"$schema\":\"https://json-schema.org/draft/2020-12/schema\"," - + "\"type\":\"object\",\"properties\":{\"record\":" - + "{\"$ref\":\"#/definitions/RecordInnerMessage\"}}," - + "\"required\":[\"record\"],\"definitions\":" - + "{\"RecordInnerMessage\":{\"type\":\"object\",\"" - + "properties\":{\"long_text\":{\"oneOf\":[{\"type\":\"null\"}," - + "{\"type\":\"object\",\"properties\":{\"string\":" - + "{\"type\":\"string\"}}}]},\"array\":{\"type\":\"array\",\"items\":" - + "{\"type\":\"string\"}},\"id\":{\"type\":\"integer\"},\"text\":" - + "{\"type\":\"string\"},\"map\":{\"type\":\"object\"," - + "\"additionalProperties\":{\"type\":\"integer\"}}," - + "\"order\":{\"enum\":[\"SPADES\",\"HEARTS\",\"DIAMONDS\",\"CLUBS\"]," - + "\"type\":\"string\"}}," - + "\"required\":[\"id\",\"text\",\"order\",\"array\",\"map\"]}}}"; - - final JsonSchema convertRecord = converter.convert(basePath, recordSchema); - - ObjectMapper om = new ObjectMapper(); - Assertions.assertEquals( - om.readTree(expected), - om.readTree( - convertRecord.toJson() - ) - ); - + convertAndCompare(expectedJsonSchema, avroSchema); } @Test - public void testNullableUnions() throws URISyntaxException, IOException, ProcessingException { - final AvroJsonSchemaConverter converter = new AvroJsonSchemaConverter(); - URI basePath = new URI("http://example.com/"); - final ObjectMapper objectMapper = new ObjectMapper(); - - Schema recordSchema = (new Schema.Parser()).parse( + void testNullableUnions() { + String avroSchema = " {" + " \"type\": \"record\"," + " \"name\": \"Message\"," @@ -138,38 +154,105 @@ public void testNullableUnions() throws URISyntaxException, IOException, Process + " \"default\": null" + " }" + " ]" - + " }" - ); - - final GenericData.Record record = new GenericData.Record(recordSchema); - record.put("text", "Hello world"); - record.put("value", 100L); - byte[] jsonBytes = AvroSchemaUtils.toJson(record); - String serialized = new String(jsonBytes); + + " }"; - String expected = + String expectedJsonSchema = "{\"$id\":\"http://example.com/Message\"," + "\"$schema\":\"https://json-schema.org/draft/2020-12/schema\"," + "\"type\":\"object\",\"properties\":{\"text\":" + "{\"oneOf\":[{\"type\":\"null\"},{\"type\":\"object\"," + "\"properties\":{\"string\":{\"type\":\"string\"}}}]},\"value\":" + "{\"oneOf\":[{\"type\":\"null\"},{\"type\":\"object\"," - + "\"properties\":{\"string\":{\"type\":\"string\"},\"long\":{\"type\":\"integer\"}}}]}}}"; + + "\"properties\":{\"string\":{\"type\":\"string\"},\"long\":{\"type\":\"integer\"}}}]}}," + + "\"definitions\" : { \"com.provectus.kafka.Message\" : { \"$ref\" : \"#\" }}}"; - final JsonSchema convert = converter.convert(basePath, recordSchema); - Assertions.assertEquals( - objectMapper.readTree(expected), - objectMapper.readTree(convert.toJson()) - ); + convertAndCompare(expectedJsonSchema, avroSchema); + } + @Test + void testRecordReferences() { + String avroSchema = + "{\n" + + " \"type\": \"record\", " + + " \"namespace\": \"n.s\", " + + " \"name\": \"RootMsg\", " + + " \"fields\":\n" + + " [ " + + " { " + + " \"name\": \"inner1\", " + + " \"type\": { " + + " \"type\": \"record\", " + + " \"name\": \"Inner\", " + + " \"fields\": [ { \"name\": \"f1\", \"type\": \"double\" } ] " + + " } " + + " }, " + + " { " + + " \"name\": \"inner2\", " + + " \"type\": { " + + " \"type\": \"record\", " + + " \"namespace\": \"n.s2\", " + + " \"name\": \"Inner\", " + + " \"fields\": " + + " [ { \"name\": \"f1\", \"type\": \"double\" } ] " + + " } " + + " }, " + + " { " + + " \"name\": \"refField\", " + + " \"type\": [ \"null\", \"Inner\", \"n.s2.Inner\", \"RootMsg\" ] " + + " } " + + " ] " + + "}"; + + String expectedJsonSchema = "{ " + + " \"$id\" : \"http://example.com/RootMsg\", " + + " \"$schema\" : \"https://json-schema.org/draft/2020-12/schema\", " + + " \"type\" : \"object\", " + + " \"properties\" : { " + + " \"inner1\" : { \"$ref\" : \"#/definitions/n.s.Inner\" }, " + + " \"inner2\" : { \"$ref\" : \"#/definitions/n.s2.Inner\" }, " + + " \"refField\" : { " + + " \"oneOf\" : [ " + + " { " + + " \"type\" : \"null\" " + + " }, " + + " { " + + " \"type\" : \"object\", " + + " \"properties\" : { " + + " \"n.s.RootMsg\" : { \"$ref\" : \"#/definitions/n.s.RootMsg\" }, " + + " \"n.s2.Inner\" : { \"$ref\" : \"#/definitions/n.s2.Inner\" }, " + + " \"n.s.Inner\" : { \"$ref\" : \"#/definitions/n.s.Inner\" } " + + " } " + + " } ] " + + " } " + + " }, " + + " \"required\" : [ \"inner1\", \"inner2\" ], " + + " \"definitions\" : { " + + " \"n.s.RootMsg\" : { \"$ref\" : \"#\" }, " + + " \"n.s2.Inner\" : { " + + " \"type\" : \"object\", " + + " \"properties\" : { \"f1\" : { \"type\" : \"number\" } }, " + + " \"required\" : [ \"f1\" ] " + + " }, " + + " \"n.s.Inner\" : { " + + " \"type\" : \"object\", " + + " \"properties\" : { \"f1\" : { \"type\" : \"number\" } }, " + + " \"required\" : [ \"f1\" ] " + + " } " + + " } " + + "}"; - final ProcessingReport validate = - JsonSchemaFactory.byDefault().getJsonSchema( - objectMapper.readTree(expected) - ).validate( - objectMapper.readTree(serialized) - ); + convertAndCompare(expectedJsonSchema, avroSchema); + } - Assertions.assertTrue(validate.isSuccess()); + @SneakyThrows + private void convertAndCompare(String expectedJsonSchema, String sourceAvroSchema) { + var parseAvroSchema = new Schema.Parser().parse(sourceAvroSchema); + var converted = converter.convert(basePath, parseAvroSchema).toJson(); + var objectMapper = new ObjectMapper(); + Assertions.assertEquals( + objectMapper.readTree(expectedJsonSchema), + objectMapper.readTree(converted) + ); } + } \ No newline at end of file diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/JsonAvroConversionTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/JsonAvroConversionTest.java new file mode 100644 index 00000000000..7c4d79f30d7 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/JsonAvroConversionTest.java @@ -0,0 +1,713 @@ +package com.provectus.kafka.ui.util.jsonschema; + +import static com.provectus.kafka.ui.util.jsonschema.JsonAvroConversion.convertAvroToJson; +import static com.provectus.kafka.ui.util.jsonschema.JsonAvroConversion.convertJsonToAvro; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.fasterxml.jackson.databind.node.DoubleNode; +import com.fasterxml.jackson.databind.node.FloatNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.LongNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.google.common.primitives.Longs; +import com.provectus.kafka.ui.exception.JsonAvroConversionException; +import io.confluent.kafka.schemaregistry.avro.AvroSchema; +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import lombok.SneakyThrows; +import org.apache.avro.Schema; +import org.apache.avro.generic.GenericData; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class JsonAvroConversionTest { + + // checking conversion from json to KafkaAvroSerializer-compatible avro objects + @Nested + class FromJsonToAvro { + + @Test + void primitiveRoot() { + assertThat(convertJsonToAvro("\"str\"", createSchema("\"string\""))) + .isEqualTo("str"); + + assertThat(convertJsonToAvro("123", createSchema("\"int\""))) + .isEqualTo(123); + + assertThat(convertJsonToAvro("123", createSchema("\"long\""))) + .isEqualTo(123L); + + assertThat(convertJsonToAvro("123.123", createSchema("\"float\""))) + .isEqualTo(123.123F); + + assertThat(convertJsonToAvro("12345.12345", createSchema("\"double\""))) + .isEqualTo(12345.12345); + } + + @Test + void primitiveTypedFields() { + var schema = createSchema( + """ + { + "type": "record", + "name": "TestAvroRecord", + "fields": [ + { + "name": "f_int", + "type": "int" + }, + { + "name": "f_long", + "type": "long" + }, + { + "name": "f_string", + "type": "string" + }, + { + "name": "f_boolean", + "type": "boolean" + }, + { + "name": "f_float", + "type": "float" + }, + { + "name": "f_double", + "type": "double" + }, + { + "name": "f_enum", + "type" : { + "type": "enum", + "name": "Suit", + "symbols" : ["SPADES", "HEARTS", "DIAMONDS", "CLUBS"] + } + }, + { + "name" : "f_fixed", + "type" : { "type" : "fixed" ,"size" : 8, "name": "long_encoded" } + }, + { + "name" : "f_bytes", + "type": "bytes" + } + ] + }""" + ); + + String jsonPayload = """ + { + "f_int": 123, + "f_long": 4294967294, + "f_string": "string here", + "f_boolean": true, + "f_float": 123.1, + "f_double": 123456.123456, + "f_enum": "SPADES", + "f_fixed": "\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0004Ò", + "f_bytes": "\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\t)" + } + """; + + var converted = convertJsonToAvro(jsonPayload, schema); + assertThat(converted).isInstanceOf(GenericData.Record.class); + + var record = (GenericData.Record) converted; + assertThat(record.get("f_int")).isEqualTo(123); + assertThat(record.get("f_long")).isEqualTo(4294967294L); + assertThat(record.get("f_string")).isEqualTo("string here"); + assertThat(record.get("f_boolean")).isEqualTo(true); + assertThat(record.get("f_float")).isEqualTo(123.1f); + assertThat(record.get("f_double")).isEqualTo(123456.123456); + assertThat(record.get("f_enum")) + .isEqualTo( + new GenericData.EnumSymbol( + schema.getField("f_enum").schema(), + "SPADES" + ) + ); + assertThat(((GenericData.Fixed) record.get("f_fixed")).bytes()).isEqualTo(Longs.toByteArray(1234L)); + assertThat(((ByteBuffer) record.get("f_bytes")).array()).isEqualTo(Longs.toByteArray(2345L)); + } + + @Test + void unionRoot() { + var schema = createSchema("[ \"null\", \"string\", \"int\" ]"); + + var converted = convertJsonToAvro("{\"string\":\"string here\"}", schema); + assertThat(converted).isEqualTo("string here"); + + converted = convertJsonToAvro("{\"int\": 123}", schema); + assertThat(converted).isEqualTo(123); + + converted = convertJsonToAvro("null", schema); + assertThat(converted).isEqualTo(null); + } + + @Test + void unionField() { + var schema = createSchema( + """ + { + "type": "record", + "namespace": "com.test", + "name": "TestAvroRecord", + "fields": [ + { + "name": "f_union", + "type": [ "null", "int", "TestAvroRecord"] + } + ] + }""" + ); + + String jsonPayload = "{ \"f_union\": null }"; + + var record = (GenericData.Record) convertJsonToAvro(jsonPayload, schema); + assertThat(record.get("f_union")).isNull(); + + jsonPayload = "{ \"f_union\": { \"int\": 123 } }"; + record = (GenericData.Record) convertJsonToAvro(jsonPayload, schema); + assertThat(record.get("f_union")).isEqualTo(123); + + //short name can be used since there is no clash with other type names + jsonPayload = "{ \"f_union\": { \"TestAvroRecord\": { \"f_union\": { \"int\": 123 } } } }"; + record = (GenericData.Record) convertJsonToAvro(jsonPayload, schema); + assertThat(record.get("f_union")).isInstanceOf(GenericData.Record.class); + var innerRec = (GenericData.Record) record.get("f_union"); + assertThat(innerRec.get("f_union")).isEqualTo(123); + + assertThatThrownBy(() -> + convertJsonToAvro("{ \"f_union\": { \"NotExistingType\": 123 } }", schema) + ).isInstanceOf(JsonAvroConversionException.class); + } + + @Test + void unionFieldWithTypeNamesClash() { + var schema = createSchema( + """ + { + "type": "record", + "namespace": "com.test", + "name": "TestAvroRecord", + "fields": [ + { + "name": "nestedClass", + "type": { + "type": "record", + "namespace": "com.nested", + "name": "TestAvroRecord", + "fields": [ + {"name" : "inner_obj_field", "type": "int" } + ] + } + }, + { + "name": "f_union", + "type": [ "null", "int", "com.test.TestAvroRecord", "com.nested.TestAvroRecord"] + } + ] + }""" + ); + //short name can't can be used since there is a clash with other type names + var jsonPayload = "{ \"f_union\": { \"com.test.TestAvroRecord\": { \"f_union\": { \"int\": 123 } } } }"; + var record = (GenericData.Record) convertJsonToAvro(jsonPayload, schema); + assertThat(record.get("f_union")).isInstanceOf(GenericData.Record.class); + var innerRec = (GenericData.Record) record.get("f_union"); + assertThat(innerRec.get("f_union")).isEqualTo(123); + + //short name can't can be used since there is a clash with other type names + jsonPayload = "{ \"f_union\": { \"com.nested.TestAvroRecord\": { \"inner_obj_field\": 234 } } }"; + record = (GenericData.Record) convertJsonToAvro(jsonPayload, schema); + assertThat(record.get("f_union")).isInstanceOf(GenericData.Record.class); + innerRec = (GenericData.Record) record.get("f_union"); + assertThat(innerRec.get("inner_obj_field")).isEqualTo(234); + + assertThatThrownBy(() -> + convertJsonToAvro("{ \"f_union\": { \"TestAvroRecord\": { \"inner_obj_field\": 234 } } }", schema) + ).isInstanceOf(JsonAvroConversionException.class); + } + + @Test + void mapField() { + var schema = createSchema( + """ + { + "type": "record", + "name": "TestAvroRecord", + "fields": [ + { + "name": "long_map", + "type": { + "type": "map", + "values" : "long", + "default": {} + } + }, + { + "name": "string_map", + "type": { + "type": "map", + "values" : "string", + "default": {} + } + }, + { + "name": "self_ref_map", + "type": { + "type": "map", + "values" : "TestAvroRecord", + "default": {} + } + } + ] + }""" + ); + + String jsonPayload = """ + { + "long_map": { + "k1": 123, + "k2": 456 + }, + "string_map": { + "k3": "s1", + "k4": "s2" + }, + "self_ref_map": { + "k5" : { + "long_map": { "_k1": 222 }, + "string_map": { "_k2": "_s1" } + } + } + } + """; + + var record = (GenericData.Record) convertJsonToAvro(jsonPayload, schema); + assertThat(record.get("long_map")) + .isEqualTo(Map.of("k1", 123L, "k2", 456L)); + assertThat(record.get("string_map")) + .isEqualTo(Map.of("k3", "s1", "k4", "s2")); + assertThat(record.get("self_ref_map")) + .isNotNull(); + + Map selfRefMapField = (Map) record.get("self_ref_map"); + assertThat(selfRefMapField) + .hasSize(1) + .hasEntrySatisfying("k5", v -> { + assertThat(v).isInstanceOf(GenericData.Record.class); + var innerRec = (GenericData.Record) v; + assertThat(innerRec.get("long_map")) + .isEqualTo(Map.of("_k1", 222L)); + assertThat(innerRec.get("string_map")) + .isEqualTo(Map.of("_k2", "_s1")); + }); + } + + @Test + void arrayField() { + var schema = createSchema( + """ + { + "type": "record", + "name": "TestAvroRecord", + "fields": [ + { + "name": "f_array", + "type": { + "type": "array", + "items" : "string", + "default": [] + } + } + ] + }""" + ); + + String jsonPayload = """ + { + "f_array": [ "e1", "e2" ] + } + """; + + var record = (GenericData.Record) convertJsonToAvro(jsonPayload, schema); + assertThat(record.get("f_array")).isEqualTo(List.of("e1", "e2")); + } + + @Test + void logicalTypesField() { + var schema = createSchema( + """ + { + "type": "record", + "name": "TestAvroRecord", + "fields": [ + { + "name": "lt_date", + "type": { "type": "int", "logicalType": "date" } + }, + { + "name": "lt_uuid", + "type": { "type": "string", "logicalType": "uuid" } + }, + { + "name": "lt_decimal", + "type": { "type": "bytes", "logicalType": "decimal", "precision": 22, "scale":10 } + }, + { + "name": "lt_time_millis", + "type": { "type": "int", "logicalType": "time-millis"} + }, + { + "name": "lt_time_micros", + "type": { "type": "long", "logicalType": "time-micros"} + }, + { + "name": "lt_timestamp_millis", + "type": { "type": "long", "logicalType": "timestamp-millis" } + }, + { + "name": "lt_timestamp_micros", + "type": { "type": "long", "logicalType": "timestamp-micros" } + }, + { + "name": "lt_local_timestamp_millis", + "type": { "type": "long", "logicalType": "local-timestamp-millis" } + }, + { + "name": "lt_local_timestamp_micros", + "type": { "type": "long", "logicalType": "local-timestamp-micros" } + } + ] + }""" + ); + + String jsonPayload = """ + { + "lt_date":"1991-08-14", + "lt_decimal": 2.1617413862327545E11, + "lt_time_millis": "10:15:30.001", + "lt_time_micros": "10:15:30.123456", + "lt_uuid": "a37b75ca-097c-5d46-6119-f0637922e908", + "lt_timestamp_millis": "2007-12-03T10:15:30.123Z", + "lt_timestamp_micros": "2007-12-13T10:15:30.123456Z", + "lt_local_timestamp_millis": "2017-12-03T10:15:30.123", + "lt_local_timestamp_micros": "2017-12-13T10:15:30.123456" + } + """; + + var converted = convertJsonToAvro(jsonPayload, schema); + assertThat(converted).isInstanceOf(GenericData.Record.class); + + var record = (GenericData.Record) converted; + + assertThat(record.get("lt_date")) + .isEqualTo(LocalDate.of(1991, 8, 14)); + assertThat(record.get("lt_decimal")) + .isEqualTo(new BigDecimal("2.1617413862327545E11")); + assertThat(record.get("lt_time_millis")) + .isEqualTo(LocalTime.parse("10:15:30.001")); + assertThat(record.get("lt_time_micros")) + .isEqualTo(LocalTime.parse("10:15:30.123456")); + assertThat(record.get("lt_timestamp_millis")) + .isEqualTo(Instant.parse("2007-12-03T10:15:30.123Z")); + assertThat(record.get("lt_timestamp_micros")) + .isEqualTo(Instant.parse("2007-12-13T10:15:30.123456Z")); + assertThat(record.get("lt_local_timestamp_millis")) + .isEqualTo(LocalDateTime.parse("2017-12-03T10:15:30.123")); + assertThat(record.get("lt_local_timestamp_micros")) + .isEqualTo(LocalDateTime.parse("2017-12-13T10:15:30.123456")); + } + } + + // checking conversion of KafkaAvroDeserializer output to JsonNode + @Nested + class FromAvroToJson { + + @Test + void primitiveRoot() { + assertThat(convertAvroToJson("str", createSchema("\"string\""))) + .isEqualTo(new TextNode("str")); + + assertThat(convertAvroToJson(123, createSchema("\"int\""))) + .isEqualTo(new IntNode(123)); + + assertThat(convertAvroToJson(123L, createSchema("\"long\""))) + .isEqualTo(new LongNode(123)); + + assertThat(convertAvroToJson(123.1F, createSchema("\"float\""))) + .isEqualTo(new FloatNode(123.1F)); + + assertThat(convertAvroToJson(123.1, createSchema("\"double\""))) + .isEqualTo(new DoubleNode(123.1)); + + assertThat(convertAvroToJson(true, createSchema("\"boolean\""))) + .isEqualTo(BooleanNode.valueOf(true)); + + assertThat(convertAvroToJson(ByteBuffer.wrap(Longs.toByteArray(123L)), createSchema("\"bytes\""))) + .isEqualTo(new TextNode(new String(Longs.toByteArray(123L), StandardCharsets.ISO_8859_1))); + } + + @SneakyThrows + @Test + void primitiveTypedFields() { + var schema = createSchema( + """ + { + "type": "record", + "name": "TestAvroRecord", + "fields": [ + { + "name": "f_int", + "type": "int" + }, + { + "name": "f_long", + "type": "long" + }, + { + "name": "f_string", + "type": "string" + }, + { + "name": "f_boolean", + "type": "boolean" + }, + { + "name": "f_float", + "type": "float" + }, + { + "name": "f_double", + "type": "double" + }, + { + "name": "f_enum", + "type" : { + "type": "enum", + "name": "Suit", + "symbols" : ["SPADES", "HEARTS", "DIAMONDS", "CLUBS"] + } + }, + { + "name" : "f_fixed", + "type" : { "type" : "fixed" ,"size" : 8, "name": "long_encoded" } + }, + { + "name" : "f_bytes", + "type": "bytes" + } + ] + }""" + ); + + byte[] fixedFieldValue = Longs.toByteArray(1234L); + byte[] bytesFieldValue = Longs.toByteArray(2345L); + + GenericData.Record inputRecord = new GenericData.Record(schema); + inputRecord.put("f_int", 123); + inputRecord.put("f_long", 4294967294L); + inputRecord.put("f_string", "string here"); + inputRecord.put("f_boolean", true); + inputRecord.put("f_float", 123.1f); + inputRecord.put("f_double", 123456.123456); + inputRecord.put("f_enum", new GenericData.EnumSymbol(schema.getField("f_enum").schema(), "SPADES")); + inputRecord.put("f_fixed", new GenericData.Fixed(schema.getField("f_fixed").schema(), fixedFieldValue)); + inputRecord.put("f_bytes", ByteBuffer.wrap(bytesFieldValue)); + + String expectedJson = """ + { + "f_int": 123, + "f_long": 4294967294, + "f_string": "string here", + "f_boolean": true, + "f_float": 123.1, + "f_double": 123456.123456, + "f_enum": "SPADES", + "f_fixed": "\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0004Ò", + "f_bytes": "\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\t)" + } + """; + + assertJsonsEqual(expectedJson, convertAvroToJson(inputRecord, schema)); + } + + @Test + void logicalTypesField() { + var schema = createSchema( + """ + { + "type": "record", + "name": "TestAvroRecord", + "fields": [ + { + "name": "lt_date", + "type": { "type": "int", "logicalType": "date" } + }, + { + "name": "lt_uuid", + "type": { "type": "string", "logicalType": "uuid" } + }, + { + "name": "lt_decimal", + "type": { "type": "bytes", "logicalType": "decimal", "precision": 22, "scale":10 } + }, + { + "name": "lt_time_millis", + "type": { "type": "int", "logicalType": "time-millis"} + }, + { + "name": "lt_time_micros", + "type": { "type": "long", "logicalType": "time-micros"} + }, + { + "name": "lt_timestamp_millis", + "type": { "type": "long", "logicalType": "timestamp-millis" } + }, + { + "name": "lt_timestamp_micros", + "type": { "type": "long", "logicalType": "timestamp-micros" } + }, + { + "name": "lt_local_timestamp_millis", + "type": { "type": "long", "logicalType": "local-timestamp-millis" } + }, + { + "name": "lt_local_timestamp_micros", + "type": { "type": "long", "logicalType": "local-timestamp-micros" } + } + ] + }""" + ); + + GenericData.Record inputRecord = new GenericData.Record(schema); + inputRecord.put("lt_date", LocalDate.of(1991, 8, 14)); + inputRecord.put("lt_uuid", UUID.fromString("a37b75ca-097c-5d46-6119-f0637922e908")); + inputRecord.put("lt_decimal", new BigDecimal("2.16")); + inputRecord.put("lt_time_millis", LocalTime.parse("10:15:30.001")); + inputRecord.put("lt_time_micros", LocalTime.parse("10:15:30.123456")); + inputRecord.put("lt_timestamp_millis", Instant.parse("2007-12-03T10:15:30.123Z")); + inputRecord.put("lt_timestamp_micros", Instant.parse("2007-12-13T10:15:30.123456Z")); + inputRecord.put("lt_local_timestamp_millis", LocalDateTime.parse("2017-12-03T10:15:30.123")); + inputRecord.put("lt_local_timestamp_micros", LocalDateTime.parse("2017-12-13T10:15:30.123456")); + + String expectedJson = """ + { + "lt_date":"1991-08-14", + "lt_uuid": "a37b75ca-097c-5d46-6119-f0637922e908", + "lt_decimal": 2.16, + "lt_time_millis": "10:15:30.001", + "lt_time_micros": "10:15:30.123456", + "lt_timestamp_millis": "2007-12-03T10:15:30.123Z", + "lt_timestamp_micros": "2007-12-13T10:15:30.123456Z", + "lt_local_timestamp_millis": "2017-12-03T10:15:30.123", + "lt_local_timestamp_micros": "2017-12-13T10:15:30.123456" + } + """; + + assertJsonsEqual(expectedJson, convertAvroToJson(inputRecord, schema)); + } + + @Test + void unionField() { + var schema = createSchema( + """ + { + "type": "record", + "namespace": "com.test", + "name": "TestAvroRecord", + "fields": [ + { + "name": "f_union", + "type": [ "null", "int", "TestAvroRecord"] + } + ] + }""" + ); + + var r = new GenericData.Record(schema); + r.put("f_union", null); + assertJsonsEqual(" {}", convertAvroToJson(r, schema)); + + r = new GenericData.Record(schema); + r.put("f_union", 123); + assertJsonsEqual(" { \"f_union\" : { \"int\" : 123 } }", convertAvroToJson(r, schema)); + + + r = new GenericData.Record(schema); + var innerRec = new GenericData.Record(schema); + innerRec.put("f_union", 123); + r.put("f_union", innerRec); + // short type name can be set since there is NO clash with other types name + assertJsonsEqual( + " { \"f_union\" : { \"TestAvroRecord\" : { \"f_union\" : { \"int\" : 123 } } } }", + convertAvroToJson(r, schema) + ); + } + + @Test + void unionFieldWithInnerTypesNamesClash() { + var schema = createSchema( + """ + { + "type": "record", + "namespace": "com.test", + "name": "TestAvroRecord", + "fields": [ + { + "name": "nestedClass", + "type": { + "type": "record", + "namespace": "com.nested", + "name": "TestAvroRecord", + "fields": [ + {"name" : "inner_obj_field", "type": "int" } + ] + } + }, + { + "name": "f_union", + "type": [ "null", "int", "com.test.TestAvroRecord", "com.nested.TestAvroRecord"] + } + ] + }""" + ); + + var r = new GenericData.Record(schema); + var innerRec = new GenericData.Record(schema); + innerRec.put("f_union", 123); + r.put("f_union", innerRec); + // full type name should be set since there is a clash with other type name + assertJsonsEqual( + " { \"f_union\" : { \"com.test.TestAvroRecord\" : { \"f_union\" : { \"int\" : 123 } } } }", + convertAvroToJson(r, schema) + ); + } + + } + + private Schema createSchema(String schema) { + return new AvroSchema(schema).rawSchema(); + } + + @SneakyThrows + private void assertJsonsEqual(String expectedJson, JsonNode actual) { + var mapper = new JsonMapper(); + assertThat(actual.toPrettyString()) + .isEqualTo(mapper.readTree(expectedJson).toPrettyString()); + } + +} diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/ProtobufSchemaConverterTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/ProtobufSchemaConverterTest.java index 605ed6ea007..04161848068 100644 --- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/ProtobufSchemaConverterTest.java +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/ProtobufSchemaConverterTest.java @@ -1,70 +1,144 @@ package com.provectus.kafka.ui.util.jsonschema; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema; import java.net.URI; -import java.net.URISyntaxException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -public class ProtobufSchemaConverterTest { +class ProtobufSchemaConverterTest { @Test - public void testSimpleProto() throws URISyntaxException, JsonProcessingException { - - String proto = "syntax = \"proto3\";\n" - + "package com.acme;\n" - + "\n" - + "message MyRecord {\n" - + " string f1 = 1;\n" - + " OtherRecord f2 = 2;\n" - + " repeated OtherRecord f3 = 3;\n" - + "}\n" - + "\n" - + "message OtherRecord {\n" - + " int32 other_id = 1;\n" - + " Order order = 2;\n" - + " oneof optionalField {" - + " string name = 3;" - + " uint64 size = 4;" - + " }" - + "}\n" - + "\n" - + "enum Order {\n" - + " FIRST = 1;\n" - + " SECOND = 1;\n" - + "}\n"; - - String expected = - "{\"$id\":\"http://example.com/com.acme.MyRecord\"," - + "\"$schema\":\"https://json-schema.org/draft/2020-12/schema\"," - + "\"type\":\"object\",\"properties\":{\"f1\":{\"type\":\"string\"}," - + "\"f2\":{\"$ref\":\"#/definitions/record.com.acme.OtherRecord\"}," - + "\"f3\":{\"type\":\"array\"," - + "\"items\":{\"$ref\":\"#/definitions/record.com.acme.OtherRecord\"}}}," - + "\"required\":[\"f3\"]," - + "\"definitions\":" - + "{\"record.com.acme.OtherRecord\":" - + "{\"type\":\"object\",\"properties\":" - + "{\"optionalField\":{\"oneOf\":[{\"type\":\"string\"}," - + "{\"type\":\"integer\"}]},\"other_id\":" - + "{\"type\":\"integer\"},\"order\":{\"enum\":[\"FIRST\",\"SECOND\"]," - + "\"type\":\"string\"}}}}}"; - - ProtobufSchema protobufSchema = new ProtobufSchema(proto); - - final ProtobufSchemaConverter converter = new ProtobufSchemaConverter(); + void testSchemaConvert() throws Exception { + String protoSchema = """ + syntax = "proto3"; + package test; + + import "google/protobuf/timestamp.proto"; + import "google/protobuf/duration.proto"; + import "google/protobuf/struct.proto"; + import "google/protobuf/wrappers.proto"; + + message TestMsg { + string string_field = 1; + int32 int32_field = 2; + bool bool_field = 3; + SampleEnum enum_field = 4; + + enum SampleEnum { + ENUM_V1 = 0; + ENUM_V2 = 1; + } + + google.protobuf.Timestamp ts_field = 5; + google.protobuf.Struct struct_field = 6; + google.protobuf.ListValue lst_v_field = 7; + google.protobuf.Duration duration_field = 8; + + oneof some_oneof1 { + google.protobuf.Value v1 = 9; + google.protobuf.Value v2 = 10; + } + // wrapper fields: + google.protobuf.Int64Value int64_w_field = 11; + google.protobuf.Int32Value int32_w_field = 12; + google.protobuf.UInt64Value uint64_w_field = 13; + google.protobuf.UInt32Value uint32_w_field = 14; + google.protobuf.StringValue string_w_field = 15; + google.protobuf.BoolValue bool_w_field = 16; + google.protobuf.DoubleValue double_w_field = 17; + google.protobuf.FloatValue float_w_field = 18; + + //embedded msg + EmbeddedMsg emb = 19; + repeated EmbeddedMsg emb_list = 20; + + message EmbeddedMsg { + int32 emb_f1 = 1; + TestMsg outer_ref = 2; + EmbeddedMsg self_ref = 3; + } + + map intToStringMap = 21; + map strToObjMap = 22; + }"""; + + String expectedJsonSchema = """ + { + "$id": "http://example.com/test.TestMsg", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "definitions": + { + "test.TestMsg": + { + "type": "object", + "properties": + { + "enum_field": { + "enum": + [ + "ENUM_V1", + "ENUM_V2" + ], + "type": "string" + }, + "string_w_field": { "type": "string" }, + "ts_field": { "type": "string", "format": "date-time" }, + "emb_list": { + "type": "array", + "items": { "$ref": "#/definitions/test.TestMsg.EmbeddedMsg" } + }, + "float_w_field": { "type": "number" }, + "lst_v_field": { + "type": "array", + "items": { "type":[ "number", "string", "object", "array", "boolean", "null" ] } + }, + "struct_field": { "type": "object", "properties": {} }, + "string_field": { "type": "string" }, + "double_w_field": { "type": "number" }, + "bool_field": { "type": "boolean" }, + "int32_w_field": { "type": "integer", "maximum": 2147483647, "minimum": -2147483648 }, + "duration_field": { "type": "string" }, + "int32_field": { "type": "integer", "maximum": 2147483647, "minimum": -2147483648 }, + "int64_w_field": { + "type": "integer", + "maximum": 9223372036854775807, "minimum": -9223372036854775808 + }, + "v1": { "type": [ "number", "string", "object", "array", "boolean", "null" ] }, + "emb": { "$ref": "#/definitions/test.TestMsg.EmbeddedMsg" }, + "v2": { "type": [ "number", "string", "object", "array", "boolean", "null" ] }, + "uint32_w_field": { "type": "integer", "maximum": 4294967295, "minimum": 0 }, + "bool_w_field": { "type": "boolean" }, + "uint64_w_field": { "type": "integer", "maximum": 18446744073709551615, "minimum": 0 }, + "strToObjMap": { "type": "object", "additionalProperties": true }, + "intToStringMap": { "type": "object", "additionalProperties": true } + } + }, + "test.TestMsg.EmbeddedMsg": { + "type": "object", + "properties": + { + "emb_f1": { "type": "integer", "maximum": 2147483647, "minimum": -2147483648 }, + "outer_ref": { "$ref": "#/definitions/test.TestMsg" }, + "self_ref": { "$ref": "#/definitions/test.TestMsg.EmbeddedMsg" } + } + } + }, + "$ref": "#/definitions/test.TestMsg" + }"""; + + ProtobufSchemaConverter converter = new ProtobufSchemaConverter(); + ProtobufSchema protobufSchema = new ProtobufSchema(protoSchema); URI basePath = new URI("http://example.com/"); - final JsonSchema convert = - converter.convert(basePath, protobufSchema.toDescriptor("MyRecord")); + JsonSchema converted = converter.convert(basePath, protobufSchema.toDescriptor()); + assertJsonEqual(expectedJsonSchema, converted.toJson()); + } + private void assertJsonEqual(String expected, String actual) throws Exception { ObjectMapper om = new ObjectMapper(); - Assertions.assertEquals( - om.readTree(expected), - om.readTree(convert.toJson()) - ); + Assertions.assertEquals(om.readTree(expected), om.readTree(actual)); } -} \ No newline at end of file +} diff --git a/kafka-ui-api/src/test/resources/fileForUploadTest.txt b/kafka-ui-api/src/test/resources/fileForUploadTest.txt new file mode 100644 index 00000000000..cc58280d075 --- /dev/null +++ b/kafka-ui-api/src/test/resources/fileForUploadTest.txt @@ -0,0 +1 @@ +some content goes here diff --git a/kafka-ui-api/src/test/resources/address-book.proto b/kafka-ui-api/src/test/resources/protobuf-serde/address-book.proto similarity index 81% rename from kafka-ui-api/src/test/resources/address-book.proto rename to kafka-ui-api/src/test/resources/protobuf-serde/address-book.proto index 72eab7aab8c..f6c9a5d7880 100644 --- a/kafka-ui-api/src/test/resources/address-book.proto +++ b/kafka-ui-api/src/test/resources/protobuf-serde/address-book.proto @@ -1,16 +1,10 @@ -// [START declaration] syntax = "proto3"; package test; -// [END declaration] - -// [START java_declaration] option java_multiple_files = true; option java_package = "com.example.tutorial.protos"; option java_outer_classname = "AddressBookProtos"; -// [END java_declaration] -// [START messages] message Person { string name = 1; int32 id = 2; // Unique ID number for this person. @@ -31,9 +25,13 @@ message Person { } +message AnotherPerson { + string name = 1; + string surname = 2; +} + // Our address book file is just one of these. message AddressBook { int32 version = 1; repeated Person people = 2; } -// [END messages] \ No newline at end of file diff --git a/kafka-ui-api/src/test/resources/protobuf-serde/lang-description.proto b/kafka-ui-api/src/test/resources/protobuf-serde/lang-description.proto new file mode 100644 index 00000000000..8e213d58c41 --- /dev/null +++ b/kafka-ui-api/src/test/resources/protobuf-serde/lang-description.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package test; + +import "language/language.proto"; +import "google/protobuf/wrappers.proto"; + +message LanguageDescription { + test.lang.Language lang = 1; + google.protobuf.StringValue descr = 2; +} diff --git a/kafka-ui-api/src/test/resources/protobuf-serde/language/language.proto b/kafka-ui-api/src/test/resources/protobuf-serde/language/language.proto new file mode 100644 index 00000000000..7ef30eab236 --- /dev/null +++ b/kafka-ui-api/src/test/resources/protobuf-serde/language/language.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; +package test.lang; + +enum Language { + DE = 0; + EN = 1; + ES = 2; + FR = 3; + PL = 4; + RU = 5; +} diff --git a/kafka-ui-api/src/test/resources/protobuf-serde/sensor.proto b/kafka-ui-api/src/test/resources/protobuf-serde/sensor.proto new file mode 100644 index 00000000000..3bde20a3ae2 --- /dev/null +++ b/kafka-ui-api/src/test/resources/protobuf-serde/sensor.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; +package test; + +message Sensor { + string name = 1; + double temperature = 2; + int32 humidity = 3; + + enum SwitchLevel { + CLOSED = 0; + OPEN = 1; + } + SwitchLevel door = 5; +} diff --git a/kafka-ui-contract/pom.xml b/kafka-ui-contract/pom.xml index 581999c2953..0d8e238368f 100644 --- a/kafka-ui-contract/pom.xml +++ b/kafka-ui-contract/pom.xml @@ -21,28 +21,30 @@ org.springframework.boot spring-boot-starter-webflux - ${spring-boot.version} org.springframework.boot spring-boot-starter-validation - ${spring-boot.version} - io.swagger - swagger-annotations - ${swagger-annotations.version} + io.swagger.core.v3 + swagger-integration-jakarta + 2.2.8 org.openapitools jackson-databind-nullable - ${jackson-databind-nullable.version} + 0.2.4 - com.google.code.findbugs - jsr305 - 3.0.2 - provided + jakarta.annotation + jakarta.annotation-api + 2.1.1 + + + javax.annotation + javax.annotation-api + 1.3.2 @@ -73,6 +75,7 @@ webclient true java8 + true @@ -82,8 +85,7 @@ generate - ${project.basedir}/src/main/resources/swagger/kafka-ui-api.yaml - + ${project.basedir}/src/main/resources/swagger/kafka-ui-api.yaml ${project.build.directory}/generated-sources/api spring DTO @@ -91,14 +93,12 @@ com.provectus.kafka.ui.model com.provectus.kafka.ui.api kafka-ui-contract - true - true true true true - + true java8 @@ -115,15 +115,37 @@ java false false - com.provectus.kafka.ui.connect.model com.provectus.kafka.ui.connect.api kafka-connect-client - true webclient - + true + true + java8 + + + + + generate-sr-client + + generate + + + ${project.basedir}/src/main/resources/swagger/kafka-sr-api.yaml + + ${project.build.directory}/generated-sources/kafka-sr-client + java + false + false + + com.provectus.kafka.ui.sr.model + com.provectus.kafka.ui.sr.api + kafka-sr-client + true + webclient + true true java8 @@ -138,35 +160,36 @@ ../kafka-ui-react-app - ${project.version} + ${project.version} - install node and npm + install node and pnpm - install-node-and-npm + install-node-and-pnpm ${node.version} + ${pnpm.version} - npm install + pnpm install - npm + pnpm install - npm run gen:sources + pnpm gen:sources - npm + pnpm - run gen:sources + gen:sources @@ -174,7 +197,6 @@ org.apache.maven.plugins maven-clean-plugin - ${maven-clean-plugin.version} @@ -186,7 +208,6 @@ org.apache.maven.plugins maven-resources-plugin - ${maven-resources-plugin.version} copy-resource-one diff --git a/kafka-ui-contract/src/main/resources/swagger/kafka-sr-api.yaml b/kafka-ui-contract/src/main/resources/swagger/kafka-sr-api.yaml new file mode 100644 index 00000000000..0320e891ecd --- /dev/null +++ b/kafka-ui-contract/src/main/resources/swagger/kafka-sr-api.yaml @@ -0,0 +1,412 @@ +openapi: 3.0.0 +info: + description: Api Documentation + version: 0.1.0 + title: Api Documentation + termsOfService: urn:tos + contact: {} + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0 +tags: + - name: /schemaregistry +servers: + - url: /localhost + +paths: + /subjects: + get: + tags: + - KafkaSrClient + summary: get all connectors from Kafka Connect service + operationId: getAllSubjectNames + parameters: + - name: subjectPrefix + in: query + required: false + schema: + type: string + - name: deleted + in: query + schema: + type: boolean + responses: + 200: + description: OK + content: + application/json: + schema: + #workaround for https://github.com/spring-projects/spring-framework/issues/24734 + type: string + + /subjects/{subject}: + delete: + tags: + - KafkaSrClient + operationId: deleteAllSubjectVersions + parameters: + - name: subject + in: path + required: true + schema: + type: string + - name: permanent + in: query + schema: + type: boolean + required: false + responses: + 200: + description: OK + 404: + description: Not found + + /subjects/{subject}/versions/{version}: + get: + tags: + - KafkaSrClient + operationId: getSubjectVersion + parameters: + - name: subject + in: path + required: true + schema: + type: string + - name: version + in: path + required: true + schema: + type: string + - name: deleted + in: query + schema: + type: boolean + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/SchemaSubject' + 404: + description: Not found + 422: + description: Invalid version + delete: + tags: + - KafkaSrClient + operationId: deleteSubjectVersion + parameters: + - name: subject + in: path + required: true + schema: + type: string + - name: permanent + in: query + required: false + schema: + type: boolean + default: false + - name: version + in: path + required: true + schema: + type: string + responses: + 200: + description: OK + 404: + description: Not found + + /subjects/{subject}/versions: + get: + tags: + - KafkaSrClient + operationId: getSubjectVersions + parameters: + - name: subject + in: path + required: true + schema: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + type: array + items: + type: integer + format: int32 + 404: + description: Not found + post: + tags: + - KafkaSrClient + operationId: registerNewSchema + parameters: + - name: subject + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/NewSubject' + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/SubjectId' + + /config/: + get: + tags: + - KafkaSrClient + operationId: getGlobalCompatibilityLevel + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CompatibilityConfig' + 404: + description: Not found + put: + tags: + - KafkaSrClient + operationId: updateGlobalCompatibilityLevel + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CompatibilityLevelChange' + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CompatibilityLevelChange' + 404: + description: Not found + + /config/{subject}: + get: + tags: + - KafkaSrClient + operationId: getSubjectCompatibilityLevel + parameters: + - name: subject + in: path + required: true + schema: + type: string + - name: defaultToGlobal + in: query + required: true + schema: + type: boolean + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CompatibilityConfig' + 404: + description: Not found + put: + tags: + - KafkaSrClient + operationId: updateSubjectCompatibilityLevel + parameters: + - name: subject + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CompatibilityLevelChange' + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CompatibilityLevelChange' + 404: + description: Not found + delete: + tags: + - KafkaSrClient + operationId: deleteSubjectCompatibilityLevel + parameters: + - name: subject + in: path + required: true + schema: + type: string + responses: + 200: + description: OK + 404: + description: Not found + + /compatibility/subjects/{subject}/versions/{version}: + post: + tags: + - KafkaSrClient + operationId: checkSchemaCompatibility + parameters: + - name: subject + in: path + required: true + schema: + type: string + - name: version + in: path + required: true + schema: + type: string + - name: verbose + in: query + description: Show reason a schema fails the compatibility test + schema: + type: boolean + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/NewSubject' + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CompatibilityCheckResponse' + 404: + description: Not found + +security: + - basicAuth: [] + +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + schemas: + SchemaSubject: + type: object + properties: + subject: + type: string + version: + type: string + id: + type: integer + schema: + type: string + schemaType: + $ref: '#/components/schemas/SchemaType' + references: + type: array + items: + $ref: '#/components/schemas/SchemaReference' + required: + - id + - subject + - version + - schema + - schemaType + + SchemaType: + type: string + description: upon updating a schema, the type of an existing schema can't be changed + enum: + - AVRO + - JSON + - PROTOBUF + + SchemaReference: + type: object + properties: + name: + type: string + subject: + type: string + version: + type: integer + required: + - name + - subject + - version + + SubjectId: + type: object + properties: + id: + type: integer + + NewSubject: + type: object + description: should be set for creating/updating schema subject + properties: + schema: + type: string + schemaType: + $ref: '#/components/schemas/SchemaType' + references: + type: array + items: + $ref: '#/components/schemas/SchemaReference' + required: + - schema + - schemaType + + CompatibilityConfig: + type: object + properties: + compatibilityLevel: + $ref: '#/components/schemas/Compatibility' + required: + - compatibilityLevel + + CompatibilityLevelChange: + type: object + properties: + compatibility: + $ref: '#/components/schemas/Compatibility' + required: + - compatibility + + + Compatibility: + type: string + enum: + - BACKWARD + - BACKWARD_TRANSITIVE + - FORWARD + - FORWARD_TRANSITIVE + - FULL + - FULL_TRANSITIVE + - NONE + + + CompatibilityCheckResponse: + type: object + properties: + is_compatible: + type: boolean diff --git a/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml b/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml index 1416609982c..ae51d31568f 100644 --- a/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml +++ b/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml @@ -4,10 +4,10 @@ info: version: 0.1.0 title: Api Documentation termsOfService: urn:tos - contact: {} + contact: { } license: name: Apache 2.0 - url: http://www.apache.org/licenses/LICENSE-2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 tags: - name: /api/clusters - name: /api/clusters/connects @@ -363,6 +363,76 @@ paths: 404: description: Not found + /api/clusters/{clusterName}/topics/{topicName}/analysis: + get: + tags: + - Topics + summary: getTopicAnalysis + operationId: getTopicAnalysis + parameters: + - name: clusterName + in: path + required: true + schema: + type: string + - name: topicName + in: path + required: true + schema: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/TopicAnalysis' + 404: + description: Not found + post: + tags: + - Topics + summary: analyzeTopic + operationId: analyzeTopic + parameters: + - name: clusterName + in: path + required: true + schema: + type: string + - name: topicName + in: path + required: true + schema: + type: string + responses: + 200: + description: Analysis started + 404: + description: Not found + delete: + tags: + - Topics + summary: cancelTopicAnalysis + operationId: cancelTopicAnalysis + parameters: + - name: clusterName + in: path + required: true + schema: + type: string + - name: topicName + in: path + required: true + schema: + type: string + responses: + 200: + description: Analysis cancelled + 404: + description: Not found + + /api/clusters/{clusterName}/topics/{topicName}: get: tags: @@ -522,6 +592,58 @@ paths: $ref: '#/components/schemas/ReplicationFactorChangeResponse' 404: description: Not found + 400: + description: Bad Request + + /api/clusters/{clusterName}/topic/{topicName}/serdes: + get: + tags: + - Messages + summary: getSerdes + operationId: getSerdes + parameters: + - name: clusterName + in: path + required: true + schema: + type: string + - name: topicName + in: path + required: true + schema: + type: string + - name: use + in: query + required: true + schema: + $ref: '#/components/schemas/SerdeUsage' + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/TopicSerdeSuggestion' + + /api/smartfilters/testexecutions: + put: + tags: + - Messages + summary: executeSmartFilterTest + operationId: executeSmartFilterTest + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SmartFilterTestExecution' + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/SmartFilterTestExecutionResult' + /api/clusters/{clusterName}/topics/{topicName}/messages: get: @@ -567,6 +689,16 @@ paths: in: query schema: $ref: "#/components/schemas/SeekDirection" + - name: keySerde + in: query + description: "Serde that should be used for deserialization. Will be chosen automatically if not set." + schema: + type: string + - name: valueSerde + in: query + description: "Serde that should be used for deserialization. Will be chosen automatically if not set." + schema: + type: string responses: 200: description: OK @@ -631,12 +763,12 @@ paths: 404: description: Not found - /api/clusters/{clusterName}/topics/{topicName}/messages/schema: + /api/clusters/{clusterName}/topics/{topicName}/activeproducers: get: tags: - - Messages - summary: getTopicSchema - operationId: getTopicSchema + - Topics + summary: get producer states for topic + operationId: getActiveProducerStates parameters: - name: clusterName in: path @@ -654,7 +786,9 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/TopicMessageSchema' + type: array + items: + $ref: '#/components/schemas/TopicProducerState' /api/clusters/{clusterName}/topics/{topicName}/consumer-groups: get: @@ -687,7 +821,7 @@ paths: get: tags: - Consumer Groups - summary: Get consumer croups with paging support + summary: Get consumer groups with paging support operationId: getConsumerGroupsPage parameters: - name: clusterName @@ -774,28 +908,6 @@ paths: 200: description: OK - /api/clusters/{clusterName}/consumer-groups: - get: - tags: - - Consumer Groups - summary: get all ConsumerGroups - operationId: getConsumerGroups - parameters: - - name: clusterName - in: path - required: true - schema: - type: string - responses: - 200: - description: OK - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/ConsumerGroup' - /api/clusters/{clusterName}/consumer-groups/{id}/offsets: post: tags: @@ -826,7 +938,7 @@ paths: post: tags: - Schemas - summary: create a new subject schema + summary: create a new subject schema or update existing subject schema operationId: createNewSchema parameters: - name: clusterName @@ -1179,6 +1291,16 @@ paths: required: false schema: type: string + - name: orderBy + in: query + required: false + schema: + $ref: '#/components/schemas/ConnectorColumnsToSort' + - name: sortOrder + in: query + required: false + schema: + $ref: '#/components/schemas/SortOrder' responses: 200: description: OK @@ -1463,31 +1585,6 @@ paths: 200: description: OK - /api/clusters/{clusterName}/ksql: - description: Deprecated - use ksql/v2 instead! - post: - tags: - - Ksql - summary: executeKsqlCommand - operationId: executeKsqlCommand - parameters: - - name: clusterName - in: path - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/KsqlCommand' - responses: - 200: - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/KsqlCommandResponse' /api/clusters/{clusterName}/ksql/v2: post: @@ -1679,55 +1776,400 @@ paths: 404: description: Not found -components: - schemas: - ErrorResponse: - description: Error object that will be returned with 4XX and 5XX HTTP statuses - type: object - properties: - code: - type: integer - description: Internal error code (can be used for message formatting & localization on UI) - message: - type: string - description: Error message - timestamp: - type: number - description: Response unix timestamp in ms - requestId: - type: string - description: Unique server-defined request id for convenient debugging - fieldsErrors: - type: array - items: - $ref: '#/components/schemas/FieldError' - stackTrace: - type: string - - FieldError: - type: object - properties: - fieldName: - type: string - description: Name of field that violated format - restrictions: - description: Field format violations description (ex. ["size must be between 0 and 20", "must be a well-formed email address"]) - type: array - items: + /api/clusters/{clusterName}/acls: + get: + tags: + - Acls + summary: listKafkaAcls + operationId: listAcls + parameters: + - name: clusterName + in: path + required: true + schema: + type: string + - name: resourceType + in: query + required: false + schema: + $ref: '#/components/schemas/KafkaAclResourceType' + - name: resourceName + in: query + required: false + schema: type: string + - name: namePatternType + in: query + required: false + schema: + $ref: '#/components/schemas/KafkaAclNamePatternType' + responses: + 200: + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/KafkaAcl' - MetricsCollectionError: - type: object - properties: - message: - type: string - stackTrace: - type: string + /api/clusters/{clusterName}/acl/csv: + get: + tags: + - Acls + summary: getAclAsCsv + operationId: getAclAsCsv + parameters: + - name: clusterName + in: path + required: true + schema: + type: string + responses: + 200: + description: OK + content: + text/plain: + schema: + type: string + post: + tags: + - Acls + summary: syncAclsCsv + operationId: syncAclsCsv + parameters: + - name: clusterName + in: path + required: true + schema: + type: string + requestBody: + content: + text/plain: + schema: + type: string + responses: + 200: + description: OK - Cluster: - type: object - properties: - name: + /api/clusters/{clusterName}/acl: + post: + tags: + - Acls + summary: createAcl + operationId: createAcl + parameters: + - name: clusterName + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/KafkaAcl' + responses: + 200: + description: OK + + delete: + tags: + - Acls + summary: deleteAcl + operationId: deleteAcl + parameters: + - name: clusterName + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/KafkaAcl' + responses: + 200: + description: OK + 404: + description: Acl not found + + /api/clusters/{clusterName}/acl/consumer: + post: + tags: + - Acls + summary: createConsumerAcl + operationId: createConsumerAcl + parameters: + - name: clusterName + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateConsumerAcl' + responses: + 200: + description: OK + + /api/clusters/{clusterName}/acl/producer: + post: + tags: + - Acls + summary: createProducerAcl + operationId: createProducerAcl + parameters: + - name: clusterName + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateProducerAcl' + responses: + 200: + description: OK + + /api/clusters/{clusterName}/acl/streamApp: + post: + tags: + - Acls + summary: createStreamAppAcl + operationId: createStreamAppAcl + parameters: + - name: clusterName + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateStreamAppAcl' + responses: + 200: + description: OK + + /api/authorization: + get: + tags: + - Authorization + summary: Get user authentication related info + operationId: getUserAuthInfo + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/AuthenticationInfo' + + /api/info: + get: + tags: + - ApplicationConfig + summary: Gets application info + operationId: getApplicationInfo + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApplicationInfo' + + /api/config: + get: + tags: + - ApplicationConfig + summary: Gets current application configuration + operationId: getCurrentConfig + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApplicationConfig' + put: + tags: + - ApplicationConfig + summary: Restarts application with specified configuration + operationId: restartWithConfig + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RestartRequest' + responses: + 200: + description: OK + + /api/config/validated: + put: + tags: + - ApplicationConfig + summary: Restarts application with specified configuration + operationId: validateConfig + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ApplicationConfig' + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApplicationConfigValidation' + + + /api/config/relatedfiles: + post: + tags: + - ApplicationConfig + summary: Restarts application with specified configuration + operationId: uploadConfigRelatedFile + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UploadedFileInfo' + +components: + schemas: + TopicSerdeSuggestion: + type: object + properties: + key: + type: array + items: + $ref: '#/components/schemas/SerdeDescription' + value: + type: array + items: + $ref: '#/components/schemas/SerdeDescription' + + SerdeDescription: + type: object + properties: + name: + type: string + description: + type: string + preferred: + description: "This serde was automatically chosen by cluster config. This should be enabled in UI by default. Also it will be used for deserialization if no serdes passed." + type: boolean + schema: + type: string + additionalProperties: + type: object + additionalProperties: + type: object + + SerdeUsage: + type: string + enum: + - SERIALIZE + - DESERIALIZE + + ErrorResponse: + description: Error object that will be returned with 4XX and 5XX HTTP statuses + type: object + properties: + code: + type: integer + description: Internal error code (can be used for message formatting & localization on UI) + message: + type: string + description: Error message + timestamp: + type: number + description: Response unix timestamp in ms + requestId: + type: string + description: Unique server-defined request id for convenient debugging + fieldsErrors: + type: array + items: + $ref: '#/components/schemas/FieldError' + stackTrace: + type: string + + FieldError: + type: object + properties: + fieldName: + type: string + description: Name of field that violated format + restrictions: + description: Field format violations description (ex. ["size must be between 0 and 20", "must be a well-formed email address"]) + type: array + items: + type: string + + MetricsCollectionError: + type: object + properties: + message: + type: string + stackTrace: + type: string + + ApplicationInfo: + type: object + properties: + enabledFeatures: + type: array + items: + type: string + enum: + - DYNAMIC_CONFIG + build: + type: object + properties: + commitId: + type: string + version: + type: string + buildTime: + type: string + isLatestRelease: + type: boolean + latestRelease: + type: object + properties: + versionTag: + type: string + publishedAt: + type: string + htmlUrl: + type: string + + Cluster: + type: object + properties: + name: type: string defaultCluster: type: boolean @@ -1758,6 +2200,8 @@ components: - KAFKA_CONNECT - KSQL_DB - TOPIC_DELETION + - KAFKA_ACL_VIEW # get ACLs listing + - KAFKA_ACL_EDIT # create & delete ACLs required: - id - name @@ -1788,6 +2232,7 @@ components: deprecated: true activeControllers: type: integer + description: Id of broker which is cluster's controller. null, if controller not known yet. onlinePartitionCount: type: integer offlinePartitionCount: @@ -1874,6 +2319,14 @@ components: - REPLICATION_FACTOR - SIZE + ConnectorColumnsToSort: + type: string + enum: + - NAME + - CONNECT + - TYPE + - STATUS + SortOrder: type: string enum: @@ -1900,6 +2353,10 @@ components: format: int64 segmentCount: type: integer + bytesInPerSec: + type: number + bytesOutPerSec: + type: number underReplicatedPartitions: type: integer cleanUpPolicy: @@ -1911,6 +2368,130 @@ components: required: - name + TopicAnalysis: + type: object + description: "Represents analysis state. Note: 'progress' and 'result' fields are set exclusively depending on analysis state." + properties: + progress: + $ref: '#/components/schemas/TopicAnalysisProgress' + result: + $ref: '#/components/schemas/TopicAnalysisResult' + + TopicAnalysisProgress: + type: object + properties: + startedAt: + type: integer + format: int64 + completenessPercent: + type: number + msgsScanned: + type: integer + format: int64 + bytesScanned: + type: integer + format: int64 + + TopicAnalysisResult: + type: object + properties: + startedAt: + type: integer + format: int64 + finishedAt: + type: integer + format: int64 + error: + type: string + totalStats: + $ref: '#/components/schemas/TopicAnalysisStats' + partitionStats: + type: array + items: + $ref: "#/components/schemas/TopicAnalysisStats" + + TopicAnalysisStats: + type: object + properties: + partition: + type: integer + format: int32 + description: "null if this is total stats" + totalMsgs: + type: integer + format: int64 + minOffset: + type: integer + format: int64 + maxOffset: + type: integer + format: int64 + minTimestamp: + type: integer + format: int64 + maxTimestamp: + type: integer + format: int64 + nullKeys: + type: integer + format: int64 + nullValues: + type: integer + format: int64 + approxUniqKeys: + type: integer + format: int64 + approxUniqValues: + type: integer + format: int64 + keySize: + $ref: "#/components/schemas/TopicAnalysisSizeStats" + valueSize: + $ref: "#/components/schemas/TopicAnalysisSizeStats" + hourlyMsgCounts: + type: array + items: + type: object + properties: + hourStart: + type: integer + format: int64 + count: + type: integer + format: int64 + + TopicAnalysisSizeStats: + type: object + description: "All sizes in bytes" + properties: + sum: + type: integer + format: int64 + min: + type: integer + format: int64 + max: + type: integer + format: int64 + avg: + type: integer + format: int64 + prctl50: + type: integer + format: int64 + prctl75: + type: integer + format: int64 + prctl95: + type: integer + format: int64 + prctl99: + type: integer + format: int64 + prctl999: + type: integer + format: int64 + Replica: type: object properties: @@ -1953,6 +2534,10 @@ components: type: integer cleanUpPolicy: $ref: '#/components/schemas/CleanUpPolicy' + keySerde: + type: string + valueSerde: + type: string required: - name @@ -1975,6 +2560,8 @@ components: type: array items: $ref: "#/components/schemas/ConfigSynonym" + doc: + type: string required: - name @@ -1994,7 +2581,6 @@ components: required: - name - partitions - - replicationFactor TopicUpdate: type: object @@ -2011,10 +2597,24 @@ components: properties: id: type: integer - host: - type: string - port: + host: + type: string + port: + type: integer + bytesInPerSec: + type: number + bytesOutPerSec: + type: number + partitionsLeader: + type: integer + partitions: type: integer + inSyncPartitions: + type: integer + partitionsSkew: + type: number + leadersSkew: + type: number required: - id @@ -2046,7 +2646,36 @@ components: - PROTOBUF - UNKNOWN + TopicProducerState: + type: object + properties: + partition: + type: integer + format: int32 + producerId: + type: integer + format: int64 + producerEpoch: + type: integer + format: int32 + lastSequence: + type: integer + format: int32 + lastTimestampMs: + type: integer + format: int64 + coordinatorEpoch: + type: integer + format: int32 + currentTransactionStartOffset: + type: integer + format: int64 + ConsumerGroup: + discriminator: + propertyName: inherit + mapping: + details: "#/components/schemas/ConsumerGroupDetails" type: object properties: groupId: @@ -2063,9 +2692,10 @@ components: $ref: "#/components/schemas/ConsumerGroupState" coordinator: $ref: "#/components/schemas/Broker" - messagesBehind: + consumerLag: type: integer format: int64 + description: null if consumer group has no offsets committed required: - groupId @@ -2075,6 +2705,8 @@ components: - NAME - MEMBERS - STATE + - MESSAGES_BEHIND + - TOPIC_NUM ConsumerGroupsPageResponse: type: object @@ -2086,52 +2718,60 @@ components: items: $ref: '#/components/schemas/ConsumerGroup' - CreateTopicMessage: + SmartFilterTestExecution: type: object + required: [filterCode] properties: - partition: - type: integer + filterCode: + type: string key: type: string - nullable: true + value: + type: string headers: type: object additionalProperties: type: string - content: - type: string - nullable: true - required: - - partition + partition: + type: integer + offset: + type: integer + format: int64 + timestampMs: + type: integer + format: int64 - TopicMessageSchema: + SmartFilterTestExecutionResult: type: object properties: - key: - $ref: "#/components/schemas/MessageSchema" - value: - $ref: "#/components/schemas/MessageSchema" - required: - - key - - value + result: + type: boolean + error: + type: string - MessageSchema: + CreateTopicMessage: type: object properties: - name: + partition: + type: integer + key: type: string - source: + nullable: true + headers: + type: object + additionalProperties: + type: string + content: type: string - enum: - - SOURCE_SCHEMA_REGISTRY - - SOURCE_PROTO_FILE - - SOURCE_UNKNOWN - schema: + nullable: true + keySerde: type: string + nullable: true + valueSerde: + type: string + nullable: true required: - - name - - source - - schema + - partition TopicMessageEvent: type: object @@ -2157,6 +2797,12 @@ components: name: type: string + TimeStampFormat: + type: object + properties: + timeStampFormat: + type: string + TopicMessageConsuming: type: object properties: @@ -2170,6 +2816,8 @@ components: type: boolean messagesConsumed: type: integer + filterApplyErrors: + type: integer TopicMessage: @@ -2198,8 +2846,10 @@ components: content: type: string keyFormat: + #deprecated - wont be filled - use 'keySerde' field instead $ref: "#/components/schemas/MessageFormat" valueFormat: + #deprecated - wont be filled - use 'valueSerde' field instead $ref: "#/components/schemas/MessageFormat" keySize: type: integer @@ -2208,12 +2858,26 @@ components: type: integer format: int64 keySchemaId: + deprecated: true + description: deprecated - wont be filled - use 'keyDeserializeProperties' field instead type: string valueSchemaId: + deprecated: true + description: deprecated - wont be filled - use 'valueDeserializeProperties' field instead type: string headersSize: type: integer format: int64 + keySerde: + type: string + valueSerde: + type: string + keyDeserializeProperties: + additionalProperties: + type: object + valueDeserializeProperties: + additionalProperties: + type: object required: - partition - offset @@ -2277,9 +2941,10 @@ components: endOffset: type: integer format: int64 - messagesBehind: + consumerLag: type: integer format: int64 + description: null if consumer group has no offsets committed consumerId: type: string host: @@ -2304,16 +2969,12 @@ components: properties: name: type: string - canonicalName: - type: string - params: + labels: type: string additionalProperties: type: string value: - type: string - additionalProperties: - type: number + type: number TopicLogdirs: type: object @@ -2370,6 +3031,10 @@ components: type: string schemaType: $ref: '#/components/schemas/SchemaType' + references: + type: array + items: + $ref: '#/components/schemas/SchemaReference' required: - id - subject @@ -2380,18 +3045,37 @@ components: NewSchemaSubject: type: object + description: should be set for creating/updating schema subject properties: subject: type: string schema: type: string schemaType: - $ref: '#/components/schemas/SchemaType' + $ref: '#/components/schemas/SchemaType' # upon updating a schema, the type of existing schema can't be changed + references: + type: array + items: + $ref: '#/components/schemas/SchemaReference' required: - subject - schema - schemaType + SchemaReference: + type: object + properties: + name: + type: string + subject: + type: string + version: + type: integer + required: + - name + - subject + - version + CompatibilityLevel: type: object properties: @@ -2410,6 +3094,7 @@ components: SchemaType: type: string + description: upon updating a schema, the type of an existing schema can't be changed enum: - AVRO - JSON @@ -2702,18 +3387,6 @@ components: items: $ref: '#/components/schemas/ConnectorPluginConfig' - KsqlCommand: - type: object - properties: - ksql: - type: string - streamsProperties: - type: object - additionalProperties: - type: string - required: - - ksql - KsqlCommandV2: type: object properties: @@ -2760,31 +3433,6 @@ components: valueFormat: type: string - KsqlCommandResponse: - type: object - properties: - data: - $ref: '#/components/schemas/Table' - message: - type: string - - Table: - type: object - properties: - headers: - type: array - items: - type: string - rows: - type: array - items: - type: array - items: - type: string - required: - - headers - - rows - KsqlResponse: type: object properties: @@ -2858,7 +3506,6 @@ components: properties: totalReplicationFactor: type: integer - minimum: 1 required: - totalReplicationFactor @@ -2931,3 +3578,514 @@ components: - COMPACT - COMPACT_DELETE - UNKNOWN + + AuthenticationInfo: + type: object + properties: + rbacEnabled: + type: boolean + description: true if role based access control is enabled and granular permission access is required + userInfo: + $ref: '#/components/schemas/UserInfo' + required: + - rbacEnabled + + UserInfo: + type: object + properties: + username: + type: string + permissions: + type: array + items: + $ref: '#/components/schemas/UserPermission' + required: + - username + - permissions + + UserPermission: + type: object + properties: + clusters: + type: array + items: + type: string + resource: + $ref: '#/components/schemas/ResourceType' + value: + type: string + actions: + type: array + items: + $ref: '#/components/schemas/Action' + required: + - clusters + - resource + - actions + + Action: + type: string + enum: + - VIEW + - EDIT + - CREATE + - DELETE + - RESET_OFFSETS + - EXECUTE + - MODIFY_GLOBAL_COMPATIBILITY + - ANALYSIS_VIEW + - ANALYSIS_RUN + - MESSAGES_READ + - MESSAGES_PRODUCE + - MESSAGES_DELETE + - RESTART + + ResourceType: + type: string + enum: + - APPLICATIONCONFIG + - CLUSTERCONFIG + - TOPIC + - CONSUMER + - SCHEMA + - CONNECT + - KSQL + - ACL + - AUDIT + + KafkaAcl: + type: object + required: [resourceType, resourceName, namePatternType, principal, host, operation, permission] + properties: + resourceType: + $ref: '#/components/schemas/KafkaAclResourceType' + resourceName: + type: string # "*" if acl can be applied to any resource of given type + namePatternType: + $ref: '#/components/schemas/KafkaAclNamePatternType' + principal: + type: string + host: + type: string + operation: + type: string + enum: + - UNKNOWN # Unknown operation, need to update mapping code on BE + - ALL # Cluster, Topic, Group + - READ # Topic, Group + - WRITE # Topic, TransactionalId + - CREATE # Cluster, Topic + - DELETE # Topic, Group + - ALTER # Cluster, Topic, + - DESCRIBE # Cluster, Topic, Group, TransactionalId, DelegationToken + - CLUSTER_ACTION # Cluster + - DESCRIBE_CONFIGS # Cluster, Topic + - ALTER_CONFIGS # Cluster, Topic + - IDEMPOTENT_WRITE # Cluster + - CREATE_TOKENS + - DESCRIBE_TOKENS + permission: + type: string + enum: + - ALLOW + - DENY + + CreateConsumerAcl: + type: object + required: [principal, host] + properties: + principal: + type: string + host: + type: string + topics: + type: array + items: + type: string + topicsPrefix: + type: string + consumerGroups: + type: array + items: + type: string + consumerGroupsPrefix: + type: string + + CreateProducerAcl: + type: object + required: [principal, host] + properties: + principal: + type: string + host: + type: string + topics: + type: array + items: + type: string + topicsPrefix: + type: string + transactionalId: + type: string + transactionsIdPrefix: + type: string + idempotent: + type: boolean + default: false + + CreateStreamAppAcl: + type: object + required: [principal, host, applicationId, inputTopics, outputTopics] + properties: + principal: + type: string + host: + type: string + inputTopics: + type: array + items: + type: string + outputTopics: + type: array + items: + type: string + applicationId: + nullable: false + type: string + + KafkaAclResourceType: + type: string + enum: + - UNKNOWN # Unknown operation, need to update mapping code on BE + - TOPIC + - GROUP + - CLUSTER + - TRANSACTIONAL_ID + - DELEGATION_TOKEN + - USER + + KafkaAclNamePatternType: + type: string + enum: + - MATCH + - LITERAL + - PREFIXED + + RestartRequest: + type: object + properties: + config: + $ref: '#/components/schemas/ApplicationConfig' + + UploadedFileInfo: + type: object + required: [location] + properties: + location: + type: string + + ApplicationConfigValidation: + type: object + properties: + clusters: + type: object + additionalProperties: + $ref: '#/components/schemas/ClusterConfigValidation' + + ApplicationPropertyValidation: + type: object + required: [error] + properties: + error: + type: boolean + errorMessage: + type: string + description: Contains error message if error = true + + ClusterConfigValidation: + type: object + required: [kafka] + properties: + kafka: + $ref: '#/components/schemas/ApplicationPropertyValidation' + schemaRegistry: + $ref: '#/components/schemas/ApplicationPropertyValidation' + kafkaConnects: + type: object + additionalProperties: + $ref: '#/components/schemas/ApplicationPropertyValidation' + ksqldb: + $ref: '#/components/schemas/ApplicationPropertyValidation' + + ApplicationConfig: + type: object + properties: + properties: + type: object + properties: + auth: + type: object + properties: + type: + type: string + oauth2: + type: object + properties: + client: + type: object + additionalProperties: + type: object + properties: + provider: + type: string + clientId: + type: string + clientSecret: + type: string + clientName: + type: string + redirectUri: + type: string + authorizationGrantType: + type: string + issuerUri: + type: string + authorizationUri: + type: string + tokenUri: + type: string + userInfoUri: + type: string + jwkSetUri: + type: string + userNameAttribute: + type: string + scope: + type: array + items: + type: string + customParams: + type: object + additionalProperties: + type: string + rbac: + type: object + properties: + roles: + type: array + items: + type: object + properties: + name: + type: string + clusters: + type: array + items: + type: string + subjects: + type: array + items: + type: object + properties: + provider: + type: string + type: + type: string + value: + type: string + permissions: + type: array + items: + type: object + properties: + resource: + $ref: '#/components/schemas/ResourceType' + value: + type: string + actions: + type: array + items: + $ref: '#/components/schemas/Action' + webclient: + type: object + properties: + maxInMemoryBufferSize: + type: string + description: "examples: 20, 12KB, 5MB" + kafka: + type: object + properties: + polling: + type: object + properties: + pollTimeoutMs: + type: integer + maxPageSize: + type: integer + defaultPageSize: + type: integer + adminClientTimeout: + type: integer + internalTopicPrefix: + type: string + clusters: + type: array + items: + type: object + properties: + name: + type: string + bootstrapServers: + type: string + ssl: + type: object + properties: + truststoreLocation: + type: string + truststorePassword: + type: string + schemaRegistry: + type: string + schemaRegistryAuth: + type: object + properties: + username: + type: string + password: + type: string + schemaRegistrySsl: + type: object + properties: + keystoreLocation: + type: string + keystorePassword: + type: string + ksqldbServer: + type: string + ksqldbServerSsl: + type: object + properties: + keystoreLocation: + type: string + keystorePassword: + type: string + ksqldbServerAuth: + type: object + properties: + username: + type: string + password: + type: string + kafkaConnect: + type: array + items: + type: object + properties: + name: + type: string + address: + type: string + username: + type: string + password: + type: string + keystoreLocation: + type: string + keystorePassword: + type: string + + metrics: + type: object + properties: + type: + type: string + port: + type: integer + format: int32 + ssl: + type: boolean + username: + type: string + password: + type: string + keystoreLocation: + type: string + keystorePassword: + type: string + properties: + type: object + additionalProperties: true + readOnly: + type: boolean + disableLogDirsCollection: + type: boolean + serde: + type: array + items: + type: object + properties: + name: + type: string + className: + type: string + filePath: + type: string + properties: + type: object + additionalProperties: true + topicKeysPattern: + type: string + topicValuesPattern: + type: string + defaultKeySerde: + type: string + defaultValueSerde: + type: string + masking: + type: array + items: + type: object + properties: + type: + type: string + enum: + - REMOVE + - MASK + - REPLACE + fields: + type: array + items: + type: string + fieldsNamePattern: + type: string + maskingCharsReplacement: + type: array + items: + type: string + replacement: + type: string + topicKeysPattern: + type: string + topicValuesPattern: + type: string + pollingThrottleRate: + type: integer + format: int64 + audit: + type: object + properties: + level: + type: string + enum: [ "ALL", "ALTER_ONLY" ] + topic: + type: string + auditTopicsPartitions: + type: integer + topicAuditEnabled: + type: boolean + consoleAuditEnabled: + type: boolean + auditTopicProperties: + type: object + additionalProperties: + type: string diff --git a/kafka-ui-e2e-checks/.env.example b/kafka-ui-e2e-checks/.env.example deleted file mode 100644 index de43f7b2dc0..00000000000 --- a/kafka-ui-e2e-checks/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -USE_LOCAL_BROWSER=true -SHOULD_START_SELENOID=false -TURN_OFF_SCREENSHOTS=true diff --git a/kafka-ui-e2e-checks/QASE.md b/kafka-ui-e2e-checks/QASE.md new file mode 100644 index 00000000000..b09731515a3 --- /dev/null +++ b/kafka-ui-e2e-checks/QASE.md @@ -0,0 +1,70 @@ +### E2E integration with Qase.io TMS (for internal users) + +### Table of Contents + +- [Intro](#intro) +- [Set up Qase.io integration](#set-up-qase-integration) +- [Test case creation](#test-case-creation) +- [Test run reporting](#test-run-reporting) + +### Intro + +We're using [Qase.io](https://help.qase.io/en/) as TMS to keep test cases and accumulate test runs. +Integration is set up through API using [qase-api](https://mvnrepository.com/artifact/io.qase/qase-api) +and [qase-testng](https://mvnrepository.com/artifact/io.qase/qase-testng) libraries. + +### Set up Qase integration + +To set up integration locally add next VM option `-DQASEIO_API_TOKEN='%s'` +(add your [Qase token](https://app.qase.io/user/api/token) instead of '%s') into your run configuration + +### Test case creation + +All new test cases can be added into TMS by default if they have no QaseId and QaseTitle matching already existing +cases. +But to handle `@Suite` and `@Automation` we added custom QaseCreateListener. To create new test case for next sync with +Qase (see example `kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/Template.java`): + +1. Create new class in `kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/suit` +2. Inherit it from `kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/BaseQaseTest.java` +3. Create new test method with some name inside the class and annotate it with: + +- `@Automation` (optional - Not automated by default) - to set one of automation states: NOT_AUTOMATED, TO_BE_AUTOMATED, + AUTOMATED +- `@QaseTitle` (required) - to set title for new test case and to check is there no existing cases with same title in + Qase.io +- `@Status` (optional - Draft by default) - to set one of case statuses: ACTUAL, DRAFT, DEPRECATED +- `@Suite` (optional) - to store new case in some existing package need to set its id, otherwise case will be stored in + the root +- `@Test` (required) - annotation from TestNG to specify this method as test + +4. Create new private void step methods with some name inside the same class and annotate it with + @io.qase.api.annotation.Step to specify this method as step. +5. Use defined step methods inside created test method in concrete order +6. If there are any additional cases to create you can repeat scenario in a new class +7. There are two ways to sync newly created cases in the framework with Qase.io: + +- sync can be performed locally - run new test classes with + already [set up Qase.io integration](#Set up Qase.io integration) +- also you can commit and push your changes, then + run [E2E Manual suite](https://github.com/provectus/kafka-ui/actions/workflows/e2e-manual.yml) on your branch + +8. No test run in Qase.io will be created, new test case will be stored defined directory + in [project's repository](https://app.qase.io/project/KAFKAUI) +9. To add expected results into created test case edit in Qase.io manually + +### Test run reporting + +To handle manual test cases with status `Skipped` we added custom QaseResultListener. To create new test run: + +1. All test methods should be annotated with actual `@QaseId` +2. There are two ways to sync newly created cases in the framework with Qase.io: + +- run can be performed locally - run test classes (or suites) with + already [set up Qase.io integration](#Set up Qase.io integration), they will be labeled as `Automation CUSTOM suite` +- also you can commit and push your changes, then + run [E2E Automation suite](https://github.com/provectus/kafka-ui/actions/workflows/e2e-automation.yml) on your branch + +3. All new test runs will be added into [project's test runs](https://app.qase.io/run/KAFKAUI) with corresponding label + using QaseId to identify existing cases +4. All test cases from manual suite are set up to have `Skipped` status in test runs to perform them manually diff --git a/kafka-ui-e2e-checks/README.md b/kafka-ui-e2e-checks/README.md index 25be125c30b..a33b92739e4 100644 --- a/kafka-ui-e2e-checks/README.md +++ b/kafka-ui-e2e-checks/README.md @@ -1,13 +1,13 @@ ### E2E UI automation for Kafka-ui -This repository is for E2E UI automation. +This repository is for E2E UI automation. ### Table of Contents - [Prerequisites](#prerequisites) - [How to install](#how-to-install) -- [Environment variables](#environment-variables) - [How to run checks](#how-to-run-checks) +- [Qase.io integration (for internal users)](#qase-integration) - [Reporting](#reporting) - [Environments setup](#environments-setup) - [Test Data](#test-data) @@ -17,101 +17,92 @@ This repository is for E2E UI automation. - [How to develop](#how-to-develop) ### Prerequisites + - Docker & Docker-compose -- Java +- Java (install aarch64 jdk if you have M1/arm chip) - Maven - + ### How to install + ``` git clone https://github.com/provectus/kafka-ui.git cd kafka-ui-e2e-checks -docker pull selenoid/vnc:chrome_86.0 +docker pull selenoid/vnc_chrome:103.0 ``` -### Environment variables - -|Name | Default | Description -|---------------------------------------|-------------|--------------------- -|`USE_LOCAL_BROWSER` | `true` | clear reports dir on startup -|`CLEAR_REPORTS_DIR` | `true` | clear reports dir on startup -|`SHOULD_START_SELENOID` | `false` | starts selenoid container on startup -|`SELENOID_URL` | `http://localhost:4444/wd/hub` | URL of remote selenoid instance -|`BASE_URL` | `http://192.168.1.2:8080/` | base url for selenide configuration -|`PIXELS_THRESHOLD` | `200` | Amount of pixels, that should be different to fail screenshot check -|`SCREENSHOTS_FOLDER` | `screenshots/` | folder for keeping reference screenshots -|`DIFF_SCREENSHOTS_FOLDER` | `build/__diff__/` | folder for keeping screenshots diffs -|`ACTUAL_SCREENSHOTS_FOLDER` | `build/__actual__/` | folder for keeping actual screenshots(during checks) -|`SHOULD_SAVE_SCREENSHOTS_IF_NOT_EXIST` | `true` | folder for keeping actual screenshots(during checks) -|`TURN_OFF_SCREENSHOTS` | `false` | If true, `compareScreenshots` will not fail on different screenshots. Useful for functional debugging on local machine, while preserving golden screenshots made in selenoid ### How to run checks -1. Run `kafka-ui` -``` -cd docker -docker-compose -f kafka-ui.yaml up -d -``` -2. Run `selenoid-ui` +1. Run `kafka-ui`: + ``` -cd kafka-ui-e2e-checks/docker -docker-compose -f selenoid.yaml up -d +cd kafka-ui +docker-compose -f kafka-ui-e2e-checks/docker/selenoid-local.yaml up -d +docker-compose -f documentation/compose/e2e-tests.yaml up -d ``` -3. Run checks + +2. To run test suite select its name (options: regression, sanity, smoke) and put it instead %s into command below + ``` -cd kafka-ui-e2e-checks -mvn test +./mvnw -Dsurefire.suiteXmlFiles='src/test/resources/%s.xml' -f 'kafka-ui-e2e-checks' test -Pprod ``` -* There are several ways to run checks +3. To run tests on your local Chrome browser just add next VM option to the Run Configuration -1. If you don't have selenoid run on your machine ``` - mvn test -DSHOULD_START_SELENOID=true +-Dbrowser=local ``` -⚠️ If you want to run checks in IDE with this approach, you'd need to set up -environment variable(`SHOULD_START_SELENOID=true`) in `Run/Edit Configurations..` -2. For development purposes it is better to just start separate selenoid in docker-compose -Do it in separate window +Expected Location of Chrome ``` -cd docker -docker-compose -f selenoid.yaml up +Linux: /usr/bin/google-chrome1 +Mac: /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome +Windows XP: %HOMEPATH%\Local Settings\Application Data\Google\Chrome\Application\chrome.exe +Windows Vista and newer: C:\Users%USERNAME%\AppData\Local\Google\Chrome\Application\chrome.exe ``` -Then you can just `mvn test`. By default, `SELENOID_URL` will resolve to `http://localhost:4444/wd/hub` - -It's preferred way to run. - -* If you have remote selenoid instance, set -`SELENOID_URL` environment variable +### Qase integration -Example: -`mvn test -DSELENOID_URL=http://localhost:4444/wd/hub` -That's the way to run tests in CI with selenoid set up somewhere in cloud +Found instruction for Qase.io integration (for internal use only) at `kafka-ui-e2e-checks/QASE.md` ### Reporting Reports are in `allure-results` folder. -If you have installed allure commandline(e.g. like [here](https://docs.qameta.io/allure/#_installing_a_commandline) or [here](https://www.npmjs.com/package/allure-commandline)) +If you have installed allure commandline [here](https://www.npmjs.com/package/allure-commandline)) You can see allure report with command: + ``` allure serve ``` + ### Screenshots Reference screenshots are in `SCREENSHOTS_FOLDER` (default,`kafka-ui-e2e-checks/screenshots`) ### How to develop -> ⚠️ todo + +> ⚠️ todo + ### Setting for different environments -> ⚠️ todo + +> ⚠️ todo + ### Test Data -> ⚠️ todo + +> ⚠️ todo + ### Actions -> ⚠️ todo + +> ⚠️ todo + ### Checks -> ⚠️ todo + +> ⚠️ todo + ### Parallelization -> ⚠️ todo + +> ⚠️ todo + ### Tips - - install `Selenium UI Testing plugin` in IDEA + +- install `Selenium UI Testing plugin` in IDEA diff --git a/kafka-ui-e2e-checks/docker/selenoid-git.yaml b/kafka-ui-e2e-checks/docker/selenoid-git.yaml new file mode 100644 index 00000000000..f4c5430f16a --- /dev/null +++ b/kafka-ui-e2e-checks/docker/selenoid-git.yaml @@ -0,0 +1,33 @@ +--- +version: '3' + +services: + + selenoid: + network_mode: bridge + image: aerokube/selenoid:1.10.7 + volumes: + - "../selenoid/config:/etc/selenoid" + - "/var/run/docker.sock:/var/run/docker.sock" + - "../selenoid/video:/opt/selenoid/video" + - "../selenoid/logs:/opt/selenoid/logs" + environment: + - OVERRIDE_VIDEO_OUTPUT_DIR=../selenoid/video + command: [ "-conf", "/etc/selenoid/browsersGit.json", "-video-output-dir", "/opt/selenoid/video", "-log-output-dir", "/opt/selenoid/logs" ] + ports: + - "4444:4444" + + selenoid-ui: + network_mode: bridge + image: aerokube/selenoid-ui:latest-release + links: + - selenoid + ports: + - "8081:8080" + command: [ "--selenoid-uri", "http://selenoid:4444" ] + + selenoid-chrome: + network_mode: bridge + image: selenoid/vnc_chrome:103.0 + extra_hosts: + - "host.docker.internal:host-gateway" diff --git a/kafka-ui-e2e-checks/docker/selenoid-local.yaml b/kafka-ui-e2e-checks/docker/selenoid-local.yaml new file mode 100644 index 00000000000..9d7fb8e0bec --- /dev/null +++ b/kafka-ui-e2e-checks/docker/selenoid-local.yaml @@ -0,0 +1,33 @@ +--- +version: '3' + +services: + + selenoid: + network_mode: bridge + image: aerokube/selenoid:1.10.7 + volumes: + - "../selenoid/config:/etc/selenoid" + - "/var/run/docker.sock:/var/run/docker.sock" + - "../selenoid/video:/opt/selenoid/video" + - "../selenoid/logs:/opt/selenoid/logs" + environment: + - OVERRIDE_VIDEO_OUTPUT_DIR=../selenoid/video + command: [ "-conf", "/etc/selenoid/browsersLocal.json", "-video-output-dir", "/opt/selenoid/video", "-log-output-dir", "/opt/selenoid/logs" ] + ports: + - "4444:4444" + + selenoid-ui: + network_mode: bridge + image: aerokube/selenoid-ui:latest-release + links: + - selenoid + ports: + - "8081:8080" + command: [ "--selenoid-uri", "http://selenoid:4444" ] + + selenoid-chrome: + network_mode: bridge + image: selenoid/vnc_chrome:103.0 + extra_hosts: + - "host.docker.internal:host-gateway" diff --git a/kafka-ui-e2e-checks/docker/selenoid.yaml b/kafka-ui-e2e-checks/docker/selenoid.yaml deleted file mode 100644 index 5e9bc170b26..00000000000 --- a/kafka-ui-e2e-checks/docker/selenoid.yaml +++ /dev/null @@ -1,25 +0,0 @@ -version: '3' - -services: - selenoid: - network_mode: bridge - image: aerokube/selenoid:1.10.3 - volumes: - - "../selenoid/config:/etc/selenoid" - - "/var/run/docker.sock:/var/run/docker.sock" - - "../selenoid/video:/video" - - "../selenoid/logs:/opt/selenoid/logs" - environment: - - OVERRIDE_VIDEO_OUTPUT_DIR=video - command: [ "-conf", "/etc/selenoid/browsers.json", "-video-output-dir", "/opt/selenoid/video", "-log-output-dir", "/opt/selenoid/logs" ] - ports: - - "4444:4444" - - selenoid-ui: - network_mode: bridge - image: aerokube/selenoid-ui:latest-release - links: - - selenoid - ports: - - "8081:8080" - command: [ "--selenoid-uri", "http://selenoid:4444" ] diff --git a/kafka-ui-e2e-checks/pom.xml b/kafka-ui-e2e-checks/pom.xml index 5c74d9b28d5..22c17bf9609 100644 --- a/kafka-ui-e2e-checks/pom.xml +++ b/kafka-ui-e2e-checks/pom.xml @@ -1,46 +1,34 @@ - kafka-ui com.provectus 0.0.1-SNAPSHOT - 4.0.0 + 4.0.0 kafka-ui-e2e-checks + + 3.0.0-M8 ${project.version} - 5.8.2 - 1.9.8 - 2.17.1 - 1.3.3 - 1.15.2 - 5.16.2 - 3.17.1 - 1.0-rc7 + 1.17.6 + 5.2.1 + 4.8.1 + 6.12.3 + 7.7.1 + 2.23.0 + 3.0.5 + 1.9.9.1 + 3.24.2 2.2 - 1.7.32 - 1.15.1 - 2.13.6 - 2.2.0 - 1.6.2 - 2.6 - 1.5.4 - 2.17.2 - 2.22.2 - 2.10.0 - 3.0.0 - 4.1.77.Final + 2.0.7 + 3.3.1 - - net.minidev - json-smart - ${json-smart.version} - org.apache.kafka kafka_2.13 @@ -88,42 +76,39 @@ io.netty netty-buffer - ${netty.version} io.netty netty-common - ${netty.version} io.netty netty-codec - ${netty.version} io.netty netty-handler - ${netty.version} io.netty netty-resolver - ${netty.version} io.netty netty-transport - ${netty.version} io.netty netty-transport-native-epoll - ${netty.version} io.netty netty-transport-native-unix-common - ${netty.version} + + + io.netty + netty-resolver-dns-native-macos + osx-aarch_64 @@ -131,110 +116,90 @@ testcontainers ${testcontainers.version} - - - io.qameta.allure - allure-junit5 - ${allure.version} - - com.codeborne - selenide - ${selenide.version} - - - io.qameta.allure - allure-selenide - ${allure.version} + org.testcontainers + selenium + ${testcontainers.version} - org.hamcrest - hamcrest - ${hamcrest.version} + org.projectlombok + lombok + ${org.projectlombok.version} - org.assertj - assertj-core - ${assertj.version} + org.apache.httpcomponents.core5 + httpcore5 + ${httpcomponents.version} - com.google.auto.service - auto-service - ${google.auto-service.version} + org.apache.httpcomponents.client5 + httpclient5 + ${httpcomponents.version} - org.junit.jupiter - junit-jupiter-api - ${junit.version} + org.seleniumhq.selenium + selenium-http-jdk-client + ${selenium.version} - org.junit.jupiter - junit-jupiter-engine - ${junit.version} - test + org.seleniumhq.selenium + selenium-http + ${selenium.version} - org.slf4j - slf4j-simple - ${slf4j.version} + com.codeborne + selenide + ${selenide.version} - org.projectlombok - lombok - ${org.projectlombok.version} + org.testng + testng + ${testng.version} - org.aspectj - aspectjrt - ${aspectj.version} + io.qameta.allure + allure-selenide + ${allure.version} - - org.testcontainers - junit-jupiter - ${testcontainers.junit-jupiter.version} + io.qameta.allure + allure-testng + ${allure.version} - io.qameta.allure - allure-java-commons - ${allure.java-commons.version} + io.qase + qase-testng + ${qase.io.version} - io.github.cdimascio - dotenv-java - ${dotenv.version} + io.qase + qase-api + ${qase.io.version} - org.junit.platform - junit-platform-launcher - ${junit.platform-launcher.version} + org.hamcrest + hamcrest + ${hamcrest.version} - ru.yandex.qatools.allure - allure-maven-plugin - ${allure.maven-plugin.version} + org.assertj + assertj-core + ${assertj.version} - ru.yandex.qatools.ashot - ashot - ${ashot.version} - - - org.seleniumhq.selenium - selenium-remote-driver - - + org.aspectj + aspectjrt + ${aspectj.version} - io.qameta.allure.plugins - screen-diff-plugin - ${allure.screendiff-plugin.version} + org.slf4j + slf4j-simple + ${slf4j.version} com.provectus kafka-ui-contract ${kafka-ui-contract} - test @@ -250,18 +215,20 @@ org.apache.maven.plugins maven-surefire-plugin - ${maven.surefire-plugin.version} true + + + org.apache.maven.surefire + surefire-testng + ${maven.surefire-plugin.version} + + org.apache.maven.plugins maven-compiler-plugin - - ${maven.compiler.source} - ${maven.compiler.target} - @@ -280,6 +247,11 @@ + + org.apache.maven.surefire + surefire-testng + ${maven.surefire-plugin.version} + org.aspectj aspectjweaver @@ -290,16 +262,39 @@ io.qameta.allure allure-maven - ${allure-maven.version} + 2.10.0 org.apache.maven.plugins - maven-compiler-plugin - - ${maven.compiler.source} - ${maven.compiler.target} - + maven-checkstyle-plugin + 3.3.0 + + + com.puppycrawl.tools + checkstyle + 10.3.1 + + + + + checkstyle + validate + + check + + + warning + true + true + true + file:${basedir}/../etc/checkstyle/checkstyle-e2e.xml + file:${basedir}/../etc/checkstyle/apache-header.txt + + + + + diff --git a/kafka-ui-e2e-checks/screenshots/main.png b/kafka-ui-e2e-checks/screenshots/main.png deleted file mode 100644 index 694cc5cb1d5..00000000000 Binary files a/kafka-ui-e2e-checks/screenshots/main.png and /dev/null differ diff --git a/kafka-ui-e2e-checks/selenoid/config/browsers.json b/kafka-ui-e2e-checks/selenoid/config/browsers.json deleted file mode 100644 index f22de7eca11..00000000000 --- a/kafka-ui-e2e-checks/selenoid/config/browsers.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "chrome": { - "default": "86.0", - "versions": { - "86.0": { - "hosts": ["host.docker.internal:172.17.0.1"], - "image": "selenoid/vnc:chrome_86.0", - "port": "4444" - } - } - } -} diff --git a/kafka-ui-e2e-checks/selenoid/config/browsersGit.json b/kafka-ui-e2e-checks/selenoid/config/browsersGit.json new file mode 100644 index 00000000000..9e01861615e --- /dev/null +++ b/kafka-ui-e2e-checks/selenoid/config/browsersGit.json @@ -0,0 +1,15 @@ +{ + "chrome": { + "default": "103.0", + "versions": { + "103.0": { + "image": "selenoid/vnc_chrome:103.0", + "hosts": [ + "host.docker.internal:172.17.0.1" + ], + "port": "4444", + "path": "/" + } + } + } +} diff --git a/kafka-ui-e2e-checks/selenoid/config/browsersLocal.json b/kafka-ui-e2e-checks/selenoid/config/browsersLocal.json new file mode 100644 index 00000000000..35a494f33dc --- /dev/null +++ b/kafka-ui-e2e-checks/selenoid/config/browsersLocal.json @@ -0,0 +1,12 @@ +{ + "chrome": { + "default": "103.0", + "versions": { + "103.0": { + "image": "selenoid/vnc_chrome:103.0", + "port": "4444", + "path": "/" + } + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Connector.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Connector.java new file mode 100644 index 00000000000..493010a3f69 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Connector.java @@ -0,0 +1,11 @@ +package com.provectus.kafka.ui.models; + +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +public class Connector { + + private String name, config; +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Schema.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Schema.java new file mode 100644 index 00000000000..1d5cbb7e802 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Schema.java @@ -0,0 +1,36 @@ +package com.provectus.kafka.ui.models; + +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; + +import com.provectus.kafka.ui.api.model.SchemaType; +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +public class Schema { + + private static final String USER_DIR = "user.dir"; + + private String name, valuePath; + private SchemaType type; + + public static Schema createSchemaAvro() { + return new Schema().setName("schema_avro-" + randomAlphabetic(5)) + .setType(SchemaType.AVRO) + .setValuePath(System.getProperty(USER_DIR) + "/src/main/resources/testData/schemas/schema_avro_value.json"); + } + + public static Schema createSchemaJson() { + return new Schema().setName("schema_json-" + randomAlphabetic(5)) + .setType(SchemaType.JSON) + .setValuePath(System.getProperty(USER_DIR) + "/src/main/resources/testData/schemas/schema_json_Value.json"); + } + + public static Schema createSchemaProtobuf() { + return new Schema().setName("schema_protobuf-" + randomAlphabetic(5)) + .setType(SchemaType.PROTOBUF) + .setValuePath( + System.getProperty(USER_DIR) + "/src/main/resources/testData/schemas/schema_protobuf_value.txt"); + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Topic.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Topic.java new file mode 100644 index 00000000000..8fb3df086ea --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Topic.java @@ -0,0 +1,20 @@ +package com.provectus.kafka.ui.models; + +import com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue; +import com.provectus.kafka.ui.pages.topics.enums.CustomParameterType; +import com.provectus.kafka.ui.pages.topics.enums.MaxSizeOnDisk; +import com.provectus.kafka.ui.pages.topics.enums.TimeToRetain; +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +public class Topic { + + private String name, timeToRetainData, maxMessageBytes, messageKey, messageValue, customParameterValue; + private int numberOfPartitions; + private CustomParameterType customParameterType; + private CleanupPolicyValue cleanupPolicyValue; + private MaxSizeOnDisk maxSizeOnDisk; + private TimeToRetain timeToRetain; +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/BasePage.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/BasePage.java new file mode 100644 index 00000000000..8d0841b4407 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/BasePage.java @@ -0,0 +1,159 @@ +package com.provectus.kafka.ui.pages; + +import static com.codeborne.selenide.Selenide.$$x; +import static com.codeborne.selenide.Selenide.$x; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.ElementsCollection; +import com.codeborne.selenide.SelenideElement; +import com.codeborne.selenide.WebDriverRunner; +import com.provectus.kafka.ui.pages.panels.enums.MenuItem; +import com.provectus.kafka.ui.utilities.WebUtils; +import java.time.Duration; +import lombok.extern.slf4j.Slf4j; +import org.openqa.selenium.Keys; +import org.openqa.selenium.interactions.Actions; + +@Slf4j +public abstract class BasePage extends WebUtils { + + protected SelenideElement loadingSpinner = $x("//div[@role='progressbar']"); + protected SelenideElement submitBtn = $x("//button[@type='submit']"); + protected SelenideElement tableGrid = $x("//table"); + protected SelenideElement searchFld = $x("//input[@type='text'][contains(@id, ':r')]"); + protected SelenideElement dotMenuBtn = $x("//button[@aria-label='Dropdown Toggle']"); + protected SelenideElement alertHeader = $x("//div[@role='alert']//div[@role='heading']"); + protected SelenideElement alertMessage = $x("//div[@role='alert']//div[@role='contentinfo']"); + protected SelenideElement confirmationMdl = $x("//div[text()= 'Confirm the action']/.."); + protected SelenideElement confirmBtn = $x("//button[contains(text(),'Confirm')]"); + protected SelenideElement cancelBtn = $x("//button[contains(text(),'Cancel')]"); + protected SelenideElement backBtn = $x("//button[contains(text(),'Back')]"); + protected SelenideElement previousBtn = $x("//button[contains(text(),'Previous')]"); + protected SelenideElement nextBtn = $x("//button[contains(text(),'Next')]"); + protected ElementsCollection ddlOptions = $$x("//li[@value]"); + protected ElementsCollection gridItems = $$x("//tr[@class]"); + protected String summaryCellLocator = "//div[contains(text(),'%s')]"; + protected String tableElementNameLocator = "//tbody//a[contains(text(),'%s')]"; + protected String columnHeaderLocator = "//table//tr/th//div[text()='%s']"; + protected String pageTitleFromHeader = "//h1[text()='%s']"; + protected String pagePathFromHeader = "//a[text()='%s']/../h1"; + + protected boolean isSpinnerVisible(int... timeoutInSeconds) { + return isVisible(loadingSpinner, timeoutInSeconds); + } + + protected void waitUntilSpinnerDisappear(int... timeoutInSeconds) { + log.debug("\nwaitUntilSpinnerDisappear"); + if (isSpinnerVisible(timeoutInSeconds)) { + loadingSpinner.shouldBe(Condition.disappear, Duration.ofSeconds(60)); + } + } + + protected void searchItem(String tag) { + log.debug("\nsearchItem: {}", tag); + sendKeysAfterClear(searchFld, tag); + searchFld.pressEnter().shouldHave(Condition.value(tag)); + waitUntilSpinnerDisappear(1); + } + + protected SelenideElement getPageTitleFromHeader(MenuItem menuItem) { + return $x(String.format(pageTitleFromHeader, menuItem.getPageTitle())); + } + + protected SelenideElement getPagePathFromHeader(MenuItem menuItem) { + return $x(String.format(pagePathFromHeader, menuItem.getPageTitle())); + } + + protected void clickSubmitBtn() { + clickByJavaScript(submitBtn); + } + + protected void clickNextBtn() { + clickByJavaScript(nextBtn); + } + + protected void clickBackBtn() { + clickByJavaScript(backBtn); + } + + protected void clickPreviousBtn() { + clickByJavaScript(previousBtn); + } + + protected void setJsonInputValue(SelenideElement jsonInput, String jsonConfig) { + sendKeysByActions(jsonInput, jsonConfig.replace(" ", "")); + new Actions(WebDriverRunner.getWebDriver()) + .keyDown(Keys.SHIFT) + .sendKeys(Keys.PAGE_DOWN) + .keyUp(Keys.SHIFT) + .sendKeys(Keys.DELETE) + .perform(); + } + + protected SelenideElement getTableElement(String elementName) { + log.debug("\ngetTableElement: {}", elementName); + return $x(String.format(tableElementNameLocator, elementName)); + } + + protected ElementsCollection getDdlOptions() { + return ddlOptions; + } + + protected String getAlertHeader() { + log.debug("\ngetAlertHeader"); + String result = alertHeader.shouldBe(Condition.visible).getText(); + log.debug("-> {}", result); + return result; + } + + protected String getAlertMessage() { + log.debug("\ngetAlertMessage"); + String result = alertMessage.shouldBe(Condition.visible).getText(); + log.debug("-> {}", result); + return result; + } + + protected boolean isAlertVisible(AlertHeader header) { + log.debug("\nisAlertVisible: {}", header.toString()); + boolean result = getAlertHeader().equals(header.toString()); + log.debug("-> {}", result); + return result; + } + + protected boolean isAlertVisible(AlertHeader header, String message) { + log.debug("\nisAlertVisible: {} {}", header, message); + boolean result = isAlertVisible(header) && getAlertMessage().equals(message); + log.debug("-> {}", result); + return result; + } + + protected void clickConfirmButton() { + confirmBtn.shouldBe(Condition.enabled).click(); + confirmBtn.shouldBe(Condition.disappear); + } + + protected void clickCancelButton() { + cancelBtn.shouldBe(Condition.enabled).click(); + cancelBtn.shouldBe(Condition.disappear); + } + + protected boolean isConfirmationModalVisible() { + return isVisible(confirmationMdl); + } + + public enum AlertHeader { + SUCCESS("Success"), + VALIDATION_ERROR("Validation Error"), + BAD_REQUEST("400 Bad Request"); + + private final String value; + + AlertHeader(String value) { + this.value = value; + } + + public String toString() { + return value; + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersConfigTab.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersConfigTab.java new file mode 100644 index 00000000000..e00e938297a --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersConfigTab.java @@ -0,0 +1,170 @@ +package com.provectus.kafka.ui.pages.brokers; + +import static com.codeborne.selenide.Selenide.$$x; +import static com.codeborne.selenide.Selenide.$x; + +import com.codeborne.selenide.CollectionCondition; +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.ElementsCollection; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class BrokersConfigTab extends BasePage { + + protected List editBtn = $$x("//button[@aria-label='editAction']"); + protected SelenideElement searchByKeyField = $x("//input[@placeholder='Search by Key or Value']"); + protected SelenideElement sourceInfoIcon = $x("//div[text()='Source']/..//div/div[@class]"); + protected SelenideElement sourceInfoTooltip = $x("//div[text()='Source']/..//div/div[@style]"); + protected ElementsCollection editBtns = $$x("//button[@aria-label='editAction']"); + + @Step + public BrokersConfigTab waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + searchFld.shouldBe(Condition.visible); + return this; + } + + @Step + public BrokersConfigTab hoverOnSourceInfoIcon() { + sourceInfoIcon.shouldBe(Condition.visible).hover(); + return this; + } + + @Step + public String getSourceInfoTooltipText() { + return sourceInfoTooltip.shouldBe(Condition.visible).getText().trim(); + } + + @Step + public boolean isSearchByKeyVisible() { + return isVisible(searchFld); + } + + @Step + public BrokersConfigTab searchConfig(String key) { + searchItem(key); + return this; + } + + public List getColumnHeaders() { + return Stream.of("Key", "Value", "Source") + .map(name -> $x(String.format(columnHeaderLocator, name))) + .collect(Collectors.toList()); + } + + public List getEditButtons() { + return editBtns; + } + + @Step + public BrokersConfigTab clickNextButton() { + clickNextBtn(); + waitUntilSpinnerDisappear(1); + return this; + } + + @Step + public BrokersConfigTab clickPreviousButton() { + clickPreviousBtn(); + waitUntilSpinnerDisappear(1); + return this; + } + + private List initGridItems() { + List gridItemList = new ArrayList<>(); + gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new BrokersConfigTab.BrokersConfigItem(item))); + return gridItemList; + } + + @Step + public BrokersConfigTab.BrokersConfigItem getConfig(String key) { + return initGridItems().stream() + .filter(e -> e.getKey().equals(key)) + .findFirst().orElseThrow(); + } + + @Step + public List getAllConfigs() { + return initGridItems(); + } + + public static class BrokersConfigItem extends BasePage { + + private final SelenideElement element; + + public BrokersConfigItem(SelenideElement element) { + this.element = element; + } + + @Step + public String getKey() { + return element.$x("./td[1]").getText().trim(); + } + + @Step + public String getValue() { + return element.$x("./td[2]//span").getText().trim(); + } + + @Step + public BrokersConfigItem setValue(String value) { + sendKeysAfterClear(getValueFld(), value); + return this; + } + + @Step + public SelenideElement getValueFld() { + return element.$x("./td[2]//input"); + } + + @Step + public SelenideElement getSaveBtn() { + return element.$x("./td[2]//button[@aria-label='confirmAction']"); + } + + @Step + public SelenideElement getCancelBtn() { + return element.$x("./td[2]//button[@aria-label='cancelAction']"); + } + + @Step + public SelenideElement getEditBtn() { + return element.$x("./td[2]//button[@aria-label='editAction']"); + } + + @Step + public BrokersConfigItem clickSaveBtn() { + getSaveBtn().shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public BrokersConfigItem clickCancelBtn() { + getCancelBtn().shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public BrokersConfigItem clickEditBtn() { + getEditBtn().shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public String getSource() { + return element.$x("./td[3]").getText().trim(); + } + + @Step + public BrokersConfigItem clickConfirm() { + clickConfirmButton(); + return this; + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersDetails.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersDetails.java new file mode 100644 index 00000000000..05d571116ea --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersDetails.java @@ -0,0 +1,86 @@ +package com.provectus.kafka.ui.pages.brokers; + +import static com.codeborne.selenide.Selenide.$x; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class BrokersDetails extends BasePage { + + protected String brokersTabLocator = "//a[text()='%s']"; + + @Step + public BrokersDetails waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + $x(String.format(brokersTabLocator, DetailsTab.LOG_DIRECTORIES)).shouldBe(Condition.visible); + return this; + } + + @Step + public BrokersDetails openDetailsTab(DetailsTab menu) { + $x(String.format(brokersTabLocator, menu.toString())).shouldBe(Condition.enabled).click(); + waitUntilSpinnerDisappear(); + return this; + } + + private List getVisibleColumnHeaders() { + return Stream.of("Name", "Topics", "Error", "Partitions") + .map(name -> $x(String.format(columnHeaderLocator, name))) + .collect(Collectors.toList()); + } + + private List getEnabledColumnHeaders() { + return Stream.of("Name", "Error") + .map(name -> $x(String.format(columnHeaderLocator, name))) + .collect(Collectors.toList()); + } + + private List getVisibleSummaryCells() { + return Stream.of("Segment Size", "Segment Count", "Port", "Host") + .map(name -> $x(String.format(summaryCellLocator, name))) + .collect(Collectors.toList()); + } + + private List getDetailsTabs() { + return Stream.of(DetailsTab.values()) + .map(name -> $x(String.format(brokersTabLocator, name))) + .collect(Collectors.toList()); + } + + @Step + public List getAllEnabledElements() { + List enabledElements = new ArrayList<>(getEnabledColumnHeaders()); + enabledElements.addAll(getDetailsTabs()); + return enabledElements; + } + + @Step + public List getAllVisibleElements() { + List visibleElements = new ArrayList<>(getVisibleSummaryCells()); + visibleElements.addAll(getVisibleColumnHeaders()); + visibleElements.addAll(getDetailsTabs()); + return visibleElements; + } + + public enum DetailsTab { + LOG_DIRECTORIES("Log directories"), + CONFIGS("Configs"), + METRICS("Metrics"); + + private final String value; + + DetailsTab(String value) { + this.value = value; + } + + public String toString() { + return value; + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersList.java new file mode 100644 index 00000000000..9e0741fe747 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersList.java @@ -0,0 +1,123 @@ +package com.provectus.kafka.ui.pages.brokers; + +import static com.codeborne.selenide.Selenide.$x; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.BROKERS; + +import com.codeborne.selenide.CollectionCondition; +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class BrokersList extends BasePage { + + @Step + public BrokersList waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + getPageTitleFromHeader(BROKERS).shouldBe(Condition.visible); + return this; + } + + @Step + public BrokersList openBroker(int brokerId) { + getBroker(brokerId).openItem(); + return this; + } + + private List getUptimeSummaryCells() { + return Stream.of("Broker Count", "Active Controller", "Version") + .map(name -> $x(String.format(summaryCellLocator, name))) + .collect(Collectors.toList()); + } + + private List getPartitionsSummaryCells() { + return Stream.of("Online", "URP", "In Sync Replicas", "Out Of Sync Replicas") + .map(name -> $x(String.format(summaryCellLocator, name))) + .collect(Collectors.toList()); + } + + @Step + public List getAllVisibleElements() { + List visibleElements = new ArrayList<>(getUptimeSummaryCells()); + visibleElements.addAll(getPartitionsSummaryCells()); + return visibleElements; + } + + private List getEnabledColumnHeaders() { + return Stream.of("Broker ID", "Disk usage", "Partitions skew", + "Leaders", "Leader skew", "Online partitions", "Port", "Host") + .map(name -> $x(String.format(columnHeaderLocator, name))) + .collect(Collectors.toList()); + } + + @Step + public List getAllEnabledElements() { + return getEnabledColumnHeaders(); + } + + private List initGridItems() { + List gridItemList = new ArrayList<>(); + gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new BrokersGridItem(item))); + return gridItemList; + } + + @Step + public BrokersGridItem getBroker(int id) { + return initGridItems().stream() + .filter(e -> e.getId() == id) + .findFirst().orElseThrow(); + } + + @Step + public List getAllBrokers() { + return initGridItems(); + } + + public static class BrokersGridItem extends BasePage { + + private final SelenideElement element; + + public BrokersGridItem(SelenideElement element) { + this.element = element; + } + + private SelenideElement getIdElm() { + return element.$x("./td[1]/div/a"); + } + + @Step + public int getId() { + return Integer.parseInt(getIdElm().getText().trim()); + } + + @Step + public void openItem() { + getIdElm().click(); + } + + @Step + public int getSegmentSize() { + return Integer.parseInt(element.$x("./td[2]").getText().trim()); + } + + @Step + public int getSegmentCount() { + return Integer.parseInt(element.$x("./td[3]").getText().trim()); + } + + @Step + public int getPort() { + return Integer.parseInt(element.$x("./td[4]").getText().trim()); + } + + @Step + public String getHost() { + return element.$x("./td[5]").getText().trim(); + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorCreateForm.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorCreateForm.java new file mode 100644 index 00000000000..0b6b7b5608b --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorCreateForm.java @@ -0,0 +1,49 @@ +package com.provectus.kafka.ui.pages.connectors; + +import static com.codeborne.selenide.Selenide.$x; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; + +public class ConnectorCreateForm extends BasePage { + + protected SelenideElement nameField = $x("//input[@name='name']"); + protected SelenideElement contentTextArea = $x("//textarea[@class='ace_text-input']"); + protected SelenideElement configField = $x("//div[@id='config']"); + + @Step + public ConnectorCreateForm waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + nameField.shouldBe(Condition.visible); + return this; + } + + @Step + public ConnectorCreateForm setName(String connectName) { + nameField.shouldBe(Condition.enabled).setValue(connectName); + return this; + } + + @Step + public ConnectorCreateForm setConfig(String configJson) { + configField.shouldBe(Condition.enabled).click(); + setJsonInputValue(contentTextArea, configJson); + return this; + } + + @Step + public ConnectorCreateForm setConnectorDetails(String connectName, String configJson) { + setName(connectName); + setConfig(configJson); + return this; + } + + @Step + public ConnectorCreateForm clickSubmitButton() { + clickSubmitBtn(); + waitUntilSpinnerDisappear(); + return this; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorDetails.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorDetails.java new file mode 100644 index 00000000000..de74f67e1cc --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorDetails.java @@ -0,0 +1,84 @@ +package com.provectus.kafka.ui.pages.connectors; + +import static com.codeborne.selenide.Selenide.$x; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; + +public class ConnectorDetails extends BasePage { + + protected SelenideElement deleteBtn = $x("//li/div[contains(text(),'Delete')]"); + protected SelenideElement confirmBtnMdl = $x("//div[@role='dialog']//button[contains(text(),'Confirm')]"); + protected SelenideElement contentTextArea = $x("//textarea[@class='ace_text-input']"); + protected SelenideElement taskTab = $x("//a[contains(text(),'Tasks')]"); + protected SelenideElement configTab = $x("//a[contains(text(),'Config')]"); + protected SelenideElement configField = $x("//div[@id='config']"); + protected String connectorHeaderLocator = "//h1[contains(text(),'%s')]"; + + @Step + public ConnectorDetails waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + dotMenuBtn.shouldBe(Condition.visible); + return this; + } + + @Step + public ConnectorDetails openConfigTab() { + clickByJavaScript(configTab); + return this; + } + + @Step + public ConnectorDetails setConfig(String configJson) { + configField.shouldBe(Condition.enabled).click(); + clearByKeyboard(contentTextArea); + contentTextArea.setValue(configJson); + configField.shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public ConnectorDetails clickSubmitButton() { + clickSubmitBtn(); + return this; + } + + @Step + public ConnectorDetails openDotMenu() { + clickByJavaScript(dotMenuBtn); + return this; + } + + @Step + public ConnectorDetails clickDeleteBtn() { + clickByJavaScript(deleteBtn); + return this; + } + + @Step + public ConnectorDetails clickConfirmBtn() { + confirmBtnMdl.shouldBe(Condition.enabled).click(); + confirmBtnMdl.shouldBe(Condition.disappear); + return this; + } + + @Step + public ConnectorDetails deleteConnector() { + openDotMenu(); + clickDeleteBtn(); + clickConfirmBtn(); + return this; + } + + @Step + public boolean isConnectorHeaderVisible(String connectorName) { + return isVisible($x(String.format(connectorHeaderLocator, connectorName))); + } + + @Step + public boolean isAlertWithMessageVisible(AlertHeader header, String message) { + return isAlertVisible(header, message); + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/KafkaConnectList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/KafkaConnectList.java new file mode 100644 index 00000000000..e4b0d94e642 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/KafkaConnectList.java @@ -0,0 +1,44 @@ +package com.provectus.kafka.ui.pages.connectors; + +import static com.codeborne.selenide.Selenide.$x; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KAFKA_CONNECT; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; + + +public class KafkaConnectList extends BasePage { + + protected SelenideElement createConnectorBtn = $x("//button[contains(text(),'Create Connector')]"); + + public KafkaConnectList() { + tableElementNameLocator = "//tbody//td[contains(text(),'%s')]"; + } + + @Step + public KafkaConnectList waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + getPageTitleFromHeader(KAFKA_CONNECT).shouldBe(Condition.visible); + return this; + } + + @Step + public KafkaConnectList clickCreateConnectorBtn() { + clickByJavaScript(createConnectorBtn); + return this; + } + + @Step + public KafkaConnectList openConnector(String connectorName) { + getTableElement(connectorName).shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public boolean isConnectorVisible(String connectorName) { + tableGrid.shouldBe(Condition.visible); + return isVisible(getTableElement(connectorName)); + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersDetails.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersDetails.java new file mode 100644 index 00000000000..46025927e53 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersDetails.java @@ -0,0 +1,31 @@ +package com.provectus.kafka.ui.pages.consumers; + +import static com.codeborne.selenide.Selenide.$x; + +import com.codeborne.selenide.Condition; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; + +public class ConsumersDetails extends BasePage { + + protected String consumerIdHeaderLocator = "//h1[contains(text(),'%s')]"; + protected String topicElementLocator = "//tbody//td//a[text()='%s']"; + + @Step + public ConsumersDetails waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + tableGrid.shouldBe(Condition.visible); + return this; + } + + @Step + public boolean isRedirectedConsumerTitleVisible(String consumerGroupId) { + return isVisible($x(String.format(consumerIdHeaderLocator, consumerGroupId))); + } + + @Step + public boolean isTopicInConsumersDetailsVisible(String topicName) { + tableGrid.shouldBe(Condition.visible); + return isVisible($x(String.format(topicElementLocator, topicName))); + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersList.java new file mode 100644 index 00000000000..bc10b8f2387 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersList.java @@ -0,0 +1,17 @@ +package com.provectus.kafka.ui.pages.consumers; + +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.CONSUMERS; + +import com.codeborne.selenide.Condition; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; + +public class ConsumersList extends BasePage { + + @Step + public ConsumersList waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + getPageTitleFromHeader(CONSUMERS).shouldBe(Condition.visible); + return this; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlDbList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlDbList.java new file mode 100644 index 00000000000..98980cef4d1 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlDbList.java @@ -0,0 +1,169 @@ +package com.provectus.kafka.ui.pages.ksqldb; + +import static com.codeborne.selenide.Condition.visible; +import static com.codeborne.selenide.Selenide.$; +import static com.codeborne.selenide.Selenide.$x; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KSQL_DB; + +import com.codeborne.selenide.CollectionCondition; +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import com.provectus.kafka.ui.pages.ksqldb.enums.KsqlMenuTabs; +import io.qameta.allure.Step; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import org.openqa.selenium.By; + +public class KsqlDbList extends BasePage { + protected SelenideElement executeKsqlBtn = $x("//button[text()='Execute KSQL Request']"); + protected SelenideElement tablesTab = $x("//nav[@role='navigation']/a[text()='Tables']"); + protected SelenideElement streamsTab = $x("//nav[@role='navigation']/a[text()='Streams']"); + + @Step + public KsqlDbList waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + getPageTitleFromHeader(KSQL_DB).shouldBe(Condition.visible); + return this; + } + + @Step + public KsqlDbList clickExecuteKsqlRequestBtn() { + clickByJavaScript(executeKsqlBtn); + return this; + } + + @Step + public KsqlDbList openDetailsTab(KsqlMenuTabs menu) { + $(By.linkText(menu.toString())).shouldBe(Condition.visible).click(); + waitUntilSpinnerDisappear(); + return this; + } + + private List initTablesItems() { + List gridItemList = new ArrayList<>(); + gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new KsqlDbList.KsqlTablesGridItem(item))); + return gridItemList; + } + + @Step + public KsqlDbList.KsqlTablesGridItem getTableByName(String tableName) { + return initTablesItems().stream() + .filter(e -> e.getTableName().equals(tableName)) + .findFirst().orElseThrow(); + } + + private List initStreamsItems() { + List gridItemList = new ArrayList<>(); + gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new KsqlDbList.KsqlStreamsGridItem(item))); + return gridItemList; + } + + @Step + public KsqlDbList.KsqlStreamsGridItem getStreamByName(String streamName) { + return initStreamsItems().stream() + .filter(e -> e.getStreamName().equals(streamName)) + .findFirst().orElseThrow(); + } + + public static class KsqlTablesGridItem extends BasePage { + + private final SelenideElement element; + + public KsqlTablesGridItem(SelenideElement element) { + this.element = element; + } + + private SelenideElement getNameElm() { + return element.$x("./td[1]"); + } + + @Step + public String getTableName() { + return getNameElm().getText().trim(); + } + + @Step + public boolean isVisible() { + boolean isVisible = false; + try { + getNameElm().shouldBe(visible, Duration.ofMillis(500)); + isVisible = true; + } catch (Throwable ignored) { + } + return isVisible; + } + + @Step + public String getTopicName() { + return element.$x("./td[2]").getText().trim(); + } + + @Step + public String getKeyFormat() { + return element.$x("./td[3]").getText().trim(); + } + + @Step + public String getValueFormat() { + return element.$x("./td[4]").getText().trim(); + } + + @Step + public String getIsWindowed() { + return element.$x("./td[5]").getText().trim(); + } + } + + public static class KsqlStreamsGridItem extends BasePage { + + private final SelenideElement element; + + public KsqlStreamsGridItem(SelenideElement element) { + this.element = element; + } + + private SelenideElement getNameElm() { + return element.$x("./td[1]"); + } + + @Step + public String getStreamName() { + return getNameElm().getText().trim(); + } + + @Step + public boolean isVisible() { + boolean isVisible = false; + try { + getNameElm().shouldBe(visible, Duration.ofMillis(500)); + isVisible = true; + } catch (Throwable ignored) { + } + return isVisible; + } + + @Step + public String getTopicName() { + return element.$x("./td[2]").getText().trim(); + } + + @Step + public String getKeyFormat() { + return element.$x("./td[3]").getText().trim(); + } + + @Step + public String getValueFormat() { + return element.$x("./td[4]").getText().trim(); + } + + @Step + public String getIsWindowed() { + return element.$x("./td[5]").getText().trim(); + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlQueryForm.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlQueryForm.java new file mode 100644 index 00000000000..6c4126089bd --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlQueryForm.java @@ -0,0 +1,179 @@ +package com.provectus.kafka.ui.pages.ksqldb; + +import static com.codeborne.selenide.Condition.visible; +import static com.codeborne.selenide.Selenide.$$x; +import static com.codeborne.selenide.Selenide.$x; +import static com.codeborne.selenide.Selenide.sleep; + +import com.codeborne.selenide.CollectionCondition; +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.ElementsCollection; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +public class KsqlQueryForm extends BasePage { + protected SelenideElement clearBtn = $x("//div/button[text()='Clear']"); + protected SelenideElement executeBtn = $x("//div/button[text()='Execute']"); + protected SelenideElement clearResultsBtn = $x("//div/button[text()='Clear results']"); + protected SelenideElement addStreamPropertyBtn = $x("//button[text()='Add Stream Property']"); + protected SelenideElement queryAreaValue = $x("//div[@class='ace_content']"); + protected SelenideElement queryArea = $x("//div[@id='ksql']/textarea[@class='ace_text-input']"); + protected SelenideElement abortButton = $x("//div[@role='status']/div[text()='Abort']"); + protected SelenideElement cancelledAlert = $x("//div[@role='status'][text()='Cancelled']"); + protected ElementsCollection ksqlGridItems = $$x("//tbody//tr"); + protected ElementsCollection keyField = $$x("//input[@aria-label='key']"); + protected ElementsCollection valueField = $$x("//input[@aria-label='value']"); + + @Step + public KsqlQueryForm waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + executeBtn.shouldBe(Condition.visible); + return this; + } + + @Step + public KsqlQueryForm clickClearBtn() { + clickByJavaScript(clearBtn); + sleep(500); + return this; + } + + @Step + public String getEnteredQuery() { + return queryAreaValue.getText().trim(); + } + + @Step + public KsqlQueryForm clickExecuteBtn(String query) { + clickByActions(executeBtn); + if (query.contains("EMIT CHANGES")) { + abortButton.shouldBe(Condition.visible); + } else { + waitUntilSpinnerDisappear(); + } + return this; + } + + @Step + public boolean isAbortBtnVisible() { + return isVisible(abortButton); + } + + @Step + public KsqlQueryForm clickAbortBtn() { + clickByActions(abortButton); + return this; + } + + @Step + public boolean isCancelledAlertVisible() { + return isVisible(cancelledAlert); + } + + @Step + public boolean isClearResultsBtnEnabled() { + return isEnabled(clearResultsBtn); + } + + @Step + public KsqlQueryForm clickClearResultsBtn() { + clickByActions(clearResultsBtn); + waitUntilSpinnerDisappear(); + return this; + } + + @Step + public KsqlQueryForm clickAddStreamProperty() { + clickByActions(addStreamPropertyBtn); + return this; + } + + @Step + public KsqlQueryForm setQuery(String query) { + queryAreaValue.shouldBe(Condition.visible).click(); + sendKeysByActions(queryArea, query); + return this; + } + + @Step + public KsqlQueryForm.KsqlResponseGridItem getItemByName(String name) { + return initItems().stream() + .filter(e -> e.getName().equalsIgnoreCase(name)) + .findFirst().orElseThrow(); + } + + @Step + public boolean areResultsVisible() { + boolean visible = false; + try { + visible = initItems().size() > 0; + } catch (Throwable ignored) { + } + return visible; + } + + private List initItems() { + List gridItemList = new ArrayList<>(); + ksqlGridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new KsqlQueryForm.KsqlResponseGridItem(item))); + return gridItemList; + } + + public static class KsqlResponseGridItem extends BasePage { + + private final SelenideElement element; + + private KsqlResponseGridItem(SelenideElement element) { + this.element = element; + } + + @Step + public String getType() { + return element.$x("./td[1]").getText().trim(); + } + + private SelenideElement getNameElm() { + return element.$x("./td[2]"); + } + + @Step + public String getName() { + return getNameElm().scrollTo().getText().trim(); + } + + @Step + public boolean isVisible() { + boolean isVisible = false; + try { + getNameElm().shouldBe(visible, Duration.ofMillis(500)); + isVisible = true; + } catch (Throwable ignored) { + } + return isVisible; + } + + @Step + public String getTopic() { + return element.$x("./td[3]").getText().trim(); + } + + @Step + public String getKeyFormat() { + return element.$x("./td[4]").getText().trim(); + } + + @Step + public String getValueFormat() { + return element.$x("./td[5]").getText().trim(); + } + + @Step + public String getIsWindowed() { + return element.$x("./td[6]").getText().trim(); + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/enums/KsqlMenuTabs.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/enums/KsqlMenuTabs.java new file mode 100644 index 00000000000..016246edb91 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/enums/KsqlMenuTabs.java @@ -0,0 +1,17 @@ +package com.provectus.kafka.ui.pages.ksqldb.enums; + +public enum KsqlMenuTabs { + + TABLES("Table"), + STREAMS("Streams"); + + private final String value; + + KsqlMenuTabs(String value) { + this.value = value; + } + + public String toString() { + return value; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/enums/KsqlQueryConfig.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/enums/KsqlQueryConfig.java new file mode 100644 index 00000000000..d3cf0ddec26 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/enums/KsqlQueryConfig.java @@ -0,0 +1,18 @@ +package com.provectus.kafka.ui.pages.ksqldb.enums; + +public enum KsqlQueryConfig { + + SHOW_TABLES("show tables;"), + SHOW_STREAMS("show streams;"), + SELECT_ALL_FROM("SELECT * FROM %s\n" + "EMIT CHANGES;"); + + private final String query; + + KsqlQueryConfig(String query) { + this.query = query; + } + + public String getQuery() { + return query; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/models/Stream.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/models/Stream.java new file mode 100644 index 00000000000..3583a243665 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/models/Stream.java @@ -0,0 +1,11 @@ +package com.provectus.kafka.ui.pages.ksqldb.models; + +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +public class Stream { + + private String name, topicName, valueFormat, partitions; +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/models/Table.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/models/Table.java new file mode 100644 index 00000000000..96b3d88ba07 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/models/Table.java @@ -0,0 +1,11 @@ +package com.provectus.kafka.ui.pages.ksqldb.models; + +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +public class Table { + + private String name, streamName; +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/NaviSideBar.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/NaviSideBar.java new file mode 100644 index 00000000000..ea3cc6ecd10 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/NaviSideBar.java @@ -0,0 +1,63 @@ +package com.provectus.kafka.ui.pages.panels; + +import static com.codeborne.selenide.Selenide.$x; +import static com.provectus.kafka.ui.settings.BaseSource.CLUSTER_NAME; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import com.provectus.kafka.ui.pages.panels.enums.MenuItem; +import io.qameta.allure.Step; +import java.time.Duration; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class NaviSideBar extends BasePage { + + protected SelenideElement dashboardMenuItem = $x("//a[@title='Dashboard']"); + protected String sideMenuOptionElementLocator = ".//ul/li[contains(.,'%s')]"; + protected String clusterElementLocator = "//aside/ul/li[contains(.,'%s')]"; + + private SelenideElement expandCluster(String clusterName) { + SelenideElement clusterElement = $x(String.format(clusterElementLocator, clusterName)).shouldBe(Condition.visible); + if (clusterElement.parent().$$x(".//ul").size() == 0) { + clickByActions(clusterElement); + } + return clusterElement; + } + + @Step + public NaviSideBar waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + dashboardMenuItem.shouldBe(Condition.visible, Duration.ofSeconds(30)); + return this; + } + + @Step + public String getPagePath(MenuItem menuItem) { + return getPagePathFromHeader(menuItem) + .shouldBe(Condition.visible) + .getText().trim(); + } + + @Step + public NaviSideBar openSideMenu(String clusterName, MenuItem menuItem) { + clickByActions(expandCluster(clusterName).parent() + .$x(String.format(sideMenuOptionElementLocator, menuItem.getNaviTitle()))); + return this; + } + + @Step + public NaviSideBar openSideMenu(MenuItem menuItem) { + openSideMenu(CLUSTER_NAME, menuItem); + return this; + } + + public List getAllMenuButtons() { + expandCluster(CLUSTER_NAME); + return Stream.of(MenuItem.values()) + .map(menuItem -> $x(String.format(sideMenuOptionElementLocator, menuItem.getNaviTitle()))) + .collect(Collectors.toList()); + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/TopPanel.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/TopPanel.java new file mode 100644 index 00000000000..805e5b1ee11 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/TopPanel.java @@ -0,0 +1,25 @@ +package com.provectus.kafka.ui.pages.panels; + +import static com.codeborne.selenide.Selenide.$x; + +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import java.util.Arrays; +import java.util.List; + +public class TopPanel extends BasePage { + + protected SelenideElement kafkaLogo = $x("//a[contains(text(),'UI for Apache Kafka')]"); + protected SelenideElement kafkaVersion = $x("//a[@title='Current commit']"); + protected SelenideElement logOutBtn = $x("//button[contains(text(),'Log out')]"); + protected SelenideElement gitBtn = $x("//a[@href='https://github.com/provectus/kafka-ui']"); + protected SelenideElement discordBtn = $x("//a[contains(@href,'https://discord.com/invite')]"); + + public List getAllVisibleElements() { + return Arrays.asList(kafkaLogo, kafkaVersion, gitBtn, discordBtn); + } + + public List getAllEnabledElements() { + return Arrays.asList(gitBtn, discordBtn, kafkaLogo); + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/enums/MenuItem.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/enums/MenuItem.java new file mode 100644 index 00000000000..993d6070a02 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/enums/MenuItem.java @@ -0,0 +1,28 @@ +package com.provectus.kafka.ui.pages.panels.enums; + +public enum MenuItem { + + DASHBOARD("Dashboard", "Dashboard"), + BROKERS("Brokers", "Brokers"), + TOPICS("Topics", "Topics"), + CONSUMERS("Consumers", "Consumers"), + SCHEMA_REGISTRY("Schema Registry", "Schema Registry"), + KAFKA_CONNECT("Kafka Connect", "Connectors"), + KSQL_DB("KSQL DB", "KSQL DB"); + + private final String naviTitle; + private final String pageTitle; + + MenuItem(String naviTitle, String pageTitle) { + this.naviTitle = naviTitle; + this.pageTitle = pageTitle; + } + + public String getNaviTitle() { + return naviTitle; + } + + public String getPageTitle() { + return pageTitle; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaCreateForm.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaCreateForm.java new file mode 100644 index 00000000000..374bb427496 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaCreateForm.java @@ -0,0 +1,141 @@ +package com.provectus.kafka.ui.pages.schemas; + +import static com.codeborne.selenide.Selenide.$; +import static com.codeborne.selenide.Selenide.$$x; +import static com.codeborne.selenide.Selenide.$x; +import static org.openqa.selenium.By.id; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.codeborne.selenide.WebDriverRunner; +import com.provectus.kafka.ui.api.model.CompatibilityLevel; +import com.provectus.kafka.ui.api.model.SchemaType; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.openqa.selenium.Keys; +import org.openqa.selenium.interactions.Actions; + +public class SchemaCreateForm extends BasePage { + + protected SelenideElement schemaNameField = $x("//input[@name='subject']"); + protected SelenideElement pageTitle = $x("//h1['Edit']"); + protected SelenideElement schemaTextArea = $x("//textarea[@name='schema']"); + protected SelenideElement newSchemaInput = $("#newSchema [wrap]"); + protected SelenideElement schemaTypeDdl = $x("//ul[@name='schemaType']"); + protected SelenideElement compatibilityLevelList = $x("//ul[@name='compatibilityLevel']"); + protected SelenideElement newSchemaTextArea = $x("//div[@id='newSchema']"); + protected SelenideElement latestSchemaTextArea = $x("//div[@id='latestSchema']"); + protected SelenideElement leftVersionDdl = $(id("left-select")); + protected SelenideElement rightVersionDdl = $(id("right-select")); + protected List visibleMarkers = + $$x("//div[@class='ace_scroller']//div[contains(@class,'codeMarker')]"); + protected List elementsCompareVersionDdl = $$x("//ul[@role='listbox']/ul/li"); + protected String ddlElementLocator = "//li[@value='%s']"; + + @Step + public SchemaCreateForm waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + pageTitle.shouldBe(Condition.visible); + return this; + } + + @Step + public SchemaCreateForm setSubjectName(String name) { + schemaNameField.setValue(name); + return this; + } + + @Step + public SchemaCreateForm setSchemaField(String text) { + schemaTextArea.setValue(text); + return this; + } + + @Step + public SchemaCreateForm selectSchemaTypeFromDropdown(SchemaType schemaType) { + schemaTypeDdl.shouldBe(Condition.enabled).click(); + $x(String.format(ddlElementLocator, schemaType.getValue())).shouldBe(Condition.visible).click(); + return this; + } + + @Step + public SchemaCreateForm clickSubmitButton() { + clickSubmitBtn(); + return this; + } + + @Step + public SchemaCreateForm selectCompatibilityLevelFromDropdown(CompatibilityLevel.CompatibilityEnum level) { + compatibilityLevelList.shouldBe(Condition.enabled).click(); + $x(String.format(ddlElementLocator, level.getValue())).shouldBe(Condition.visible).click(); + return this; + } + + @Step + public SchemaCreateForm openLeftVersionDdl() { + leftVersionDdl.shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public SchemaCreateForm openRightVersionDdl() { + rightVersionDdl.shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public int getVersionsNumberFromList() { + return elementsCompareVersionDdl.size(); + } + + @Step + public SchemaCreateForm selectVersionFromDropDown(int versionNumberDd) { + $x(String.format(ddlElementLocator, versionNumberDd)).shouldBe(Condition.visible).click(); + return this; + } + + @Step + public int getMarkedLinesNumber() { + return visibleMarkers.size(); + } + + @Step + public SchemaCreateForm setNewSchemaValue(String configJson) { + newSchemaTextArea.shouldBe(Condition.visible).click(); + newSchemaInput.shouldBe(Condition.enabled); + new Actions(WebDriverRunner.getWebDriver()) + .sendKeys(Keys.PAGE_UP) + .keyDown(Keys.SHIFT) + .sendKeys(Keys.PAGE_DOWN) + .keyUp(Keys.SHIFT) + .sendKeys(Keys.DELETE) + .perform(); + setJsonInputValue(newSchemaInput, configJson); + return this; + } + + @Step + public List getAllDetailsPageElements() { + return Stream.of(compatibilityLevelList, newSchemaTextArea, latestSchemaTextArea, submitBtn, schemaTypeDdl) + .collect(Collectors.toList()); + } + + @Step + public boolean isSubmitBtnEnabled() { + return isEnabled(submitBtn); + } + + @Step + public boolean isSchemaDropDownEnabled() { + boolean enabled = true; + try { + String attribute = schemaTypeDdl.getAttribute("disabled"); + enabled = false; + } catch (Throwable ignored) { + } + return enabled; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaDetails.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaDetails.java new file mode 100644 index 00000000000..38c12d35f3d --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaDetails.java @@ -0,0 +1,69 @@ +package com.provectus.kafka.ui.pages.schemas; + +import static com.codeborne.selenide.Selenide.$x; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; + +public class SchemaDetails extends BasePage { + + protected SelenideElement actualVersionTextArea = $x("//div[@id='schema']"); + protected SelenideElement compatibilityField = $x("//h4[contains(text(),'Compatibility')]/../p"); + protected SelenideElement editSchemaBtn = $x("//button[contains(text(),'Edit Schema')]"); + protected SelenideElement removeBtn = $x("//*[contains(text(),'Remove')]"); + protected SelenideElement schemaConfirmBtn = $x("//div[@role='dialog']//button[contains(text(),'Confirm')]"); + protected SelenideElement schemaTypeField = $x("//h4[contains(text(),'Type')]/../p"); + protected SelenideElement latestVersionField = $x("//h4[contains(text(),'Latest version')]/../p"); + protected SelenideElement compareVersionBtn = $x("//button[text()='Compare Versions']"); + protected String schemaHeaderLocator = "//h1[contains(text(),'%s')]"; + + @Step + public SchemaDetails waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + actualVersionTextArea.shouldBe(Condition.visible); + return this; + } + + @Step + public String getCompatibility() { + return compatibilityField.getText(); + } + + @Step + public boolean isSchemaHeaderVisible(String schemaName) { + return isVisible($x(String.format(schemaHeaderLocator, schemaName))); + } + + @Step + public int getLatestVersion() { + return Integer.parseInt(latestVersionField.getText()); + } + + @Step + public String getSchemaType() { + return schemaTypeField.getText(); + } + + @Step + public SchemaDetails openEditSchema() { + editSchemaBtn.shouldBe(Condition.visible).click(); + return this; + } + + @Step + public SchemaDetails openCompareVersionMenu() { + compareVersionBtn.shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public SchemaDetails removeSchema() { + clickByJavaScript(dotMenuBtn); + removeBtn.shouldBe(Condition.enabled).click(); + schemaConfirmBtn.shouldBe(Condition.visible).click(); + schemaConfirmBtn.shouldBe(Condition.disappear); + return this; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaRegistryList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaRegistryList.java new file mode 100644 index 00000000000..f2d2f4b98c7 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaRegistryList.java @@ -0,0 +1,42 @@ +package com.provectus.kafka.ui.pages.schemas; + +import static com.codeborne.selenide.Selenide.$x; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.SCHEMA_REGISTRY; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; + +public class SchemaRegistryList extends BasePage { + + protected SelenideElement createSchemaBtn = $x("//button[contains(text(),'Create Schema')]"); + + @Step + public SchemaRegistryList waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + getPageTitleFromHeader(SCHEMA_REGISTRY).shouldBe(Condition.visible); + return this; + } + + @Step + public SchemaRegistryList clickCreateSchema() { + clickByJavaScript(createSchemaBtn); + return this; + } + + @Step + public SchemaRegistryList openSchema(String schemaName) { + getTableElement(schemaName) + .shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public boolean isSchemaVisible(String schemaName) { + tableGrid.shouldBe(Condition.visible); + return isVisible(getTableElement(schemaName)); + } +} + + diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/ProduceMessagePanel.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/ProduceMessagePanel.java new file mode 100644 index 00000000000..b683321bd51 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/ProduceMessagePanel.java @@ -0,0 +1,56 @@ +package com.provectus.kafka.ui.pages.topics; + +import static com.codeborne.selenide.Selenide.$x; +import static com.codeborne.selenide.Selenide.refresh; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; +import java.util.Arrays; + +public class ProduceMessagePanel extends BasePage { + + protected SelenideElement keyTextArea = $x("//div[@id='key']/textarea"); + protected SelenideElement valueTextArea = $x("//div[@id='content']/textarea"); + protected SelenideElement headersTextArea = $x("//div[@id='headers']/textarea"); + protected SelenideElement submitProduceMessageBtn = headersTextArea.$x("../../../..//button[@type='submit']"); + protected SelenideElement partitionDdl = $x("//ul[@name='partition']"); + protected SelenideElement keySerdeDdl = $x("//ul[@name='keySerde']"); + protected SelenideElement contentSerdeDdl = $x("//ul[@name='valueSerde']"); + + @Step + public ProduceMessagePanel waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + Arrays.asList(partitionDdl, keySerdeDdl, contentSerdeDdl).forEach(element -> element.shouldBe(Condition.visible)); + return this; + } + + @Step + public ProduceMessagePanel setKeyField(String value) { + clearByKeyboard(keyTextArea); + keyTextArea.setValue(value); + return this; + } + + @Step + public ProduceMessagePanel setValueFiled(String value) { + clearByKeyboard(valueTextArea); + valueTextArea.setValue(value); + return this; + } + + @Step + public ProduceMessagePanel setHeadersFld(String value) { + headersTextArea.setValue(value); + return this; + } + + @Step + public ProduceMessagePanel submitProduceMessage() { + clickByActions(submitProduceMessageBtn); + submitProduceMessageBtn.shouldBe(Condition.disappear); + refresh(); + return this; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicCreateEditForm.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicCreateEditForm.java new file mode 100644 index 00000000000..46493ab97f4 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicCreateEditForm.java @@ -0,0 +1,282 @@ +package com.provectus.kafka.ui.pages.topics; + +import static com.codeborne.selenide.Selenide.$; +import static com.codeborne.selenide.Selenide.$$; +import static com.codeborne.selenide.Selenide.$x; +import static org.openqa.selenium.By.id; + +import com.codeborne.selenide.ClickOptions; +import com.codeborne.selenide.CollectionCondition; +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.ElementsCollection; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue; +import com.provectus.kafka.ui.pages.topics.enums.CustomParameterType; +import com.provectus.kafka.ui.pages.topics.enums.MaxSizeOnDisk; +import com.provectus.kafka.ui.pages.topics.enums.TimeToRetain; +import io.qameta.allure.Step; + +public class TopicCreateEditForm extends BasePage { + + private static final String RETENTION_BYTES = "retentionBytes"; + + protected SelenideElement timeToRetainField = $x("//input[@id='timeToRetain']"); + protected SelenideElement partitionsField = $x("//input[@name='partitions']"); + protected SelenideElement nameField = $(id("topicFormName")); + protected SelenideElement maxMessageBytesField = $x("//input[@name='maxMessageBytes']"); + protected SelenideElement minInSyncReplicasField = $x("//input[@name='minInSyncReplicas']"); + protected SelenideElement cleanUpPolicyDdl = $x("//ul[@id='topicFormCleanupPolicy']"); + protected SelenideElement maxSizeOnDiscDdl = $x("//ul[@id='topicFormRetentionBytes']"); + protected SelenideElement customParameterDdl = $x("//ul[contains(@name,'customParams')]"); + protected SelenideElement deleteCustomParameterBtn = $x("//span[contains(@title,'Delete customParam')]"); + protected SelenideElement addCustomParameterTypeBtn = $x("//button[contains(text(),'Add Custom Parameter')]"); + protected SelenideElement customParameterValueField = $x("//input[@placeholder='Value']"); + protected SelenideElement validationCustomParameterValueMsg = $x("//p[contains(text(),'Value is required')]"); + protected String ddlElementLocator = "//li[@value='%s']"; + protected String btnTimeToRetainLocator = "//button[@class][text()='%s']"; + + + @Step + public TopicCreateEditForm waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + nameField.shouldBe(Condition.visible); + return this; + } + + public boolean isCreateTopicButtonEnabled() { + return isEnabled(submitBtn); + } + + public boolean isDeleteCustomParameterButtonEnabled() { + return isEnabled(deleteCustomParameterBtn); + } + + public boolean isNameFieldEnabled() { + return isEnabled(nameField); + } + + @Step + public TopicCreateEditForm setTopicName(String topicName) { + sendKeysAfterClear(nameField, topicName); + return this; + } + + @Step + public TopicCreateEditForm setMinInsyncReplicas(Integer minInsyncReplicas) { + minInSyncReplicasField.setValue(minInsyncReplicas.toString()); + return this; + } + + @Step + public TopicCreateEditForm setTimeToRetainDataInMs(Long ms) { + timeToRetainField.setValue(ms.toString()); + return this; + } + + @Step + public TopicCreateEditForm setTimeToRetainDataInMs(String ms) { + timeToRetainField.setValue(ms); + return this; + } + + @Step + public TopicCreateEditForm setMaxSizeOnDiskInGB(MaxSizeOnDisk maxSizeOnDisk) { + maxSizeOnDiscDdl.shouldBe(Condition.visible).click(); + $x(String.format(ddlElementLocator, maxSizeOnDisk.getOptionValue())).shouldBe(Condition.visible).click(); + return this; + } + + @Step + public TopicCreateEditForm clickAddCustomParameterTypeButton() { + addCustomParameterTypeBtn.click(); + return this; + } + + @Step + public TopicCreateEditForm openCustomParameterTypeDdl() { + customParameterDdl.shouldBe(Condition.visible).click(); + ddlOptions.shouldHave(CollectionCondition.sizeGreaterThan(0)); + return this; + } + + @Step + public ElementsCollection getAllDdlOptions() { + return getDdlOptions(); + } + + @Step + public TopicCreateEditForm setCustomParameterType(CustomParameterType customParameterType) { + openCustomParameterTypeDdl(); + $x(String.format(ddlElementLocator, customParameterType.getOptionValue())).shouldBe(Condition.visible).click(); + return this; + } + + @Step + public TopicCreateEditForm clearCustomParameterValue() { + clearByKeyboard(customParameterValueField); + return this; + } + + @Step + public TopicCreateEditForm setNumberOfPartitions(int partitions) { + partitionsField.shouldBe(Condition.enabled).clear(); + partitionsField.sendKeys(String.valueOf(partitions)); + return this; + } + + @Step + public TopicCreateEditForm setTimeToRetainDataByButtons(TimeToRetain timeToRetain) { + $x(String.format(btnTimeToRetainLocator, timeToRetain.getButton())).shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public TopicCreateEditForm selectCleanupPolicy(CleanupPolicyValue cleanupPolicyOptionValue) { + cleanUpPolicyDdl.shouldBe(Condition.visible).click(); + $x(String.format(ddlElementLocator, cleanupPolicyOptionValue.getOptionValue())).shouldBe(Condition.visible).click(); + return this; + } + + @Step + public TopicCreateEditForm selectRetentionBytes(String visibleValue) { + return selectFromDropDownByVisibleText(RETENTION_BYTES, visibleValue); + } + + @Step + public TopicCreateEditForm selectRetentionBytes(Long optionValue) { + return selectFromDropDownByOptionValue(RETENTION_BYTES, optionValue.toString()); + } + + @Step + public TopicCreateEditForm clickSaveTopicBtn() { + clickSubmitBtn(); + return this; + } + + @Step + public TopicCreateEditForm addCustomParameter(String customParameterName, + String customParameterValue) { + ElementsCollection customParametersElements = + $$("ul[role=listbox][name^=customParams][name$=name]"); + KafkaUiSelectElement kafkaUiSelectElement = null; + if (customParametersElements.size() == 1) { + if ("Select".equals(customParametersElements.first().getText())) { + kafkaUiSelectElement = new KafkaUiSelectElement(customParametersElements.first()); + } + } else { + $$("button") + .find(Condition.exactText("Add Custom Parameter")) + .click(); + customParametersElements = $$("ul[role=listbox][name^=customParams][name$=name]"); + kafkaUiSelectElement = new KafkaUiSelectElement(customParametersElements.last()); + } + if (kafkaUiSelectElement != null) { + kafkaUiSelectElement.selectByVisibleText(customParameterName); + } + $(String.format("input[name=\"customParams.%d.value\"]", customParametersElements.size() - 1)) + .setValue(customParameterValue); + return this; + } + + @Step + public TopicCreateEditForm updateCustomParameter(String customParameterName, + String customParameterValue) { + SelenideElement selenideElement = $$("ul[role=listbox][name^=customParams][name$=name]") + .find(Condition.exactText(customParameterName)); + String name = selenideElement.getAttribute("name"); + if (name != null) { + name = name.substring(0, name.lastIndexOf(".")); + } + $(String.format("input[name^=%s]", name)).setValue(customParameterValue); + return this; + } + + @Step + public String getCleanupPolicy() { + return new KafkaUiSelectElement("cleanupPolicy").getCurrentValue(); + } + + @Step + public String getTimeToRetain() { + return timeToRetainField.getValue(); + } + + @Step + public String getMaxSizeOnDisk() { + return new KafkaUiSelectElement(RETENTION_BYTES).getCurrentValue(); + } + + @Step + public String getMaxMessageBytes() { + return maxMessageBytesField.getValue(); + } + + @Step + public TopicCreateEditForm setMaxMessageBytes(Long bytes) { + maxMessageBytesField.setValue(bytes.toString()); + return this; + } + + @Step + public TopicCreateEditForm setMaxMessageBytes(String bytes) { + return setMaxMessageBytes(Long.parseLong(bytes)); + } + + @Step + public boolean isValidationMessageCustomParameterValueVisible() { + return isVisible(validationCustomParameterValueMsg); + } + + @Step + public String getCustomParameterValue() { + return customParameterValueField.getValue(); + } + + private TopicCreateEditForm selectFromDropDownByOptionValue(String dropDownElementName, + String optionValue) { + KafkaUiSelectElement select = new KafkaUiSelectElement(dropDownElementName); + select.selectByOptionValue(optionValue); + return this; + } + + private TopicCreateEditForm selectFromDropDownByVisibleText(String dropDownElementName, + String visibleText) { + KafkaUiSelectElement select = new KafkaUiSelectElement(dropDownElementName); + select.selectByVisibleText(visibleText); + return this; + } + + private static class KafkaUiSelectElement { + + private final SelenideElement selectElement; + + public KafkaUiSelectElement(String selectElementName) { + this.selectElement = $("ul[role=listbox][name=" + selectElementName + "]"); + } + + public KafkaUiSelectElement(SelenideElement selectElement) { + this.selectElement = selectElement; + } + + public void selectByOptionValue(String optionValue) { + selectElement.click(); + selectElement + .$$x(".//ul/li[@role='option']") + .find(Condition.attribute("value", optionValue)) + .click(ClickOptions.usingJavaScript()); + } + + public void selectByVisibleText(String visibleText) { + selectElement.click(); + selectElement + .$$("ul>li[role=option]") + .find(Condition.exactText(visibleText)) + .click(); + } + + public String getCurrentValue() { + return selectElement.$("li").getText(); + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicDetails.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicDetails.java new file mode 100644 index 00000000000..07effd8e341 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicDetails.java @@ -0,0 +1,517 @@ +package com.provectus.kafka.ui.pages.topics; + +import static com.codeborne.selenide.Selenide.$$x; +import static com.codeborne.selenide.Selenide.$x; +import static com.codeborne.selenide.Selenide.sleep; +import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.OVERVIEW; +import static org.testcontainers.shaded.org.apache.commons.lang3.RandomUtils.nextInt; + +import com.codeborne.selenide.CollectionCondition; +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.ElementsCollection; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +public class TopicDetails extends BasePage { + + protected SelenideElement clearMessagesBtn = $x(("//div[contains(text(), 'Clear messages')]")); + protected SelenideElement recreateTopicBtn = $x("//div[text()='Recreate Topic']"); + protected SelenideElement messageAmountCell = $x("//tbody/tr/td[5]"); + protected SelenideElement overviewTab = $x("//a[contains(text(),'Overview')]"); + protected SelenideElement messagesTab = $x("//a[contains(text(),'Messages')]"); + protected SelenideElement seekTypeDdl = $x("//ul[@id='selectSeekType']//li"); + protected SelenideElement seekTypeField = $x("//label[text()='Seek Type']//..//div/input"); + protected SelenideElement addFiltersBtn = $x("//button[text()='Add Filters']"); + protected SelenideElement savedFiltersLink = $x("//div[text()='Saved Filters']"); + protected SelenideElement addFilterCodeModalTitle = $x("//label[text()='Filter code']"); + protected SelenideElement addFilterCodeEditor = $x("//div[@id='ace-editor']"); + protected SelenideElement addFilterCodeTextarea = $x("//div[@id='ace-editor']//textarea"); + protected SelenideElement saveThisFilterCheckBoxAddFilterMdl = $x("//input[@name='saveFilter']"); + protected SelenideElement displayNameInputAddFilterMdl = $x("//input[@placeholder='Enter Name']"); + protected SelenideElement cancelBtnAddFilterMdl = $x("//button[text()='Cancel']"); + protected SelenideElement addFilterBtnAddFilterMdl = $x("//button[text()='Add filter']"); + protected SelenideElement saveFilterBtnEditFilterMdl = $x("//button[text()='Save']"); + protected SelenideElement addFiltersBtnMessages = $x("//button[text()='Add Filters']"); + protected SelenideElement selectFilterBtnAddFilterMdl = $x("//button[text()='Select filter']"); + protected SelenideElement editSettingsMenu = $x("//li[@role][contains(text(),'Edit settings')]"); + protected SelenideElement removeTopicBtn = $x("//ul[@role='menu']//div[contains(text(),'Remove Topic')]"); + protected SelenideElement produceMessageBtn = $x("//div//button[text()='Produce Message']"); + protected SelenideElement contentMessageTab = $x("//html//div[@id='root']/div/main//table//p"); + protected SelenideElement cleanUpPolicyField = $x("//div[contains(text(),'Clean Up Policy')]/../span/*"); + protected SelenideElement partitionsField = $x("//div[contains(text(),'Partitions')]/../span"); + protected SelenideElement backToCreateFiltersLink = $x("//div[text()='Back To create filters']"); + protected ElementsCollection messageGridItems = $$x("//tbody//tr"); + protected SelenideElement actualCalendarDate = $x("//div[@class='react-datepicker__current-month']"); + protected SelenideElement previousMonthButton = $x("//button[@aria-label='Previous Month']"); + protected SelenideElement nextMonthButton = $x("//button[@aria-label='Next Month']"); + protected SelenideElement calendarTimeFld = $x("//input[@placeholder='Time']"); + protected String detailsTabLtr = "//nav//a[contains(text(),'%s')]"; + protected String dayCellLtr = "//div[@role='option'][contains(text(),'%d')]"; + protected String seekFilterDdlLocator = "//ul[@id='selectSeekType']/ul/li[text()='%s']"; + protected String savedFilterNameLocator = "//div[@role='savedFilter']/div[contains(text(),'%s')]"; + protected String consumerIdLocator = "//a[@title='%s']"; + protected String topicHeaderLocator = "//h1[contains(text(),'%s')]"; + protected String activeFilterNameLocator = "//div[@data-testid='activeSmartFilter']/div[1][contains(text(),'%s')]"; + protected String editActiveFilterBtnLocator = "//div[text()='%s']/../div[@data-testid='editActiveSmartFilterBtn']"; + protected String settingsGridValueLocator = "//tbody/tr/td/span[text()='%s']//ancestor::tr/td[2]/span"; + + @Step + public TopicDetails waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + $x(String.format(detailsTabLtr, OVERVIEW)).shouldBe(Condition.visible); + return this; + } + + @Step + public TopicDetails openDetailsTab(TopicMenu menu) { + $x(String.format(detailsTabLtr, menu.toString())).shouldBe(Condition.enabled).click(); + waitUntilSpinnerDisappear(); + return this; + } + + @Step + public String getSettingsGridValueByKey(String key) { + return $x(String.format(settingsGridValueLocator, key)).scrollTo().shouldBe(Condition.visible).getText(); + } + + @Step + public TopicDetails openDotMenu() { + clickByJavaScript(dotMenuBtn); + return this; + } + + @Step + public boolean isAlertWithMessageVisible(AlertHeader header, String message) { + return isAlertVisible(header, message); + } + + @Step + public TopicDetails clickEditSettingsMenu() { + editSettingsMenu.shouldBe(Condition.visible).click(); + return this; + } + + @Step + public boolean isConfirmationMdlVisible() { + return isConfirmationModalVisible(); + } + + @Step + public TopicDetails clickClearMessagesMenu() { + clearMessagesBtn.shouldBe(Condition.visible).click(); + return this; + } + + @Step + public boolean isClearMessagesMenuEnabled() { + return !Objects.requireNonNull(clearMessagesBtn.shouldBe(Condition.visible) + .$x("./..").getAttribute("class")) + .contains("disabled"); + } + + @Step + public TopicDetails clickRecreateTopicMenu() { + recreateTopicBtn.shouldBe(Condition.visible).click(); + return this; + } + + @Step + public String getCleanUpPolicy() { + return cleanUpPolicyField.getText(); + } + + @Step + public int getPartitions() { + return Integer.parseInt(partitionsField.getText().trim()); + } + + @Step + public boolean isTopicHeaderVisible(String topicName) { + return isVisible($x(String.format(topicHeaderLocator, topicName))); + } + + @Step + public TopicDetails clickDeleteTopicMenu() { + removeTopicBtn.shouldBe(Condition.visible).click(); + return this; + } + + @Step + public TopicDetails clickConfirmBtnMdl() { + clickConfirmButton(); + return this; + } + + @Step + public TopicDetails clickProduceMessageBtn() { + clickByJavaScript(produceMessageBtn); + return this; + } + + @Step + public TopicDetails selectSeekTypeDdlMessagesTab(String seekTypeName) { + seekTypeDdl.shouldBe(Condition.enabled).click(); + $x(String.format(seekFilterDdlLocator, seekTypeName)).shouldBe(Condition.visible).click(); + return this; + } + + @Step + public TopicDetails setSeekTypeValueFldMessagesTab(String seekTypeValue) { + seekTypeField.shouldBe(Condition.enabled).sendKeys(seekTypeValue); + return this; + } + + @Step + public TopicDetails clickSubmitFiltersBtnMessagesTab() { + clickByJavaScript(submitBtn); + waitUntilSpinnerDisappear(); + return this; + } + + @Step + public TopicDetails clickMessagesAddFiltersBtn() { + addFiltersBtn.shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public TopicDetails clickEditActiveFilterBtn(String filterName) { + $x(String.format(editActiveFilterBtnLocator, filterName)) + .shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public TopicDetails clickNextButton() { + clickNextBtn(); + waitUntilSpinnerDisappear(); + return this; + } + + @Step + public TopicDetails openSavedFiltersListMdl() { + savedFiltersLink.shouldBe(Condition.enabled).click(); + backToCreateFiltersLink.shouldBe(Condition.visible); + return this; + } + + @Step + public boolean isFilterVisibleAtSavedFiltersMdl(String filterName) { + return isVisible($x(String.format(savedFilterNameLocator, filterName))); + } + + @Step + public TopicDetails selectFilterAtSavedFiltersMdl(String filterName) { + $x(String.format(savedFilterNameLocator, filterName)).shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public TopicDetails clickSelectFilterBtnAtSavedFiltersMdl() { + selectFilterBtnAddFilterMdl.shouldBe(Condition.enabled).click(); + addFilterCodeModalTitle.shouldBe(Condition.disappear); + return this; + } + + @Step + public TopicDetails waitUntilAddFiltersMdlVisible() { + addFilterCodeModalTitle.shouldBe(Condition.visible); + return this; + } + + @Step + public TopicDetails setFilterCodeFldAddFilterMdl(String filterCode) { + addFilterCodeTextarea.shouldBe(Condition.enabled).setValue(filterCode); + return this; + } + + @Step + public String getFilterCodeValue() { + addFilterCodeEditor.shouldBe(Condition.enabled).click(); + String value = addFilterCodeTextarea.getValue(); + if (value == null) { + return null; + } else { + return value.substring(0, value.length() - 2); + } + } + + @Step + public String getFilterNameValue() { + return displayNameInputAddFilterMdl.shouldBe(Condition.enabled).getValue(); + } + + @Step + public TopicDetails selectSaveThisFilterCheckboxMdl(boolean select) { + selectElement(saveThisFilterCheckBoxAddFilterMdl, select); + return this; + } + + @Step + public boolean isSaveThisFilterCheckBoxSelected() { + return isSelected(saveThisFilterCheckBoxAddFilterMdl); + } + + @Step + public TopicDetails setDisplayNameFldAddFilterMdl(String displayName) { + displayNameInputAddFilterMdl.shouldBe(Condition.enabled).setValue(displayName); + return this; + } + + @Step + public TopicDetails clickAddFilterBtnAndCloseMdl(boolean closeModal) { + addFilterBtnAddFilterMdl.shouldBe(Condition.enabled).click(); + if (closeModal) { + addFilterCodeModalTitle.shouldBe(Condition.hidden); + } else { + addFilterCodeModalTitle.shouldBe(Condition.visible); + } + return this; + } + + @Step + public TopicDetails clickSaveFilterBtnAndCloseMdl(boolean closeModal) { + saveFilterBtnEditFilterMdl.shouldBe(Condition.enabled).click(); + if (closeModal) { + addFilterCodeModalTitle.shouldBe(Condition.hidden); + } else { + addFilterCodeModalTitle.shouldBe(Condition.visible); + } + return this; + } + + @Step + public boolean isAddFilterBtnAddFilterMdlEnabled() { + return isEnabled(addFilterBtnAddFilterMdl); + } + + @Step + public boolean isBackButtonEnabled() { + return isEnabled(backBtn); + } + + @Step + public boolean isNextButtonEnabled() { + return isEnabled(nextBtn); + } + + @Step + public boolean isActiveFilterVisible(String filterName) { + return isVisible($x(String.format(activeFilterNameLocator, filterName))); + } + + @Step + public String getSearchFieldValue() { + return searchFld.shouldBe(Condition.visible).getValue(); + } + + public List getAllAddFilterModalVisibleElements() { + return Arrays.asList(savedFiltersLink, displayNameInputAddFilterMdl, addFilterBtnAddFilterMdl, + cancelBtnAddFilterMdl); + } + + public List getAllAddFilterModalEnabledElements() { + return Arrays.asList(displayNameInputAddFilterMdl, cancelBtnAddFilterMdl); + } + + public List getAllAddFilterModalDisabledElements() { + return Collections.singletonList(addFilterBtnAddFilterMdl); + } + + @Step + public TopicDetails openConsumerGroup(String consumerId) { + $x(String.format(consumerIdLocator, consumerId)).click(); + return this; + } + + private void selectYear(int expectedYear) { + while (getActualCalendarDate().getYear() > expectedYear) { + clickByJavaScript(previousMonthButton); + sleep(1000); + if (LocalTime.now().plusMinutes(3).isBefore(LocalTime.now())) { + throw new IllegalArgumentException("Unable to select year"); + } + } + } + + private void selectMonth(int expectedMonth) { + while (getActualCalendarDate().getMonthValue() > expectedMonth) { + clickByJavaScript(previousMonthButton); + sleep(1000); + if (LocalTime.now().plusMinutes(3).isBefore(LocalTime.now())) { + throw new IllegalArgumentException("Unable to select month"); + } + } + } + + private void selectDay(int expectedDay) { + Objects.requireNonNull($$x(String.format(dayCellLtr, expectedDay)).stream() + .filter(day -> !Objects.requireNonNull(day.getAttribute("class")).contains("outside-month")) + .findFirst().orElseThrow()).shouldBe(Condition.enabled).click(); + } + + private void setTime(LocalDateTime dateTime) { + calendarTimeFld.shouldBe(Condition.enabled) + .sendKeys(String.valueOf(dateTime.getHour()), String.valueOf(dateTime.getMinute())); + } + + @Step + public TopicDetails selectDateAndTimeByCalendar(LocalDateTime dateTime) { + setTime(dateTime); + selectYear(dateTime.getYear()); + selectMonth(dateTime.getMonthValue()); + selectDay(dateTime.getDayOfMonth()); + return this; + } + + private LocalDate getActualCalendarDate() { + String monthAndYearStr = actualCalendarDate.getText().trim(); + DateTimeFormatter formatter = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .append(DateTimeFormatter.ofPattern("MMMM yyyy")) + .toFormatter(Locale.ENGLISH); + YearMonth yearMonth = formatter.parse(monthAndYearStr, YearMonth::from); + return yearMonth.atDay(1); + } + + @Step + public TopicDetails openCalendarSeekType() { + seekTypeField.shouldBe(Condition.enabled).click(); + actualCalendarDate.shouldBe(Condition.visible); + return this; + } + + @Step + public int getMessageCountAmount() { + return Integer.parseInt(messageAmountCell.getText().trim()); + } + + private List initItems() { + List gridItemList = new ArrayList<>(); + gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new TopicDetails.MessageGridItem(item))); + return gridItemList; + } + + @Step + public TopicDetails.MessageGridItem getMessageByOffset(int offset) { + return initItems().stream() + .filter(e -> e.getOffset() == offset) + .findFirst().orElseThrow(); + } + + @Step + public TopicDetails.MessageGridItem getMessageByKey(String key) { + return initItems().stream() + .filter(e -> e.getKey().equals(key)) + .findFirst().orElseThrow(); + } + + @Step + public List getAllMessages() { + return initItems(); + } + + @Step + public TopicDetails.MessageGridItem getRandomMessage() { + return getMessageByOffset(nextInt(0, initItems().size() - 1)); + } + + public enum TopicMenu { + OVERVIEW("Overview"), + MESSAGES("Messages"), + CONSUMERS("Consumers"), + SETTINGS("Settings"); + + private final String value; + + TopicMenu(String value) { + this.value = value; + } + + public String toString() { + return value; + } + } + + public static class MessageGridItem extends BasePage { + + private final SelenideElement element; + + private MessageGridItem(SelenideElement element) { + this.element = element; + } + + @Step + public MessageGridItem clickExpand() { + clickByJavaScript(element.$x("./td[1]/span")); + return this; + } + + private SelenideElement getOffsetElm() { + return element.$x("./td[2]"); + } + + @Step + public int getOffset() { + return Integer.parseInt(getOffsetElm().getText().trim()); + } + + @Step + public int getPartition() { + return Integer.parseInt(element.$x("./td[3]").getText().trim()); + } + + @Step + public LocalDateTime getTimestamp() { + String timestampValue = element.$x("./td[4]/div").getText().trim(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("M/d/yyyy, HH:mm:ss"); + return LocalDateTime.parse(timestampValue, formatter); + } + + @Step + public String getKey() { + return element.$x("./td[5]").getText().trim(); + } + + @Step + public String getValue() { + return element.$x("./td[6]").getAttribute("title"); + } + + @Step + public MessageGridItem openDotMenu() { + getOffsetElm().hover(); + element.$x("./td[7]/div/button[@aria-label='Dropdown Toggle']") + .shouldBe(Condition.visible).click(); + return this; + } + + @Step + public MessageGridItem clickCopyToClipBoard() { + clickByJavaScript(element.$x("./td[7]//li[text() = 'Copy to clipboard']") + .shouldBe(Condition.visible)); + return this; + } + + @Step + public MessageGridItem clickSaveAsFile() { + clickByJavaScript(element.$x("./td[7]//li[text() = 'Save as a file']") + .shouldBe(Condition.visible)); + return this; + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicSettingsTab.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicSettingsTab.java new file mode 100644 index 00000000000..c36e8423767 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicSettingsTab.java @@ -0,0 +1,65 @@ +package com.provectus.kafka.ui.pages.topics; + +import static com.codeborne.selenide.Selenide.$x; + +import com.codeborne.selenide.CollectionCondition; +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; +import java.util.ArrayList; +import java.util.List; + +public class TopicSettingsTab extends BasePage { + + protected SelenideElement defaultValueColumnHeaderLocator = $x("//div[text() = 'Default Value']"); + + @Step + public TopicSettingsTab waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + defaultValueColumnHeaderLocator.shouldBe(Condition.visible); + return this; + } + + private List initGridItems() { + List gridItemList = new ArrayList<>(); + gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new SettingsGridItem(item))); + return gridItemList; + } + + private TopicSettingsTab.SettingsGridItem getItemByKey(String key) { + return initGridItems().stream() + .filter(e -> e.getKey().equals(key)) + .findFirst().orElseThrow(); + } + + @Step + public String getValueByKey(String key) { + return getItemByKey(key).getValue(); + } + + public static class SettingsGridItem extends BasePage { + + private final SelenideElement element; + + public SettingsGridItem(SelenideElement element) { + this.element = element; + } + + @Step + public String getKey() { + return element.$x("./td[1]/span").getText().trim(); + } + + @Step + public String getValue() { + return element.$x("./td[2]/span").getText().trim(); + } + + @Step + public String getDefaultValue() { + return element.$x("./td[3]/span").getText().trim(); + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicsList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicsList.java new file mode 100644 index 00000000000..c3ef098ff0f --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicsList.java @@ -0,0 +1,294 @@ +package com.provectus.kafka.ui.pages.topics; + +import static com.codeborne.selenide.Condition.visible; +import static com.codeborne.selenide.Selenide.$x; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.TOPICS; + +import com.codeborne.selenide.CollectionCondition; +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.pages.BasePage; +import io.qameta.allure.Step; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class TopicsList extends BasePage { + + protected SelenideElement addTopicBtn = $x("//button[normalize-space(text()) ='Add a Topic']"); + protected SelenideElement searchField = $x("//input[@placeholder='Search by Topic Name']"); + protected SelenideElement showInternalRadioBtn = $x("//input[@name='ShowInternalTopics']"); + protected SelenideElement deleteSelectedTopicsBtn = $x("//button[text()='Delete selected topics']"); + protected SelenideElement copySelectedTopicBtn = $x("//button[text()='Copy selected topic']"); + protected SelenideElement purgeMessagesOfSelectedTopicsBtn = + $x("//button[text()='Purge messages of selected topics']"); + protected SelenideElement clearMessagesBtn = $x("//ul[contains(@class ,'open')]//div[text()='Clear Messages']"); + protected SelenideElement recreateTopicBtn = $x("//ul[contains(@class ,'open')]//div[text()='Recreate Topic']"); + protected SelenideElement removeTopicBtn = $x("//ul[contains(@class ,'open')]//div[text()='Remove Topic']"); + + @Step + public TopicsList waitUntilScreenReady() { + waitUntilSpinnerDisappear(); + getPageTitleFromHeader(TOPICS).shouldBe(visible); + return this; + } + + @Step + public TopicsList clickAddTopicBtn() { + clickByJavaScript(addTopicBtn); + return this; + } + + @Step + public boolean isTopicVisible(String topicName) { + tableGrid.shouldBe(visible); + return isVisible(getTableElement(topicName)); + } + + @Step + public boolean isShowInternalRadioBtnSelected() { + return isSelected(showInternalRadioBtn); + } + + @Step + public TopicsList setShowInternalRadioButton(boolean select) { + if (select) { + if (!showInternalRadioBtn.isSelected()) { + clickByJavaScript(showInternalRadioBtn); + waitUntilSpinnerDisappear(1); + } + } else { + if (showInternalRadioBtn.isSelected()) { + clickByJavaScript(showInternalRadioBtn); + waitUntilSpinnerDisappear(1); + } + } + return this; + } + + @Step + public TopicsList goToLastPage() { + if (nextBtn.exists()) { + while (nextBtn.isEnabled()) { + clickNextBtn(); + waitUntilSpinnerDisappear(1); + } + } + return this; + } + + @Step + public TopicsList openTopic(String topicName) { + getTopicItem(topicName).openItem(); + return this; + } + + @Step + public TopicsList openDotMenuByTopicName(String topicName) { + getTopicItem(topicName).openDotMenu(); + return this; + } + + @Step + public boolean isCopySelectedTopicBtnEnabled() { + return isEnabled(copySelectedTopicBtn); + } + + @Step + public List getActionButtons() { + return Stream.of(deleteSelectedTopicsBtn, copySelectedTopicBtn, purgeMessagesOfSelectedTopicsBtn) + .collect(Collectors.toList()); + } + + @Step + public TopicsList clickCopySelectedTopicBtn() { + copySelectedTopicBtn.shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public TopicsList clickPurgeMessagesOfSelectedTopicsBtn() { + purgeMessagesOfSelectedTopicsBtn.shouldBe(Condition.enabled).click(); + return this; + } + + @Step + public TopicsList clickClearMessagesBtn() { + clickByJavaScript(clearMessagesBtn.shouldBe(visible)); + return this; + } + + @Step + public TopicsList clickRecreateTopicBtn() { + clickByJavaScript(recreateTopicBtn.shouldBe(visible)); + return this; + } + + @Step + public TopicsList clickRemoveTopicBtn() { + clickByJavaScript(removeTopicBtn.shouldBe(visible)); + return this; + } + + @Step + public TopicsList clickConfirmBtnMdl() { + clickConfirmButton(); + return this; + } + + @Step + public TopicsList clickCancelBtnMdl() { + clickCancelButton(); + return this; + } + + @Step + public boolean isConfirmationMdlVisible() { + return isConfirmationModalVisible(); + } + + @Step + public boolean isAlertWithMessageVisible(AlertHeader header, String message) { + return isAlertVisible(header, message); + } + + private List getVisibleColumnHeaders() { + return Stream.of("Replication Factor", "Number of messages", "Topic Name", "Partitions", "Out of sync replicas", + "Size") + .map(name -> $x(String.format(columnHeaderLocator, name))) + .collect(Collectors.toList()); + } + + private List getEnabledColumnHeaders() { + return Stream.of("Topic Name", "Partitions", "Out of sync replicas", "Size") + .map(name -> $x(String.format(columnHeaderLocator, name))) + .collect(Collectors.toList()); + } + + @Step + public List getAllVisibleElements() { + List visibleElements = new ArrayList<>(getVisibleColumnHeaders()); + visibleElements.addAll(Arrays.asList(searchField, addTopicBtn, tableGrid)); + visibleElements.addAll(getActionButtons()); + return visibleElements; + } + + @Step + public List getAllEnabledElements() { + List enabledElements = new ArrayList<>(getEnabledColumnHeaders()); + enabledElements.addAll(Arrays.asList(searchField, showInternalRadioBtn, addTopicBtn)); + return enabledElements; + } + + private List initGridItems() { + List gridItemList = new ArrayList<>(); + gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0)) + .forEach(item -> gridItemList.add(new TopicGridItem(item))); + return gridItemList; + } + + @Step + public TopicGridItem getTopicItem(String name) { + TopicGridItem topicGridItem = initGridItems().stream() + .filter(e -> e.getName().equals(name)) + .findFirst().orElse(null); + if (topicGridItem == null) { + searchItem(name); + topicGridItem = initGridItems().stream() + .filter(e -> e.getName().equals(name)) + .findFirst().orElseThrow(); + } + return topicGridItem; + } + + @Step + public TopicGridItem getAnyNonInternalTopic() { + return getNonInternalTopics().stream() + .findAny().orElseThrow(); + } + + @Step + public List getNonInternalTopics() { + return initGridItems().stream() + .filter(e -> !e.isInternal()) + .collect(Collectors.toList()); + } + + @Step + public List getInternalTopics() { + return initGridItems().stream() + .filter(TopicGridItem::isInternal) + .collect(Collectors.toList()); + } + + public static class TopicGridItem extends BasePage { + + private final SelenideElement element; + + public TopicGridItem(SelenideElement element) { + this.element = element; + } + + @Step + public TopicsList selectItem(boolean select) { + selectElement(element.$x("./td[1]/input"), select); + return new TopicsList(); + } + + private SelenideElement getNameElm() { + return element.$x("./td[2]"); + } + + @Step + public boolean isInternal() { + boolean internal = false; + try { + internal = getNameElm().$x("./a/span").isDisplayed(); + } catch (Throwable ignored) { + } + return internal; + } + + @Step + public String getName() { + return getNameElm().$x("./a").getAttribute("title"); + } + + @Step + public void openItem() { + getNameElm().click(); + } + + @Step + public int getPartition() { + return Integer.parseInt(element.$x("./td[3]").getText().trim()); + } + + @Step + public int getOutOfSyncReplicas() { + return Integer.parseInt(element.$x("./td[4]").getText().trim()); + } + + @Step + public int getReplicationFactor() { + return Integer.parseInt(element.$x("./td[5]").getText().trim()); + } + + @Step + public int getNumberOfMessages() { + return Integer.parseInt(element.$x("./td[6]").getText().trim()); + } + + @Step + public int getSize() { + return Integer.parseInt(element.$x("./td[7]").getText().trim()); + } + + @Step + public void openDotMenu() { + element.$x("./td[8]//button").click(); + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CleanupPolicyValue.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CleanupPolicyValue.java new file mode 100644 index 00000000000..6e4d31a3a24 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CleanupPolicyValue.java @@ -0,0 +1,25 @@ +package com.provectus.kafka.ui.pages.topics.enums; + +public enum CleanupPolicyValue { + + DELETE("delete", "Delete"), + COMPACT("compact", "Compact"), + COMPACT_DELETE("compact,delete", "Compact,Delete"); + + private final String optionValue; + private final String visibleText; + + CleanupPolicyValue(String optionValue, String visibleText) { + this.optionValue = optionValue; + this.visibleText = visibleText; + } + + public String getOptionValue() { + return optionValue; + } + + public String getVisibleText() { + return visibleText; + } +} + diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CustomParameterType.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CustomParameterType.java new file mode 100644 index 00000000000..4ed3e899666 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CustomParameterType.java @@ -0,0 +1,37 @@ +package com.provectus.kafka.ui.pages.topics.enums; + +public enum CustomParameterType { + + COMPRESSION_TYPE("compression.type"), + DELETE_RETENTION_MS("delete.retention.ms"), + FILE_DELETE_DELAY_MS("file.delete.delay.ms"), + FLUSH_MESSAGES("flush.messages"), + FLUSH_MS("flush.ms"), + FOLLOWER_REPLICATION_THROTTLED_REPLICAS("follower.replication.throttled.replicas"), + INDEX_INTERVAL_BYTES("index.interval.bytes"), + LEADER_REPLICATION_THROTTLED_REPLICAS("leader.replication.throttled.replicas"), + MAX_COMPACTION_LAG_MS("max.compaction.lag.ms"), + MESSAGE_DOWNCONVERSION_ENABLE("message.downconversion.enable"), + MESSAGE_FORMAT_VERSION("message.format.version"), + MESSAGE_TIMESTAMP_DIFFERENCE_MAX_MS("message.timestamp.difference.max.ms"), + MESSAGE_TIMESTAMP_TYPE("message.timestamp.type"), + MIN_CLEANABLE_DIRTY_RATIO("min.cleanable.dirty.ratio"), + MIN_COMPACTION_LAG_MS("min.compaction.lag.ms"), + PREALLOCATE("preallocate"), + RETENTION_BYTES("retention.bytes"), + SEGMENT_BYTES("segment.bytes"), + SEGMENT_INDEX_BYTES("segment.index.bytes"), + SEGMENT_JITTER_MS("segment.jitter.ms"), + SEGMENT_MS("segment.ms"), + UNCLEAN_LEADER_ELECTION_ENABLE("unclean.leader.election.enable"); + + private final String optionValue; + + CustomParameterType(String optionValue) { + this.optionValue = optionValue; + } + + public String getOptionValue() { + return optionValue; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/MaxSizeOnDisk.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/MaxSizeOnDisk.java new file mode 100644 index 00000000000..c77c1a96290 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/MaxSizeOnDisk.java @@ -0,0 +1,27 @@ +package com.provectus.kafka.ui.pages.topics.enums; + +public enum MaxSizeOnDisk { + + NOT_SET("-1", "Not Set"), + SIZE_1_GB("1073741824", "1 GB"), + SIZE_10_GB("10737418240", "10 GB"), + SIZE_20_GB("21474836480", "20 GB"), + SIZE_50_GB("53687091200", "50 GB"); + + private final String optionValue; + private final String visibleText; + + MaxSizeOnDisk(String optionValue, String visibleText) { + this.optionValue = optionValue; + this.visibleText = visibleText; + } + + public String getOptionValue() { + return optionValue; + } + + public String getVisibleText() { + return visibleText; + } +} + diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/TimeToRetain.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/TimeToRetain.java new file mode 100644 index 00000000000..c2768ca240f --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/TimeToRetain.java @@ -0,0 +1,26 @@ +package com.provectus.kafka.ui.pages.topics.enums; + +public enum TimeToRetain { + + BTN_12_HOURS("12 hours", "43200000"), + BTN_1_DAY("1 day", "86400000"), + BTN_2_DAYS("2 days", "172800000"), + BTN_7_DAYS("7 days", "604800000"), + BTN_4_WEEKS("4 weeks", "2419200000"); + + private final String button; + private final String value; + + TimeToRetain(String button, String value) { + this.button = button; + this.value = value; + } + + public String getButton() { + return button; + } + + public String getValue() { + return value; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java new file mode 100644 index 00000000000..b4cc54a38f3 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java @@ -0,0 +1,284 @@ +package com.provectus.kafka.ui.services; + +import static com.codeborne.selenide.Selenide.sleep; +import static com.provectus.kafka.ui.utilities.FileUtils.fileToString; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.provectus.kafka.ui.api.ApiClient; +import com.provectus.kafka.ui.api.api.KafkaConnectApi; +import com.provectus.kafka.ui.api.api.KsqlApi; +import com.provectus.kafka.ui.api.api.MessagesApi; +import com.provectus.kafka.ui.api.api.SchemasApi; +import com.provectus.kafka.ui.api.api.TopicsApi; +import com.provectus.kafka.ui.api.model.CreateTopicMessage; +import com.provectus.kafka.ui.api.model.KsqlCommandV2; +import com.provectus.kafka.ui.api.model.KsqlCommandV2Response; +import com.provectus.kafka.ui.api.model.KsqlResponse; +import com.provectus.kafka.ui.api.model.NewConnector; +import com.provectus.kafka.ui.api.model.NewSchemaSubject; +import com.provectus.kafka.ui.api.model.TopicCreation; +import com.provectus.kafka.ui.models.Connector; +import com.provectus.kafka.ui.models.Schema; +import com.provectus.kafka.ui.models.Topic; +import com.provectus.kafka.ui.pages.ksqldb.models.Stream; +import com.provectus.kafka.ui.pages.ksqldb.models.Table; +import com.provectus.kafka.ui.settings.BaseSource; +import io.qameta.allure.Step; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.reactive.function.client.WebClientResponseException; + + +@Slf4j +public class ApiService extends BaseSource { + + private final ApiClient apiClient = new ApiClient().setBasePath(BASE_API_URL); + + @SneakyThrows + private TopicsApi topicApi() { + return new TopicsApi(apiClient); + } + + @SneakyThrows + private SchemasApi schemaApi() { + return new SchemasApi(apiClient); + } + + @SneakyThrows + private KafkaConnectApi connectorApi() { + return new KafkaConnectApi(apiClient); + } + + @SneakyThrows + private MessagesApi messageApi() { + return new MessagesApi(apiClient); + } + + @SneakyThrows + private KsqlApi ksqlApi() { + return new KsqlApi(apiClient); + } + + @SneakyThrows + private void createTopic(String clusterName, String topicName) { + TopicCreation topic = new TopicCreation(); + topic.setName(topicName); + topic.setPartitions(1); + topic.setReplicationFactor(1); + try { + topicApi().createTopic(clusterName, topic).block(); + sleep(2000); + } catch (WebClientResponseException ex) { + ex.printStackTrace(); + } + } + + @Step + public ApiService createTopic(Topic topic) { + createTopic(CLUSTER_NAME, topic.getName()); + return this; + } + + @SneakyThrows + private void deleteTopic(String clusterName, String topicName) { + try { + topicApi().deleteTopic(clusterName, topicName).block(); + } catch (WebClientResponseException ignored) { + } + } + + @Step + public ApiService deleteTopic(String topicName) { + deleteTopic(CLUSTER_NAME, topicName); + return this; + } + + @SneakyThrows + private void createSchema(String clusterName, Schema schema) { + NewSchemaSubject schemaSubject = new NewSchemaSubject(); + schemaSubject.setSubject(schema.getName()); + schemaSubject.setSchema(fileToString(schema.getValuePath())); + schemaSubject.setSchemaType(schema.getType()); + try { + schemaApi().createNewSchema(clusterName, schemaSubject).block(); + } catch (WebClientResponseException ex) { + ex.printStackTrace(); + } + } + + @Step + public ApiService createSchema(Schema schema) { + createSchema(CLUSTER_NAME, schema); + return this; + } + + @SneakyThrows + private void deleteSchema(String clusterName, String schemaName) { + try { + schemaApi().deleteSchema(clusterName, schemaName).block(); + } catch (WebClientResponseException ignored) { + } + } + + @Step + public ApiService deleteSchema(String schemaName) { + deleteSchema(CLUSTER_NAME, schemaName); + return this; + } + + @SneakyThrows + private void deleteConnector(String clusterName, String connectName, String connectorName) { + try { + connectorApi().deleteConnector(clusterName, connectName, connectorName).block(); + } catch (WebClientResponseException ignored) { + } + } + + @Step + public ApiService deleteConnector(String connectName, String connectorName) { + deleteConnector(CLUSTER_NAME, connectName, connectorName); + return this; + } + + @Step + public ApiService deleteConnector(String connectorName) { + deleteConnector(CLUSTER_NAME, CONNECT_NAME, connectorName); + return this; + } + + @SneakyThrows + private void createConnector(String clusterName, String connectName, Connector connector) { + NewConnector connectorProperties = new NewConnector(); + connectorProperties.setName(connector.getName()); + Map configMap = new ObjectMapper().readValue(connector.getConfig(), HashMap.class); + connectorProperties.setConfig(configMap); + try { + connectorApi().deleteConnector(clusterName, connectName, connector.getName()).block(); + } catch (WebClientResponseException ignored) { + } + connectorApi().createConnector(clusterName, connectName, connectorProperties).block(); + } + + @Step + public ApiService createConnector(String connectName, Connector connector) { + createConnector(CLUSTER_NAME, connectName, connector); + return this; + } + + @Step + public ApiService createConnector(Connector connector) { + createConnector(CLUSTER_NAME, CONNECT_NAME, connector); + return this; + } + + @Step + public String getFirstConnectName(String clusterName) { + return Objects.requireNonNull(connectorApi().getConnects(clusterName).blockFirst()).getName(); + } + + @SneakyThrows + private void sendMessage(String clusterName, Topic topic) { + CreateTopicMessage createMessage = new CreateTopicMessage(); + createMessage.setPartition(0); + createMessage.setKeySerde("String"); + createMessage.setValueSerde("String"); + createMessage.setKey(topic.getMessageKey()); + createMessage.setContent(topic.getMessageValue()); + try { + messageApi().sendTopicMessages(clusterName, topic.getName(), createMessage).block(); + } catch (WebClientResponseException ex) { + ex.getRawStatusCode(); + } + } + + @Step + public ApiService sendMessage(Topic topic) { + sendMessage(CLUSTER_NAME, topic); + return this; + } + + @Step + public ApiService createStream(Stream stream) { + KsqlCommandV2Response pipeIdStream = ksqlApi() + .executeKsql(CLUSTER_NAME, new KsqlCommandV2() + .ksql(String.format("CREATE STREAM %s (profileId VARCHAR, latitude DOUBLE, longitude DOUBLE) ", + stream.getName()) + + String.format("WITH (kafka_topic='%s', value_format='json', partitions=1);", + stream.getTopicName()))) + .block(); + assert pipeIdStream != null; + List responseListStream = ksqlApi() + .openKsqlResponsePipe(CLUSTER_NAME, pipeIdStream.getPipeId()) + .collectList() + .block(); + assert Objects.requireNonNull(responseListStream).size() != 0; + return this; + } + + @Step + public ApiService createTables(Table firstTable, Table secondTable) { + KsqlCommandV2Response pipeIdTable1 = ksqlApi() + .executeKsql(CLUSTER_NAME, new KsqlCommandV2() + .ksql(String.format("CREATE TABLE %s AS ", firstTable.getName()) + + " SELECT profileId, " + + " LATEST_BY_OFFSET(latitude) AS la, " + + " LATEST_BY_OFFSET(longitude) AS lo " + + String.format(" FROM %s ", firstTable.getStreamName()) + + " GROUP BY profileId " + + " EMIT CHANGES;")) + .block(); + assert pipeIdTable1 != null; + List responseListTable = ksqlApi() + .openKsqlResponsePipe(CLUSTER_NAME, pipeIdTable1.getPipeId()) + .collectList() + .block(); + assert Objects.requireNonNull(responseListTable).size() != 0; + KsqlCommandV2Response pipeIdTable2 = ksqlApi() + .executeKsql(CLUSTER_NAME, new KsqlCommandV2() + .ksql(String.format("CREATE TABLE %s AS ", secondTable.getName()) + + " SELECT ROUND(GEO_DISTANCE(la, lo, 37.4133, -122.1162), -1) AS distanceInMiles, " + + " COLLECT_LIST(profileId) AS riders, " + + " COUNT(*) AS count " + + String.format(" FROM %s ", firstTable.getName()) + + " GROUP BY ROUND(GEO_DISTANCE(la, lo, 37.4133, -122.1162), -1);")) + .block(); + assert pipeIdTable2 != null; + List responseListTable2 = ksqlApi() + .openKsqlResponsePipe(CLUSTER_NAME, pipeIdTable2.getPipeId()) + .collectList() + .block(); + assert Objects.requireNonNull(responseListTable2).size() != 0; + return this; + } + + @Step + public ApiService insertInto(Stream stream) { + String streamName = stream.getName(); + KsqlCommandV2Response pipeIdInsert = ksqlApi() + .executeKsql(CLUSTER_NAME, new KsqlCommandV2() + .ksql("INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('c2309eec', 37.7877, -122.4205);" + + "INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('18f4ea86', 37.3903, -122.0643); " + + "INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('4ab5cbad', 37.3952, -122.0813); " + + "INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('8b6eae59', 37.3944, -122.0813); " + + "INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('4a7c7b41', 37.4049, -122.0822); " + + "INSERT INTO " + streamName + + " (profileId, latitude, longitude) VALUES ('4ddad000', 37.7857, -122.4011);")) + .block(); + assert pipeIdInsert != null; + List responseListInsert = ksqlApi() + .openKsqlResponsePipe(CLUSTER_NAME, pipeIdInsert.getPipeId()) + .collectList() + .block(); + assert Objects.requireNonNull(responseListInsert).size() != 0; + return this; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/BaseSource.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/BaseSource.java new file mode 100644 index 00000000000..821f2ba6486 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/BaseSource.java @@ -0,0 +1,29 @@ +package com.provectus.kafka.ui.settings; + +import static com.provectus.kafka.ui.variables.Browser.LOCAL; + +import com.provectus.kafka.ui.settings.configs.Config; +import org.aeonbits.owner.ConfigFactory; + +public abstract class BaseSource { + + public static final String CLUSTER_NAME = "local"; + public static final String CONNECT_NAME = "first"; + private static final String LOCAL_HOST = "localhost"; + public static final String REMOTE_URL = String.format("http://%s:4444/wd/hub", LOCAL_HOST); + public static final String BASE_API_URL = String.format("http://%s:8080", LOCAL_HOST); + private static Config config; + public static final String BROWSER = config().browser(); + public static final String BASE_HOST = BROWSER.equals(LOCAL) + ? LOCAL_HOST + : "host.docker.internal"; + public static final String BASE_UI_URL = String.format("http://%s:8080", BASE_HOST); + public static final String SUITE_NAME = config().suite(); + + private static Config config() { + if (config == null) { + config = ConfigFactory.create(Config.class, System.getProperties()); + } + return config; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/configs/Config.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/configs/Config.java new file mode 100644 index 00000000000..5340db0176f --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/configs/Config.java @@ -0,0 +1,4 @@ +package com.provectus.kafka.ui.settings.configs; + +public interface Config extends Profiles { +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/configs/Profiles.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/configs/Profiles.java new file mode 100644 index 00000000000..fb9f9c1b192 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/configs/Profiles.java @@ -0,0 +1,17 @@ +package com.provectus.kafka.ui.settings.configs; + +import static com.provectus.kafka.ui.variables.Browser.CONTAINER; +import static com.provectus.kafka.ui.variables.Suite.CUSTOM; + +import org.aeonbits.owner.Config; + +public interface Profiles extends Config { + + @Key("browser") + @DefaultValue(CONTAINER) + String browser(); + + @Key("suite") + @DefaultValue(CUSTOM) + String suite(); +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/drivers/WebDriver.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/drivers/WebDriver.java new file mode 100644 index 00000000000..27f0d28532c --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/drivers/WebDriver.java @@ -0,0 +1,103 @@ +package com.provectus.kafka.ui.settings.drivers; + +import static com.codeborne.selenide.Selenide.clearBrowserCookies; +import static com.codeborne.selenide.Selenide.clearBrowserLocalStorage; +import static com.codeborne.selenide.Selenide.refresh; +import static com.provectus.kafka.ui.settings.BaseSource.BROWSER; +import static com.provectus.kafka.ui.settings.BaseSource.REMOTE_URL; +import static com.provectus.kafka.ui.variables.Browser.CONTAINER; +import static com.provectus.kafka.ui.variables.Browser.LOCAL; + +import com.codeborne.selenide.Configuration; +import com.codeborne.selenide.Selenide; +import com.codeborne.selenide.WebDriverRunner; +import com.codeborne.selenide.logevents.SelenideLogger; +import io.qameta.allure.Step; +import io.qameta.allure.selenide.AllureSelenide; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.openqa.selenium.chrome.ChromeOptions; + +@Slf4j +public abstract class WebDriver { + + @Step + public static void browserSetup() { + Configuration.headless = false; + Configuration.browser = "chrome"; + Configuration.browserSize = "1920x1080"; + Configuration.screenshots = true; + Configuration.savePageSource = false; + Configuration.pageLoadTimeout = 120000; + ChromeOptions chromeOptions = new ChromeOptions() + .addArguments("--no-sandbox") + .addArguments("--verbose") + .addArguments("--remote-allow-origins=*") + .addArguments("--disable-dev-shm-usage") + .addArguments("--disable-gpu") + .addArguments("--lang=en_US"); + switch (BROWSER) { + case (LOCAL) -> Configuration.browserCapabilities = chromeOptions; + case (CONTAINER) -> { + Configuration.remote = REMOTE_URL; + Configuration.remoteConnectionTimeout = 180000; + Map selenoidOptions = new HashMap<>(); + selenoidOptions.put("enableVNC", true); + selenoidOptions.put("enableVideo", false); + chromeOptions.setCapability("selenoid:options", selenoidOptions); + Configuration.browserCapabilities = chromeOptions; + } + default -> throw new IllegalStateException("Unexpected value: " + BROWSER); + } + } + + private static org.openqa.selenium.WebDriver getWebDriver() { + try { + return WebDriverRunner.getWebDriver(); + } catch (IllegalStateException ex) { + browserSetup(); + Selenide.open(); + return WebDriverRunner.getWebDriver(); + } + } + + @Step + public static void openUrl(String url) { + org.openqa.selenium.WebDriver driver = getWebDriver(); + if (!driver.getCurrentUrl().equals(url)) { + driver.get(url); + } + } + + @Step + public static void browserInit() { + getWebDriver(); + } + + @Step + public static void browserClear() { + clearBrowserLocalStorage(); + clearBrowserCookies(); + refresh(); + } + + @Step + public static void browserQuit() { + org.openqa.selenium.WebDriver driver = null; + try { + driver = WebDriverRunner.getWebDriver(); + } catch (Throwable ignored) { + } + if (driver != null) { + driver.quit(); + } + } + + @Step + public static void loggerSetup() { + SelenideLogger.addListener("AllureSelenide", new AllureSelenide() + .screenshots(true) + .savePageSource(false)); + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/AllureListener.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/AllureListener.java new file mode 100644 index 00000000000..61125408c66 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/AllureListener.java @@ -0,0 +1,39 @@ +package com.provectus.kafka.ui.settings.listeners; + +import static java.nio.file.Files.newInputStream; + +import com.codeborne.selenide.Screenshots; +import io.qameta.allure.Allure; +import io.qameta.allure.testng.AllureTestNg; +import java.io.File; +import java.io.IOException; +import lombok.extern.slf4j.Slf4j; +import org.testng.ITestListener; +import org.testng.ITestResult; + +@Slf4j +public class AllureListener extends AllureTestNg implements ITestListener { + + private void takeScreenshot() { + File screenshot = Screenshots.takeScreenShotAsFile(); + try { + if (screenshot != null) { + Allure.addAttachment(screenshot.getName(), newInputStream(screenshot.toPath())); + } else { + log.warn("Unable to take screenshot"); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void onTestFailure(ITestResult result) { + takeScreenshot(); + } + + @Override + public void onTestSkipped(ITestResult result) { + takeScreenshot(); + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/LoggerListener.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/LoggerListener.java new file mode 100644 index 00000000000..81f510f7525 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/LoggerListener.java @@ -0,0 +1,37 @@ +package com.provectus.kafka.ui.settings.listeners; + +import lombok.extern.slf4j.Slf4j; +import org.testng.ITestResult; +import org.testng.TestListenerAdapter; + +@Slf4j +public class LoggerListener extends TestListenerAdapter { + + @Override + public void onTestStart(final ITestResult testResult) { + log.info(String.format("\n------------------------------------------------------------------------ " + + "\nTEST STARTED: %s.%s \n------------------------------------------------------------------------ \n", + testResult.getInstanceName(), testResult.getName())); + } + + @Override + public void onTestSuccess(final ITestResult testResult) { + log.info(String.format("\n------------------------------------------------------------------------ " + + "\nTEST PASSED: %s.%s \n------------------------------------------------------------------------ \n", + testResult.getInstanceName(), testResult.getName())); + } + + @Override + public void onTestFailure(final ITestResult testResult) { + log.info(String.format("\n------------------------------------------------------------------------ " + + "\nTEST FAILED: %s.%s \n------------------------------------------------------------------------ \n", + testResult.getInstanceName(), testResult.getName())); + } + + @Override + public void onTestSkipped(final ITestResult testResult) { + log.info(String.format("\n------------------------------------------------------------------------ " + + "\nTEST SKIPPED: %s.%s \n------------------------------------------------------------------------ \n", + testResult.getInstanceName(), testResult.getName())); + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseCreateListener.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseCreateListener.java new file mode 100644 index 00000000000..ac3f84eba4e --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseCreateListener.java @@ -0,0 +1,136 @@ +package com.provectus.kafka.ui.settings.listeners; + +import static io.qase.api.utils.IntegrationUtils.getCaseTitle; + +import com.provectus.kafka.ui.utilities.qase.annotations.Automation; +import com.provectus.kafka.ui.utilities.qase.annotations.Status; +import com.provectus.kafka.ui.utilities.qase.annotations.Suite; +import io.qase.api.QaseClient; +import io.qase.api.StepStorage; +import io.qase.api.annotation.QaseId; +import io.qase.client.ApiClient; +import io.qase.client.api.CasesApi; +import io.qase.client.model.GetCasesFiltersParameter; +import io.qase.client.model.ResultCreateStepsInner; +import io.qase.client.model.TestCase; +import io.qase.client.model.TestCaseCreate; +import io.qase.client.model.TestCaseCreateStepsInner; +import io.qase.client.model.TestCaseListResponse; +import io.qase.client.model.TestCaseListResponseAllOfResult; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.testng.Assert; +import org.testng.ITestListener; +import org.testng.ITestResult; +import org.testng.TestListenerAdapter; + +@Slf4j +public class QaseCreateListener extends TestListenerAdapter implements ITestListener { + + private static final CasesApi QASE_API = getQaseApi(); + + private static CasesApi getQaseApi() { + ApiClient apiClient = QaseClient.getApiClient(); + apiClient.setApiKey(System.getProperty("QASEIO_API_TOKEN")); + return new CasesApi(apiClient); + } + + private static int getStatus(Method method) { + if (method.isAnnotationPresent(Status.class)) { + return method.getDeclaredAnnotation(Status.class).status().getValue(); + } + return 1; + } + + private static int getAutomation(Method method) { + if (method.isAnnotationPresent(Automation.class)) { + return method.getDeclaredAnnotation(Automation.class).state().getValue(); + } + return 0; + } + + @SneakyThrows + private static HashMap getCaseTitlesAndIdsFromQase() { + HashMap cases = new HashMap<>(); + boolean getCases = true; + int offSet = 0; + while (getCases) { + getCases = false; + TestCaseListResponse response = QASE_API.getCases(System.getProperty("QASE_PROJECT_CODE"), + new GetCasesFiltersParameter().status(GetCasesFiltersParameter.SERIALIZED_NAME_STATUS), 100, offSet); + TestCaseListResponseAllOfResult result = response.getResult(); + Assert.assertNotNull(result); + List entities = result.getEntities(); + Assert.assertNotNull(entities); + if (entities.size() > 0) { + for (TestCase testCase : entities) { + cases.put(testCase.getId(), testCase.getTitle()); + } + offSet = offSet + 100; + getCases = true; + } + } + return cases; + } + + private static boolean isCaseWithTitleExistInQase(Method method) { + HashMap cases = getCaseTitlesAndIdsFromQase(); + String title = getCaseTitle(method); + if (cases.containsValue(title)) { + for (Map.Entry map : cases.entrySet()) { + if (map.getValue().matches(title)) { + long id = map.getKey(); + log.warn(String.format("Test case with @QaseTitle='%s' already exists with @QaseId=%d. " + + "Please verify @QaseTitle annotation", title, id)); + return true; + } + } + } + return false; + } + + @Override + @SneakyThrows + public void onTestSuccess(final ITestResult testResult) { + Method method = testResult.getMethod() + .getConstructorOrMethod() + .getMethod(); + String title = getCaseTitle(method); + if (!method.isAnnotationPresent(QaseId.class)) { + if (title != null) { + if (!isCaseWithTitleExistInQase(method)) { + LinkedList resultSteps = StepStorage.stopSteps(); + LinkedList createSteps = new LinkedList<>(); + resultSteps.forEach(step -> { + TestCaseCreateStepsInner caseStep = new TestCaseCreateStepsInner(); + caseStep.setAction(step.getAction()); + caseStep.setExpectedResult(step.getExpectedResult()); + createSteps.add(caseStep); + }); + TestCaseCreate newCase = new TestCaseCreate(); + newCase.setTitle(title); + newCase.setStatus(getStatus(method)); + newCase.setAutomation(getAutomation(method)); + newCase.setSteps(createSteps); + if (method.isAnnotationPresent(Suite.class)) { + long suiteId = method.getDeclaredAnnotation(Suite.class).id(); + newCase.suiteId(suiteId); + } + Long id = Objects.requireNonNull(QASE_API.createCase(System.getProperty("QASE_PROJECT_CODE"), + newCase).getResult()).getId(); + log.info(String.format("New test case '%s' was created with @QaseId=%d", title, id)); + } + } else { + log.warn("To create new test case in Qase.io please add @QaseTitle annotation"); + } + } else { + log.warn("To create new test case in Qase.io please remove @QaseId annotation"); + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseResultListener.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseResultListener.java new file mode 100644 index 00000000000..d413ca4a28c --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseResultListener.java @@ -0,0 +1,105 @@ +package com.provectus.kafka.ui.settings.listeners; + +import static io.qase.api.utils.IntegrationUtils.getCaseId; +import static io.qase.api.utils.IntegrationUtils.getCaseTitle; +import static io.qase.api.utils.IntegrationUtils.getStacktrace; +import static io.qase.client.model.ResultCreate.StatusEnum.FAILED; +import static io.qase.client.model.ResultCreate.StatusEnum.PASSED; +import static io.qase.client.model.ResultCreate.StatusEnum.SKIPPED; + +import io.qase.api.StepStorage; +import io.qase.api.config.QaseConfig; +import io.qase.api.services.QaseTestCaseListener; +import io.qase.client.model.ResultCreate; +import io.qase.client.model.ResultCreateCase; +import io.qase.client.model.ResultCreateStepsInner; +import io.qase.testng.guice.module.TestNgModule; +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.Optional; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.testng.ITestContext; +import org.testng.ITestListener; +import org.testng.ITestResult; +import org.testng.TestListenerAdapter; + +@Slf4j +public class QaseResultListener extends TestListenerAdapter implements ITestListener { + + private static final String REPORTER_NAME = "TestNG"; + + static { + System.setProperty(QaseConfig.QASE_CLIENT_REPORTER_NAME_KEY, REPORTER_NAME); + } + + @Getter(lazy = true, value = AccessLevel.PRIVATE) + private final QaseTestCaseListener qaseTestCaseListener = createQaseListener(); + + private static QaseTestCaseListener createQaseListener() { + return TestNgModule.getInjector().getInstance(QaseTestCaseListener.class); + } + + @Override + public void onTestStart(ITestResult tr) { + getQaseTestCaseListener().onTestCaseStarted(); + super.onTestStart(tr); + } + + @Override + public void onTestSuccess(ITestResult tr) { + getQaseTestCaseListener() + .onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, PASSED)); + super.onTestSuccess(tr); + } + + @Override + public void onTestSkipped(ITestResult tr) { + getQaseTestCaseListener() + .onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, SKIPPED)); + super.onTestSuccess(tr); + } + + @Override + public void onTestFailure(ITestResult tr) { + getQaseTestCaseListener() + .onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, FAILED)); + super.onTestFailure(tr); + } + + @Override + public void onFinish(ITestContext testContext) { + getQaseTestCaseListener().onTestCasesSetFinished(); + super.onFinish(testContext); + } + + private void setupResultItem(ResultCreate resultCreate, ITestResult result, ResultCreate.StatusEnum status) { + Optional resultThrowable = Optional.ofNullable(result.getThrowable()); + String comment = resultThrowable + .flatMap(throwable -> Optional.of(throwable.toString())).orElse(null); + Boolean isDefect = resultThrowable + .flatMap(throwable -> Optional.of(throwable instanceof AssertionError)) + .orElse(false); + String stacktrace = resultThrowable + .flatMap(throwable -> Optional.of(getStacktrace(throwable))) + .orElse(null); + Method method = result.getMethod() + .getConstructorOrMethod() + .getMethod(); + Long caseId = getCaseId(method); + String caseTitle = null; + if (caseId == null) { + caseTitle = getCaseTitle(method); + } + LinkedList steps = StepStorage.stopSteps(); + resultCreate + ._case(caseTitle == null ? null : new ResultCreateCase().title(caseTitle)) + .caseId(caseId) + .status(status) + .comment(comment) + .stacktrace(stacktrace) + .steps(steps.isEmpty() ? null : steps) + .defect(isDefect); + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/FileUtils.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/FileUtils.java new file mode 100644 index 00000000000..8bb57809b89 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/FileUtils.java @@ -0,0 +1,26 @@ +package com.provectus.kafka.ui.utilities; + +import static org.apache.kafka.common.utils.Utils.readFileAsString; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.testcontainers.shaded.org.apache.commons.io.IOUtils; + +public class FileUtils { + + public static String getResourceAsString(String resourceFileName) { + try { + return IOUtils.resourceToString("/" + resourceFileName, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static String fileToString(String path) { + try { + return readFileAsString(path); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/StringUtils.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/StringUtils.java new file mode 100644 index 00000000000..77a46b805e9 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/StringUtils.java @@ -0,0 +1,15 @@ +package com.provectus.kafka.ui.utilities; + +import java.util.stream.IntStream; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class StringUtils { + + public static String getMixedCase(String original) { + return IntStream.range(0, original.length()) + .mapToObj(i -> i % 2 == 0 ? Character.toUpperCase(original.charAt(i)) : original.charAt(i)) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/TimeUtils.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/TimeUtils.java new file mode 100644 index 00000000000..7f72f8a4d6f --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/TimeUtils.java @@ -0,0 +1,16 @@ +package com.provectus.kafka.ui.utilities; + +import static com.codeborne.selenide.Selenide.sleep; + +import java.time.LocalTime; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TimeUtils { + + public static void waitUntilNewMinuteStarted() { + int secondsLeft = 60 - LocalTime.now().getSecond(); + log.debug("\nwaitUntilNewMinuteStarted: {}s", secondsLeft); + sleep(secondsLeft * 1000); + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/WebUtils.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/WebUtils.java new file mode 100644 index 00000000000..a1b1523aa51 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/WebUtils.java @@ -0,0 +1,109 @@ +package com.provectus.kafka.ui.utilities; + +import static com.codeborne.selenide.Selenide.executeJavaScript; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.codeborne.selenide.WebDriverRunner; +import java.time.Duration; +import lombok.extern.slf4j.Slf4j; +import org.openqa.selenium.Keys; +import org.openqa.selenium.interactions.Actions; + +@Slf4j +public class WebUtils { + + public static int getTimeout(int... timeoutInSeconds) { + return (timeoutInSeconds != null && timeoutInSeconds.length > 0) ? timeoutInSeconds[0] : 4; + } + + public static void sendKeysAfterClear(SelenideElement element, String keys) { + log.debug("\nsendKeysAfterClear: {} \nsend keys '{}'", element.getSearchCriteria(), keys); + element.shouldBe(Condition.enabled).clear(); + if (keys != null) { + element.sendKeys(keys); + } + } + + public static void clickByActions(SelenideElement element) { + log.debug("\nclickByActions: {}", element.getSearchCriteria()); + element.shouldBe(Condition.enabled); + new Actions(WebDriverRunner.getWebDriver()) + .moveToElement(element) + .click(element) + .perform(); + } + + public static void sendKeysByActions(SelenideElement element, String keys) { + log.debug("\nsendKeysByActions: {} \nsend keys '{}'", element.getSearchCriteria(), keys); + element.shouldBe(Condition.enabled); + new Actions(WebDriverRunner.getWebDriver()) + .moveToElement(element) + .sendKeys(element, keys) + .perform(); + } + + public static void clickByJavaScript(SelenideElement element) { + log.debug("\nclickByJavaScript: {}", element.getSearchCriteria()); + element.shouldBe(Condition.enabled); + String script = "arguments[0].click();"; + executeJavaScript(script, element); + } + + public static void clearByKeyboard(SelenideElement field) { + log.debug("\nclearByKeyboard: {}", field.getSearchCriteria()); + field.shouldBe(Condition.enabled).sendKeys(Keys.END); + field.sendKeys(Keys.chord(Keys.CONTROL + "a"), Keys.DELETE); + } + + public static boolean isVisible(SelenideElement element, int... timeoutInSeconds) { + log.debug("\nisVisible: {}", element.getSearchCriteria()); + boolean isVisible = false; + try { + element.shouldBe(Condition.visible, + Duration.ofSeconds(getTimeout(timeoutInSeconds))); + isVisible = true; + } catch (Throwable e) { + log.debug("{} is not visible", element.getSearchCriteria()); + } + return isVisible; + } + + public static boolean isEnabled(SelenideElement element, int... timeoutInSeconds) { + log.debug("\nisEnabled: {}", element.getSearchCriteria()); + boolean isEnabled = false; + try { + element.shouldBe(Condition.enabled, + Duration.ofSeconds(getTimeout(timeoutInSeconds))); + isEnabled = true; + } catch (Throwable e) { + log.debug("{} is not enabled", element.getSearchCriteria()); + } + return isEnabled; + } + + public static boolean isSelected(SelenideElement element, int... timeoutInSeconds) { + log.debug("\nisSelected: {}", element.getSearchCriteria()); + boolean isSelected = false; + try { + element.shouldBe(Condition.selected, + Duration.ofSeconds(getTimeout(timeoutInSeconds))); + isSelected = true; + } catch (Throwable e) { + log.debug("{} is not selected", element.getSearchCriteria()); + } + return isSelected; + } + + public static void selectElement(SelenideElement element, boolean select) { + if (select) { + if (!element.isSelected()) { + clickByJavaScript(element); + } + } else { + if (element.isSelected()) { + clickByJavaScript(element); + } + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/QaseSetup.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/QaseSetup.java new file mode 100644 index 00000000000..f0ed509aada --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/QaseSetup.java @@ -0,0 +1,33 @@ +package com.provectus.kafka.ui.utilities.qase; + +import static com.provectus.kafka.ui.settings.BaseSource.SUITE_NAME; +import static com.provectus.kafka.ui.variables.Suite.MANUAL; +import static org.apache.commons.lang3.BooleanUtils.FALSE; +import static org.apache.commons.lang3.BooleanUtils.TRUE; +import static org.apache.commons.lang3.StringUtils.isEmpty; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class QaseSetup { + + public static void qaseIntegrationSetup() { + String qaseApiToken = System.getProperty("QASEIO_API_TOKEN"); + if (isEmpty(qaseApiToken)) { + log.warn("Integration with Qase is disabled due to run config or token wasn't defined."); + System.setProperty("QASE_ENABLE", FALSE); + } else { + log.warn("Integration with Qase is enabled. Find this run at https://app.qase.io/run/KAFKAUI."); + String automation = SUITE_NAME.equalsIgnoreCase(MANUAL) ? "" : "Automation "; + System.setProperty("QASE_ENABLE", TRUE); + System.setProperty("QASE_PROJECT_CODE", "KAFKAUI"); + System.setProperty("QASE_API_TOKEN", qaseApiToken); + System.setProperty("QASE_USE_BULK", TRUE); + System.setProperty("QASE_RUN_NAME", DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm") + .format(OffsetDateTime.now(ZoneOffset.UTC)) + ": " + automation + SUITE_NAME.toUpperCase() + " suite"); + } + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Automation.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Automation.java new file mode 100644 index 00000000000..1b8d5b65b81 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Automation.java @@ -0,0 +1,14 @@ +package com.provectus.kafka.ui.utilities.qase.annotations; + +import com.provectus.kafka.ui.utilities.qase.enums.State; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Automation { + + State state(); +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Status.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Status.java new file mode 100644 index 00000000000..df078d04368 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Status.java @@ -0,0 +1,13 @@ +package com.provectus.kafka.ui.utilities.qase.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Status { + + com.provectus.kafka.ui.utilities.qase.enums.Status status(); +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Suite.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Suite.java new file mode 100644 index 00000000000..fcaff09744a --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Suite.java @@ -0,0 +1,13 @@ +package com.provectus.kafka.ui.utilities.qase.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Suite { + + long id(); +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/enums/State.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/enums/State.java new file mode 100644 index 00000000000..11f49d56471 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/enums/State.java @@ -0,0 +1,18 @@ +package com.provectus.kafka.ui.utilities.qase.enums; + +public enum State { + + NOT_AUTOMATED(0), + TO_BE_AUTOMATED(1), + AUTOMATED(2); + + private final int value; + + State(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/enums/Status.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/enums/Status.java new file mode 100644 index 00000000000..a4e7e0cce32 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/enums/Status.java @@ -0,0 +1,18 @@ +package com.provectus.kafka.ui.utilities.qase.enums; + +public enum Status { + + ACTUAL(0), + DRAFT(1), + DEPRECATED(2); + + private final int value; + + Status(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Browser.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Browser.java new file mode 100644 index 00000000000..f435dcbf019 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Browser.java @@ -0,0 +1,7 @@ +package com.provectus.kafka.ui.variables; + +public interface Browser { + + String CONTAINER = "container"; + String LOCAL = "local"; +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Expected.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Expected.java new file mode 100644 index 00000000000..3e5b7726116 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Expected.java @@ -0,0 +1,15 @@ +package com.provectus.kafka.ui.variables; + +public interface Expected { + + String BROKER_SOURCE_INFO_TOOLTIP = + "DYNAMIC_TOPIC_CONFIG = dynamic topic config that is configured for a specific topic\n" + + "DYNAMIC_BROKER_LOGGER_CONFIG = dynamic broker logger config that is configured for a specific broker\n" + + "DYNAMIC_BROKER_CONFIG = dynamic broker config that is configured for a specific broker\n" + + "DYNAMIC_DEFAULT_BROKER_CONFIG = dynamic broker config that is configured as default " + + "for all brokers in the cluster\n" + + "STATIC_BROKER_CONFIG = static broker config provided as broker properties at start up " + + "(e.g. server.properties file)\n" + + "DEFAULT_CONFIG = built-in default configuration for configs that have a default value\n" + + "UNKNOWN = source unknown e.g. in the ConfigEntry used for alter requests where source is not set"; +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Suite.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Suite.java new file mode 100644 index 00000000000..3d337548c94 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Suite.java @@ -0,0 +1,10 @@ +package com.provectus.kafka.ui.variables; + +public interface Suite { + + String CUSTOM = "custom"; + String MANUAL = "manual"; + String REGRESSION = "regression"; + String SANITY = "sanity"; + String SMOKE = "smoke"; +} diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Url.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Url.java new file mode 100644 index 00000000000..5b800608f38 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Url.java @@ -0,0 +1,11 @@ +package com.provectus.kafka.ui.variables; + +public interface Url { + + String BROKERS_LIST_URL = "http://%s:8080/ui/clusters/local/brokers"; + String TOPICS_LIST_URL = "http://%s:8080/ui/clusters/local/all-topics"; + String CONSUMERS_LIST_URL = "http://%s:8080/ui/clusters/local/consumer-groups"; + String SCHEMA_REGISTRY_LIST_URL = "http://%s:8080/ui/clusters/local/schemas"; + String KAFKA_CONNECT_LIST_URL = "http://%s:8080/ui/clusters/local/connectors"; + String KSQL_DB_LIST_URL = "http://%s:8080/ui/clusters/local/ksqldb/tables"; +} diff --git a/kafka-ui-e2e-checks/src/main/resources/config_for_create_connector.json b/kafka-ui-e2e-checks/src/main/resources/config_for_create_connector.json deleted file mode 100644 index 832c4ffb710..00000000000 --- a/kafka-ui-e2e-checks/src/main/resources/config_for_create_connector.json +++ /dev/null @@ -1,17 +0,0 @@ -{ -"connector.class": "io.confluent.connect.jdbc.JdbcSinkConnector", -"connection.url": "jdbc:postgresql://postgres-db:5432/test", -"connection.user": "dev_user", -"connection.password": "12345", -"topics": "topic_for_connector", -"table.name.format": "sink_activities_e2e_test_connector_creating", -"key.converter": "org.apache.kafka.connect.storage.StringConverter", -"key.converter.schema.registry.url": "http://schemaregistry0:8085", -"value.converter": "org.apache.kafka.connect.json.JsonConverter", -"value.converter.schema.registry.url": "http://schemaregistry0:8085", -"auto.create": "true", -"pk.mode": "record_value", -"pk.fields": "id", -"insert.mode": "upsert", -"errors.log.enable": "true", -"errors.log.include.messages": "true" \ No newline at end of file diff --git a/kafka-ui-e2e-checks/src/main/resources/config_for_create_connector_via_api.json b/kafka-ui-e2e-checks/src/main/resources/config_for_create_connector_via_api.json deleted file mode 100644 index be759ed8c76..00000000000 --- a/kafka-ui-e2e-checks/src/main/resources/config_for_create_connector_via_api.json +++ /dev/null @@ -1,18 +0,0 @@ -{ -"connector.class": "io.confluent.connect.jdbc.JdbcSinkConnector", -"connection.url": "jdbc:postgresql://postgres-db:5432/test", -"connection.user": "dev_user", -"connection.password": "12345", -"topics": "topic_for_connector", -"table.name.format": "sink_activities_e2e_test_connector_updating", -"key.converter": "org.apache.kafka.connect.storage.StringConverter", -"key.converter.schema.registry.url": "http://schemaregistry0:8085", -"value.converter": "org.apache.kafka.connect.json.JsonConverter", -"value.converter.schema.registry.url": "http://schemaregistry0:8085", -"auto.create": "true", -"pk.mode": "record_value", -"pk.fields": "id", -"insert.mode": "upsert", -"errors.log.enable": "true", -"errors.log.include.messages": "true" -} \ No newline at end of file diff --git a/kafka-ui-e2e-checks/src/main/resources/config_for_update_connector.json b/kafka-ui-e2e-checks/src/main/resources/config_for_update_connector.json deleted file mode 100644 index dce01cc6ccf..00000000000 --- a/kafka-ui-e2e-checks/src/main/resources/config_for_update_connector.json +++ /dev/null @@ -1,17 +0,0 @@ -{ -"connector.class": "io.confluent.connect.jdbc.JdbcSinkConnector", -"connection.url": "jdbc:postgresql://postgres-db:5432/test", -"connection.user": "dev_user", -"connection.password": "12345", -"topics": "topic_for_update_connector", -"table.name.format": "sink_activities_e2e_test_connector_updating", -"key.converter": "org.apache.kafka.connect.storage.StringConverter", -"key.converter.schema.registry.url": "http://schemaregistry0:8085", -"value.converter": "org.apache.kafka.connect.json.JsonConverter", -"value.converter.schema.registry.url": "http://schemaregistry0:8085", -"auto.create": "true", -"pk.mode": "record_value", -"pk.fields": "id", -"insert.mode": "upsert", -"errors.log.enable": "true", -"errors.log.include.messages": "true" diff --git a/kafka-ui-e2e-checks/src/main/resources/testData/connectors/config_for_create_connector.json b/kafka-ui-e2e-checks/src/main/resources/testData/connectors/config_for_create_connector.json new file mode 100644 index 00000000000..096d4191e45 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/resources/testData/connectors/config_for_create_connector.json @@ -0,0 +1,18 @@ +{ + "connector.class": "io.confluent.connect.jdbc.JdbcSinkConnector", + "connection.url": "jdbc:postgresql://postgres-db:5432/test", + "connection.user": "dev_user", + "connection.password": "12345", + "topics": "topic_for_connector", + "table.name.format": "sink_activities_e2e_test_connector_creating", + "key.converter": "org.apache.kafka.connect.storage.StringConverter", + "key.converter.schema.registry.url": "http://schemaregistry0:8085", + "value.converter": "org.apache.kafka.connect.json.JsonConverter", + "value.converter.schema.registry.url": "http://schemaregistry0:8085", + "auto.create": "true", + "pk.mode": "record_value", + "pk.fields": "id", + "insert.mode": "upsert", + "errors.log.enable": "true", + "errors.log.include.messages": "true" +} diff --git a/kafka-ui-e2e-checks/src/main/resources/testData/connectors/config_for_create_connector_via_api.json b/kafka-ui-e2e-checks/src/main/resources/testData/connectors/config_for_create_connector_via_api.json new file mode 100644 index 00000000000..dffd66cae55 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/resources/testData/connectors/config_for_create_connector_via_api.json @@ -0,0 +1,7 @@ +{ + "connector.class": "io.confluent.connect.jdbc.JdbcSinkConnector", + "connection.url": "jdbc:postgresql://postgres-db:5432/test", + "connection.user": "dev_user", + "connection.password": "12345", + "topics": "topic_for_connector" +} diff --git a/kafka-ui-e2e-checks/src/main/resources/testData/connectors/config_for_update_connector.json b/kafka-ui-e2e-checks/src/main/resources/testData/connectors/config_for_update_connector.json new file mode 100644 index 00000000000..141b24b3d5d --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/resources/testData/connectors/config_for_update_connector.json @@ -0,0 +1,18 @@ +{ + "connector.class": "io.confluent.connect.jdbc.JdbcSinkConnector", + "connection.url": "jdbc:postgresql://postgres-db:5432/test", + "connection.user": "dev_user", + "connection.password": "12345", + "topics": "topic_for_update_connector", + "table.name.format": "sink_activities_e2e_test_connector_updating", + "key.converter": "org.apache.kafka.connect.storage.StringConverter", + "key.converter.schema.registry.url": "http://schemaregistry0:8085", + "value.converter": "org.apache.kafka.connect.json.JsonConverter", + "value.converter.schema.registry.url": "http://schemaregistry0:8085", + "auto.create": "true", + "pk.mode": "record_value", + "pk.fields": "id", + "insert.mode": "upsert", + "errors.log.enable": "true", + "errors.log.include.messages": "true" +} diff --git a/kafka-ui-e2e-checks/src/main/resources/delete_connector_config.json b/kafka-ui-e2e-checks/src/main/resources/testData/connectors/delete_connector_config.json similarity index 96% rename from kafka-ui-e2e-checks/src/main/resources/delete_connector_config.json rename to kafka-ui-e2e-checks/src/main/resources/testData/connectors/delete_connector_config.json index ad696e5b827..121613fc1c3 100644 --- a/kafka-ui-e2e-checks/src/main/resources/delete_connector_config.json +++ b/kafka-ui-e2e-checks/src/main/resources/testData/connectors/delete_connector_config.json @@ -9,10 +9,10 @@ "key.converter.schema.registry.url": "http://schemaregistry0:8085", "value.converter": "org.apache.kafka.connect.json.JsonConverter", "value.converter.schema.registry.url": "http://schemaregistry0:8085", - "auto.create": "true", + "auto.create": "false", "pk.mode": "record_value", "pk.fields": "id", "insert.mode": "upsert", "errors.log.enable": "true", "errors.log.include.messages": "true" -} \ No newline at end of file +} diff --git a/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_avro_for_update.json b/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_avro_for_update.json new file mode 100644 index 00000000000..bbfe011c9f7 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_avro_for_update.json @@ -0,0 +1,17 @@ +{ + "type": "record", + "name": "Message", + "namespace": "com.provectus.kafka", + "fields": [ + { + "name": "text", + "type": "string", + "default": null + }, + { + "name": "value", + "type": "string", + "default": null + } + ] +} diff --git a/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_avro_value.json b/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_avro_value.json new file mode 100644 index 00000000000..d84caf2ea80 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_avro_value.json @@ -0,0 +1,15 @@ +{ + "type": "record", + "name": "Student", + "namespace": "DataFlair", + "fields": [ + { + "name": "Name", + "type": "string" + }, + { + "name": "Age", + "type": "int" + } + ] +} diff --git a/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_json_Value.json b/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_json_Value.json new file mode 100644 index 00000000000..dffd66cae55 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_json_Value.json @@ -0,0 +1,7 @@ +{ + "connector.class": "io.confluent.connect.jdbc.JdbcSinkConnector", + "connection.url": "jdbc:postgresql://postgres-db:5432/test", + "connection.user": "dev_user", + "connection.password": "12345", + "topics": "topic_for_connector" +} diff --git a/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_protobuf_value.txt b/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_protobuf_value.txt new file mode 100644 index 00000000000..ec0000121c2 --- /dev/null +++ b/kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_protobuf_value.txt @@ -0,0 +1,5 @@ +enum SchemaType { + AVRO = 0; + JSON = 1; + PROTOBUF = 2; + } \ No newline at end of file diff --git a/kafka-ui-e2e-checks/src/main/resources/message_content_create_topic.json b/kafka-ui-e2e-checks/src/main/resources/testData/topics/message_content_create_topic.json similarity index 99% rename from kafka-ui-e2e-checks/src/main/resources/message_content_create_topic.json rename to kafka-ui-e2e-checks/src/main/resources/testData/topics/message_content_create_topic.json index 8c8fabe5bfb..a4c0d940f4d 100644 --- a/kafka-ui-e2e-checks/src/main/resources/message_content_create_topic.json +++ b/kafka-ui-e2e-checks/src/main/resources/testData/topics/message_content_create_topic.json @@ -21,4 +21,4 @@ "id":"1", "value":"kafka" } -} \ No newline at end of file +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/BaseTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/BaseTest.java new file mode 100644 index 00000000000..45daf6a4b5c --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/BaseTest.java @@ -0,0 +1,153 @@ +package com.provectus.kafka.ui; + +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.BROKERS; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.CONSUMERS; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KAFKA_CONNECT; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KSQL_DB; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.SCHEMA_REGISTRY; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.TOPICS; +import static com.provectus.kafka.ui.settings.BaseSource.BASE_UI_URL; +import static com.provectus.kafka.ui.settings.drivers.WebDriver.browserClear; +import static com.provectus.kafka.ui.settings.drivers.WebDriver.browserQuit; +import static com.provectus.kafka.ui.settings.drivers.WebDriver.browserSetup; +import static com.provectus.kafka.ui.settings.drivers.WebDriver.loggerSetup; +import static com.provectus.kafka.ui.utilities.qase.QaseSetup.qaseIntegrationSetup; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.Selenide; +import com.codeborne.selenide.SelenideElement; +import com.provectus.kafka.ui.settings.listeners.AllureListener; +import com.provectus.kafka.ui.settings.listeners.LoggerListener; +import com.provectus.kafka.ui.settings.listeners.QaseResultListener; +import io.qameta.allure.Step; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.AfterSuite; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeSuite; +import org.testng.annotations.Listeners; +import org.testng.asserts.SoftAssert; + +@Slf4j +@Listeners({AllureListener.class, LoggerListener.class, QaseResultListener.class}) +public abstract class BaseTest extends Facade { + + @BeforeSuite(alwaysRun = true) + public void beforeSuite() { + qaseIntegrationSetup(); + loggerSetup(); + browserSetup(); + } + + @AfterSuite(alwaysRun = true) + public void afterSuite() { + browserQuit(); + } + + @BeforeMethod(alwaysRun = true) + public void beforeMethod() { + Selenide.open(BASE_UI_URL); + naviSideBar.waitUntilScreenReady(); + } + + @AfterMethod(alwaysRun = true) + public void afterMethod() { + browserClear(); + } + + @Step + protected void navigateToBrokers() { + naviSideBar + .openSideMenu(BROKERS); + brokersList + .waitUntilScreenReady(); + } + + @Step + protected void navigateToBrokersAndOpenDetails(int brokerId) { + naviSideBar + .openSideMenu(BROKERS); + brokersList + .waitUntilScreenReady() + .openBroker(brokerId); + brokersDetails + .waitUntilScreenReady(); + } + + @Step + protected void navigateToTopics() { + naviSideBar + .openSideMenu(TOPICS); + topicsList + .waitUntilScreenReady() + .setShowInternalRadioButton(false); + } + + @Step + protected void navigateToTopicsAndOpenDetails(String topicName) { + navigateToTopics(); + topicsList + .openTopic(topicName); + topicDetails + .waitUntilScreenReady(); + } + + @Step + protected void navigateToConsumers() { + naviSideBar + .openSideMenu(CONSUMERS); + consumersList + .waitUntilScreenReady(); + } + + @Step + protected void navigateToSchemaRegistry() { + naviSideBar + .openSideMenu(SCHEMA_REGISTRY); + schemaRegistryList + .waitUntilScreenReady(); + } + + @Step + protected void navigateToSchemaRegistryAndOpenDetails(String schemaName) { + navigateToSchemaRegistry(); + schemaRegistryList + .openSchema(schemaName); + schemaDetails + .waitUntilScreenReady(); + } + + @Step + protected void navigateToConnectors() { + naviSideBar + .openSideMenu(KAFKA_CONNECT); + kafkaConnectList + .waitUntilScreenReady(); + } + + @Step + protected void navigateToConnectorsAndOpenDetails(String connectorName) { + navigateToConnectors(); + kafkaConnectList + .openConnector(connectorName); + connectorDetails + .waitUntilScreenReady(); + } + + @Step + protected void navigateToKsqlDb() { + naviSideBar + .openSideMenu(KSQL_DB); + ksqlDbList + .waitUntilScreenReady(); + } + + @Step + protected void verifyElementsCondition(List elementList, Condition expectedCondition) { + SoftAssert softly = new SoftAssert(); + elementList.forEach(element -> softly.assertTrue(element.is(expectedCondition), + element.getSearchCriteria() + " is " + expectedCondition)); + softly.assertAll(); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/Facade.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/Facade.java new file mode 100644 index 00000000000..abc0b0aa6bc --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/Facade.java @@ -0,0 +1,48 @@ +package com.provectus.kafka.ui; + +import com.provectus.kafka.ui.pages.brokers.BrokersConfigTab; +import com.provectus.kafka.ui.pages.brokers.BrokersDetails; +import com.provectus.kafka.ui.pages.brokers.BrokersList; +import com.provectus.kafka.ui.pages.connectors.ConnectorCreateForm; +import com.provectus.kafka.ui.pages.connectors.ConnectorDetails; +import com.provectus.kafka.ui.pages.connectors.KafkaConnectList; +import com.provectus.kafka.ui.pages.consumers.ConsumersDetails; +import com.provectus.kafka.ui.pages.consumers.ConsumersList; +import com.provectus.kafka.ui.pages.ksqldb.KsqlDbList; +import com.provectus.kafka.ui.pages.ksqldb.KsqlQueryForm; +import com.provectus.kafka.ui.pages.panels.NaviSideBar; +import com.provectus.kafka.ui.pages.panels.TopPanel; +import com.provectus.kafka.ui.pages.schemas.SchemaCreateForm; +import com.provectus.kafka.ui.pages.schemas.SchemaDetails; +import com.provectus.kafka.ui.pages.schemas.SchemaRegistryList; +import com.provectus.kafka.ui.pages.topics.ProduceMessagePanel; +import com.provectus.kafka.ui.pages.topics.TopicCreateEditForm; +import com.provectus.kafka.ui.pages.topics.TopicDetails; +import com.provectus.kafka.ui.pages.topics.TopicSettingsTab; +import com.provectus.kafka.ui.pages.topics.TopicsList; +import com.provectus.kafka.ui.services.ApiService; + +public abstract class Facade { + + protected ApiService apiService = new ApiService(); + protected ConnectorCreateForm connectorCreateForm = new ConnectorCreateForm(); + protected KafkaConnectList kafkaConnectList = new KafkaConnectList(); + protected ConnectorDetails connectorDetails = new ConnectorDetails(); + protected SchemaCreateForm schemaCreateForm = new SchemaCreateForm(); + protected SchemaDetails schemaDetails = new SchemaDetails(); + protected SchemaRegistryList schemaRegistryList = new SchemaRegistryList(); + protected ProduceMessagePanel produceMessagePanel = new ProduceMessagePanel(); + protected TopicCreateEditForm topicCreateEditForm = new TopicCreateEditForm(); + protected TopicsList topicsList = new TopicsList(); + protected TopicDetails topicDetails = new TopicDetails(); + protected ConsumersDetails consumersDetails = new ConsumersDetails(); + protected ConsumersList consumersList = new ConsumersList(); + protected NaviSideBar naviSideBar = new NaviSideBar(); + protected TopPanel topPanel = new TopPanel(); + protected BrokersList brokersList = new BrokersList(); + protected BrokersDetails brokersDetails = new BrokersDetails(); + protected BrokersConfigTab brokersConfigTab = new BrokersConfigTab(); + protected TopicSettingsTab topicSettingsTab = new TopicSettingsTab(); + protected KsqlQueryForm ksqlQueryForm = new KsqlQueryForm(); + protected KsqlDbList ksqlDbList = new KsqlDbList(); +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/SmokeTests.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/SmokeTests.java deleted file mode 100644 index af88cc03887..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/SmokeTests.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.provectus.kafka.ui; - -import com.provectus.kafka.ui.base.BaseTest; -import io.qameta.allure.Issue; -import lombok.SneakyThrows; -import org.junit.Ignore; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -@Disabled // TODO #1480 -public class SmokeTests extends BaseTest { - @Test - @SneakyThrows - @DisplayName("main page should load") - @Issue("380") - void mainPageLoads() { - pages.open() - .isOnPage(); - compareScreenshots("main"); - } - -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/base/BaseTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/base/BaseTest.java deleted file mode 100644 index a5ac78f5660..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/base/BaseTest.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.provectus.kafka.ui.base; - -import com.codeborne.selenide.Configuration; -import com.codeborne.selenide.logevents.SelenideLogger; -import com.provectus.kafka.ui.helpers.Helpers; -import com.provectus.kafka.ui.pages.Pages; -import com.provectus.kafka.ui.screenshots.Screenshooter; -import com.provectus.kafka.ui.steps.Steps; -import io.github.cdimascio.dotenv.Dotenv; -import io.qameta.allure.selenide.AllureSelenide; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.FileUtils; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.openqa.selenium.remote.DesiredCapabilities; -import org.testcontainers.containers.BindMode; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.utility.DockerImageName; - -import java.io.File; -import java.io.IOException; -import java.util.Arrays; - -@Slf4j -@DisplayNameGeneration(CamelCaseToSpacedDisplayNameGenerator.class) -public class BaseTest { - - protected Steps steps = Steps.INSTANCE; - protected Pages pages = Pages.INSTANCE; - protected Helpers helpers = Helpers.INSTANCE; - - private Screenshooter screenshooter = new Screenshooter(); - - public void compareScreenshots(String name) { - screenshooter.compareScreenshots(name); - } - - public void compareScreenshots(String name, Boolean shouldUpdateScreenshots) { - screenshooter.compareScreenshots(name, shouldUpdateScreenshots); - } - - public static GenericContainer selenoid = - new GenericContainer(DockerImageName.parse("aerokube/selenoid:latest-release")) - .withExposedPorts(4444) - .withFileSystemBind("selenoid/config/", "/etc/selenoid", BindMode.READ_WRITE) - .withFileSystemBind("/var/run/docker.sock", "/var/run/docker.sock", BindMode.READ_WRITE) - .withFileSystemBind("selenoid/video", "/opt/selenoid/video", BindMode.READ_WRITE) - .withFileSystemBind("selenoid/logs", "/opt/selenoid/logs", BindMode.READ_WRITE) - .withEnv("OVERRIDE_VIDEO_OUTPUT_DIR", "/opt/selenoid/video") - .withCommand( - "-conf", "/etc/selenoid/browsers.json", "-log-output-dir", "/opt/selenoid/logs"); - - static { - if (!new File("./.env").exists()) { - try { - FileUtils.copyFile(new File(".env.example"), new File(".env")); - } catch (IOException e) { - log.error("couldn't copy .env.example to .env. Please add .env"); - e.printStackTrace(); - } - } - Dotenv.load().entries().forEach(env -> System.setProperty(env.getKey(), env.getValue())); - if (TestConfiguration.CLEAR_REPORTS_DIR) { - clearReports(); - } - setupSelenoid(); - } - - @AfterAll - public static void afterAll() { -// closeWebDriver(); -// selenoid.close(); - } - - @SneakyThrows - private static void setupSelenoid() { - String remote = TestConfiguration.SELENOID_URL; - if (TestConfiguration.SHOULD_START_SELENOID) { - selenoid.start(); - remote = - "http://%s:%s/wd/hub" - .formatted(selenoid.getContainerIpAddress(), selenoid.getMappedPort(4444)); - } - - Configuration.reportsFolder = TestConfiguration.REPORTS_FOLDER; - if (!TestConfiguration.USE_LOCAL_BROWSER) { - Configuration.remote = remote; - TestConfiguration.BASE_URL = - TestConfiguration.BASE_URL.replace("localhost", "host.docker.internal"); - } - Configuration.screenshots = TestConfiguration.SCREENSHOTS; - Configuration.savePageSource = TestConfiguration.SAVE_PAGE_SOURCE; - Configuration.reopenBrowserOnFail = TestConfiguration.REOPEN_BROWSER_ON_FAIL; - Configuration.browser = TestConfiguration.BROWSER; - Configuration.baseUrl = TestConfiguration.BASE_URL; - Configuration.browserSize = TestConfiguration.BROWSER_SIZE; - var capabilities = new DesiredCapabilities(); -// DesiredCapabilities capabilities = DesiredCapabilities.chrome(); - capabilities.setCapability("enableVNC", TestConfiguration.ENABLE_VNC); - Configuration.browserCapabilities = capabilities; - - SelenideLogger.addListener("allure", new AllureSelenide().savePageSource(false)); - } - - public static void clearReports() { - log.info("Clearing reports dir [%s]...".formatted(TestConfiguration.REPORTS_FOLDER)); - File allureResults = new File(TestConfiguration.REPORTS_FOLDER); - if (allureResults.isDirectory()) { - File[] list = allureResults.listFiles(); - if (list != null) - Arrays.stream(list) - .sequential() - .filter(e -> !e.getName().equals("categories.json")) - .forEach(File::delete); - } - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/base/CamelCaseToSpacedDisplayNameGenerator.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/base/CamelCaseToSpacedDisplayNameGenerator.java deleted file mode 100644 index 105f15e83a9..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/base/CamelCaseToSpacedDisplayNameGenerator.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.provectus.kafka.ui.base; - -import org.junit.jupiter.api.DisplayNameGenerator; -import org.junit.platform.commons.util.ClassUtils; -import org.junit.platform.commons.util.Preconditions; - -import java.lang.reflect.Method; - -public class CamelCaseToSpacedDisplayNameGenerator implements DisplayNameGenerator { - @Override - public String generateDisplayNameForClass(Class testClass) { - String name = testClass.getName(); - int lastDot = name.lastIndexOf('.'); - return name.substring(lastDot + 1).replaceAll("([A-Z])", " $1").toLowerCase(); - } - - @Override - public String generateDisplayNameForNestedClass(Class nestedClass) { - return nestedClass.getSimpleName(); - } - - @Override - public String generateDisplayNameForMethod(Class testClass, Method testMethod) { - return testMethod.getName().replaceAll("([A-Z])", " $1").toLowerCase() - + parameterTypesAsString(testMethod); - } - - static String parameterTypesAsString(Method method) { - Preconditions.notNull(method, "Method must not be null"); - return method.getParameterTypes().length == 0 - ? "" - : '(' + ClassUtils.nullSafeToString(Class::getSimpleName, method.getParameterTypes()) + ')'; - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/base/TestConfiguration.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/base/TestConfiguration.java deleted file mode 100644 index 09c85db1c04..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/base/TestConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.provectus.kafka.ui.base; - -public class TestConfiguration { - public static boolean CLEAR_REPORTS_DIR = - Boolean.parseBoolean(System.getProperty("CLEAR_REPORTS_DIR", "true")); - - public static boolean SHOULD_START_SELENOID = - Boolean.parseBoolean(System.getProperty("SHOULD_START_SELENOID", "false")); - - public static String BASE_URL = System.getProperty("BASE_URL", "http://localhost:8080/"); - - public static boolean USE_LOCAL_BROWSER = - Boolean.parseBoolean(System.getProperty("USE_LOCAL_BROWSER", "true")); - - public static String SELENOID_URL = - System.getProperty("SELENOID_URL", "http://localhost:4444/wd/hub"); - public static String REPORTS_FOLDER = System.getProperty("REPORTS_FOLDER", "allure-results"); - public static Boolean SCREENSHOTS = - Boolean.parseBoolean(System.getProperty("SCREENSHOTS", "false")); - public static Boolean SAVE_PAGE_SOURCE = - Boolean.parseBoolean(System.getProperty("SAVE_PAGE_SOURCE", "false")); - public static Boolean REOPEN_BROWSER_ON_FAIL = - Boolean.parseBoolean(System.getProperty("REOPEN_BROWSER_ON_FAIL", "true")); - public static String BROWSER = System.getProperty("BROWSER", "chrome"); - public static String BROWSER_SIZE = System.getProperty("BROWSER_SIZE", "1920x1080"); - public static Boolean ENABLE_VNC = Boolean.parseBoolean(System.getProperty("ENABLE_VNC", "true")); -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/extensions/FileUtils.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/extensions/FileUtils.java deleted file mode 100644 index 22926226403..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/extensions/FileUtils.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.provectus.kafka.ui.extensions; - -import org.testcontainers.shaded.org.apache.commons.io.IOUtils; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; - - -public class FileUtils { - - public static String getResourceAsString(String resourceFileName) throws IOException { - return IOUtils.resourceToString("/" + resourceFileName, StandardCharsets.UTF_8); - } - -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/extensions/WaitUtils.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/extensions/WaitUtils.java deleted file mode 100644 index f6f404d4b17..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/extensions/WaitUtils.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.provectus.kafka.ui.extensions; - -import com.codeborne.selenide.Condition; -import com.codeborne.selenide.SelenideElement; -import org.junit.jupiter.api.Assertions; -import org.openqa.selenium.By; - -import static com.codeborne.selenide.Selenide.*; -import static com.codeborne.selenide.Selenide.$; - -public class WaitUtils { - public static void refreshUntil(By by, Condition condition) { - int i = 0; - do { - refresh(); - i++; - sleep(2000); - } while ($$(by).size() < 1 && i != 20); - $(by).shouldBe(condition); - } - - public static void waitForSelectedValue(SelenideElement element, String selectedValue) { - int i = 0; - do { - refresh(); - i++; - sleep(2000); - } while (!selectedValue.equals(element.getSelectedValue()) && i != 60); - Assertions.assertEquals(selectedValue, element.getSelectedValue()) ; - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/helpers/ApiHelper.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/helpers/ApiHelper.java deleted file mode 100644 index 926bf4504c6..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/helpers/ApiHelper.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.provectus.kafka.ui.helpers; - -import com.provectus.kafka.ui.api.api.KafkaConnectApi; -import com.provectus.kafka.ui.api.api.MessagesApi; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.provectus.kafka.ui.api.ApiClient; -import com.provectus.kafka.ui.api.api.TopicsApi; -import com.provectus.kafka.ui.api.model.CreateTopicMessage; -import com.provectus.kafka.ui.api.model.NewConnector; -import com.provectus.kafka.ui.api.model.TopicCreation; -import lombok.SneakyThrows; -import org.springframework.web.reactive.function.client.WebClientResponseException; - -import java.util.HashMap; -import java.util.Map; - -public class ApiHelper { - int partitions = 1; - int replicationFactor = 1; - String newTopic = "new-topic"; - String baseURL = "http://localhost:8080/"; - - @SneakyThrows - private TopicsApi topicApi() { - ApiClient defaultClient = new ApiClient(); - defaultClient.setBasePath(baseURL); - TopicsApi topicsApi = new TopicsApi(defaultClient); - return topicsApi; - } - - @SneakyThrows - public void createTopic(String clusterName, String topicName) { - TopicCreation topic = new TopicCreation(); - topic.setName(topicName); - topic.setPartitions(partitions); - topic.setReplicationFactor(replicationFactor); - topicApi().createTopic(clusterName,topic).block(); - } - - @SneakyThrows - public void deleteTopic(String clusterName, String topicName) { - try { - topicApi().deleteTopic(clusterName, topicName).block(); - } catch (WebClientResponseException ex) { - if (ex.getRawStatusCode() != 404) // except already deleted - throw ex; - } - } - - @SneakyThrows - private KafkaConnectApi connectorApi(){ - ApiClient defaultClient = new ApiClient(); - defaultClient.setBasePath(baseURL); - KafkaConnectApi connectorsApi = new KafkaConnectApi(defaultClient); - return connectorsApi; - } - - @SneakyThrows - public void deleteConnector(String clusterName, String connectName, String connectorName) { - try { - connectorApi().deleteConnector(clusterName, connectName, connectorName).block(); - } catch (WebClientResponseException ex) { - if (ex.getRawStatusCode() != 404) - throw ex; - } - } - - @SneakyThrows - public void createConnector(String clusterName, String connectName, String connectorName, String configJson) { - NewConnector connector = new NewConnector(); - connector.setName(connectorName); - Map configMap = new ObjectMapper().readValue(configJson, HashMap.class); - connector.setConfig(configMap); - connectorApi().createConnector(clusterName, connectName, connector).block(); - } - - @SneakyThrows - private MessagesApi messageApi() { - ApiClient defaultClient = new ApiClient(); - defaultClient.setBasePath(baseURL); - MessagesApi messagesApi = new MessagesApi(defaultClient); - return messagesApi; - } - - @SneakyThrows - public void sendMessage(String clusterName, String topicName, String messageContentJson, String messageKey){ - CreateTopicMessage createMessage = new CreateTopicMessage(); - createMessage.setContent(messageContentJson); - createMessage.setKey(messageKey); - messageApi().sendTopicMessages(clusterName, topicName, createMessage).block(); - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/helpers/Helpers.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/helpers/Helpers.java deleted file mode 100644 index cda55a0cb25..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/helpers/Helpers.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.provectus.kafka.ui.helpers; - - - -public class Helpers { - public static final Helpers INSTANCE = new Helpers(); - - private Helpers(){} - - public ApiHelper apiHelper = new ApiHelper(); -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/BaseManualTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/BaseManualTest.java new file mode 100644 index 00000000000..827dc1ce432 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/BaseManualTest.java @@ -0,0 +1,30 @@ +package com.provectus.kafka.ui.manualsuite; + +import static com.provectus.kafka.ui.utilities.qase.QaseSetup.qaseIntegrationSetup; +import static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED; +import static com.provectus.kafka.ui.utilities.qase.enums.State.TO_BE_AUTOMATED; + +import com.provectus.kafka.ui.settings.listeners.QaseResultListener; +import com.provectus.kafka.ui.utilities.qase.annotations.Automation; +import java.lang.reflect.Method; +import org.testng.SkipException; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeSuite; +import org.testng.annotations.Listeners; + +@Listeners(QaseResultListener.class) +public abstract class BaseManualTest { + + @BeforeSuite + public void beforeSuite() { + qaseIntegrationSetup(); + } + + @BeforeMethod + public void beforeMethod(Method method) { + if (method.getAnnotation(Automation.class).state().equals(NOT_AUTOMATED) + || method.getAnnotation(Automation.class).state().equals(TO_BE_AUTOMATED)) { + throw new SkipException("Skip test exception"); + } + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SanityBacklog.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SanityBacklog.java new file mode 100644 index 00000000000..4021c8ec94f --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SanityBacklog.java @@ -0,0 +1,7 @@ +package com.provectus.kafka.ui.manualsuite.backlog; + +import com.provectus.kafka.ui.manualsuite.BaseManualTest; + +public class SanityBacklog extends BaseManualTest { + +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java new file mode 100644 index 00000000000..eb82a50db1b --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java @@ -0,0 +1,72 @@ +package com.provectus.kafka.ui.manualsuite.backlog; + +import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.SCHEMAS_SUITE_ID; +import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.TOPICS_PROFILE_SUITE_ID; +import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.TOPICS_SUITE_ID; +import static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED; +import static com.provectus.kafka.ui.utilities.qase.enums.State.TO_BE_AUTOMATED; + +import com.provectus.kafka.ui.manualsuite.BaseManualTest; +import com.provectus.kafka.ui.utilities.qase.annotations.Automation; +import com.provectus.kafka.ui.utilities.qase.annotations.Suite; +import io.qase.api.annotation.QaseId; +import org.testng.annotations.Test; + +public class SmokeBacklog extends BaseManualTest { + + @Automation(state = TO_BE_AUTOMATED) + @Suite(id = TOPICS_PROFILE_SUITE_ID) + @QaseId(335) + @Test + public void testCaseA() { + } + + @Automation(state = TO_BE_AUTOMATED) + @Suite(id = TOPICS_PROFILE_SUITE_ID) + @QaseId(336) + @Test + public void testCaseB() { + } + + @Automation(state = TO_BE_AUTOMATED) + @Suite(id = TOPICS_PROFILE_SUITE_ID) + @QaseId(343) + @Test + public void testCaseC() { + } + + @Automation(state = TO_BE_AUTOMATED) + @Suite(id = SCHEMAS_SUITE_ID) + @QaseId(345) + @Test + public void testCaseD() { + } + + @Automation(state = TO_BE_AUTOMATED) + @Suite(id = SCHEMAS_SUITE_ID) + @QaseId(346) + @Test + public void testCaseE() { + } + + @Automation(state = TO_BE_AUTOMATED) + @Suite(id = TOPICS_PROFILE_SUITE_ID) + @QaseId(347) + @Test + public void testCaseF() { + } + + @Automation(state = NOT_AUTOMATED) + @Suite(id = TOPICS_SUITE_ID) + @QaseId(50) + @Test + public void testCaseG() { + } + + @Automation(state = NOT_AUTOMATED) + @Suite(id = SCHEMAS_SUITE_ID) + @QaseId(351) + @Test + public void testCaseH() { + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/DataMaskingTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/DataMaskingTest.java new file mode 100644 index 00000000000..540511d0c4b --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/DataMaskingTest.java @@ -0,0 +1,29 @@ +package com.provectus.kafka.ui.manualsuite.suite; + +import static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED; + +import com.provectus.kafka.ui.manualsuite.BaseManualTest; +import com.provectus.kafka.ui.utilities.qase.annotations.Automation; +import io.qase.api.annotation.QaseId; +import org.testng.annotations.Test; + +public class DataMaskingTest extends BaseManualTest { + + @Automation(state = NOT_AUTOMATED) + @QaseId(262) + @Test + public void testCaseA() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(264) + @Test + public void testCaseB() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(265) + @Test + public void testCaseC() { + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/RbacTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/RbacTest.java new file mode 100644 index 00000000000..7c7ac3153bf --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/RbacTest.java @@ -0,0 +1,53 @@ +package com.provectus.kafka.ui.manualsuite.suite; + +import static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED; + +import com.provectus.kafka.ui.manualsuite.BaseManualTest; +import com.provectus.kafka.ui.utilities.qase.annotations.Automation; +import io.qase.api.annotation.QaseId; +import org.testng.annotations.Test; + +public class RbacTest extends BaseManualTest { + + @Automation(state = NOT_AUTOMATED) + @QaseId(249) + @Test + public void testCaseA() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(251) + @Test + public void testCaseB() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(257) + @Test + public void testCaseC() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(258) + @Test + public void testCaseD() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(259) + @Test + public void testCaseE() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(260) + @Test + public void testCaseF() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(261) + @Test + public void testCaseG() { + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/TopicsTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/TopicsTest.java new file mode 100644 index 00000000000..a4043871a3a --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/TopicsTest.java @@ -0,0 +1,113 @@ +package com.provectus.kafka.ui.manualsuite.suite; + +import static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED; + +import com.provectus.kafka.ui.manualsuite.BaseManualTest; +import com.provectus.kafka.ui.utilities.qase.annotations.Automation; +import io.qase.api.annotation.QaseId; +import org.testng.annotations.Test; + +public class TopicsTest extends BaseManualTest { + + @Automation(state = NOT_AUTOMATED) + @QaseId(17) + @Test + public void testCaseA() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(18) + @Test + public void testCaseB() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(21) + @Test() + public void testCaseC() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(22) + @Test + public void testCaseD() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(47) + @Test + public void testCaseE() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(48) + @Test + public void testCaseF() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(49) + @Test + public void testCaseG() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(57) + @Test + public void testCaseH() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(58) + @Test + public void testCaseI() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(269) + @Test + public void testCaseJ() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(270) + @Test + public void testCaseK() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(271) + @Test + public void testCaseL() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(272) + @Test + public void testCaseM() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(337) + @Test + public void testCaseN() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(339) + @Test + public void testCaseO() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(341) + @Test + public void testCaseP() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(342) + @Test + public void testCaseQ() { + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/WizardTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/WizardTest.java new file mode 100644 index 00000000000..c74c1ba6f07 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/WizardTest.java @@ -0,0 +1,29 @@ +package com.provectus.kafka.ui.manualsuite.suite; + +import static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED; + +import com.provectus.kafka.ui.manualsuite.BaseManualTest; +import com.provectus.kafka.ui.utilities.qase.annotations.Automation; +import io.qase.api.annotation.QaseId; +import org.testng.annotations.Test; + +public class WizardTest extends BaseManualTest { + + @Automation(state = NOT_AUTOMATED) + @QaseId(333) + @Test + public void testCaseA() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(338) + @Test + public void testCaseB() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(340) + @Test + public void testCaseC() { + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/ConnectorCreateView.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/ConnectorCreateView.java deleted file mode 100644 index fc4f65363f1..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/ConnectorCreateView.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.provectus.kafka.ui.pages; - -import com.codeborne.selenide.Condition; -import com.provectus.kafka.ui.extensions.WaitUtils; -import io.qameta.allure.Step; -import lombok.experimental.ExtensionMethod; -import org.openqa.selenium.By; -import org.openqa.selenium.Keys; - -import static com.codeborne.selenide.Selenide.$; -import static com.provectus.kafka.ui.screenshots.Screenshooter.log; -import static java.lang.Thread.sleep; - -@ExtensionMethod(WaitUtils.class) -public class ConnectorCreateView { - private static final String path = "ui/clusters/secondLocal/connectors/create_new"; - - @Step - public ConnectorsView setConnectorConfig(String connectName, String configJson) throws InterruptedException { - $(By.xpath("//input[@name='name']")).sendKeys(connectName); - $(".ace_text-input").sendKeys(Keys.BACK_SPACE); - $(".ace_text-input").sendKeys(Keys.BACK_SPACE); - $(".ace_text-input").sendKeys(String.valueOf(configJson.toCharArray())); - $(By.xpath("//input[@name='name']")).click(); - $(By.xpath("//input[@type='submit']")).click(); - sleep(2000); - log.info("Connector config is submitted"); - return new ConnectorsView(); - } - - @Step - public ConnectorCreateView isOnConnectorCreatePage() { - $(By.xpath("//input[@name='name']")).shouldBe(Condition.visible); - return this; - } -} \ No newline at end of file diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/ConnectorUpdateView.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/ConnectorUpdateView.java deleted file mode 100644 index ef69b0abf93..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/ConnectorUpdateView.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.provectus.kafka.ui.pages; - -import io.qameta.allure.Step; -import org.openqa.selenium.By; -import org.openqa.selenium.Keys; - -import static com.codeborne.selenide.Selenide.$; -import static org.openqa.selenium.Keys.*; - -public class ConnectorUpdateView { - @Step - public ConnectorUpdateView updateConnectorConfig(String configJson) { - String os = System.getProperty("os.name"); - Keys CMD = os.equalsIgnoreCase("Mac OS X") ? COMMAND : CONTROL; - - $(".ace_text-input").sendKeys(CMD, "a"); - $(".ace_text-input").sendKeys(Keys.BACK_SPACE); - $(".ace_text-input").sendKeys(String.valueOf(configJson.toCharArray())); - $(".ace_text-input").sendKeys(CMD, "a"); - $(".ace_text-input").sendKeys(SHIFT, TAB); - $("div.ace_content").click(); - $(By.xpath("//input[@type='submit']")).click(); - return this; - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/ConnectorsList.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/ConnectorsList.java deleted file mode 100644 index 3503079d360..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/ConnectorsList.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.provectus.kafka.ui.pages; - -import com.codeborne.selenide.Condition; -import com.codeborne.selenide.Selenide; -import com.provectus.kafka.ui.base.TestConfiguration; -import com.provectus.kafka.ui.extensions.WaitUtils; -import io.qameta.allure.Step; -import lombok.SneakyThrows; -import lombok.experimental.ExtensionMethod; -import org.openqa.selenium.By; - -import static com.codeborne.selenide.Selenide.$; - -@ExtensionMethod(WaitUtils.class) -public class ConnectorsList { - private static final String path = "ui/clusters/%s/connectors"; - - @Step - public ConnectorsList goTo(String cluster) { - Selenide.open(TestConfiguration.BASE_URL+path.formatted(cluster)); - return this; - } - - @Step - public ConnectorsList isOnPage() { - $(By.xpath("//*[contains(text(),'Loading')]")).shouldBe(Condition.disappear); - $(By.xpath("//span[text()='All Connectors']")).shouldBe(Condition.visible); - return this; - } - - @Step - public ConnectorCreateView clickCreateConnectorButton() { - $(By.xpath("//a[text()='Create Connector']")).click(); - return new ConnectorCreateView(); - } - - @SneakyThrows - public ConnectorsList openConnector(String connectorName) { - $(By.xpath("//*/tr/td[1]/a[text()='%s']".formatted(connectorName))) - .click(); - return this; - } - - @SneakyThrows - public ConnectorsList isNotVisible(String connectorName) { - By.xpath("//div[contains(@class,'section')]//table").refreshUntil(Condition.visible); - $(By.xpath("//a[text()='%s']".formatted(connectorName))).shouldNotBe(Condition.visible); - return this; - } - - @Step - public ConnectorsList connectorIsVisibleInList(String connectorName, String topicName) { - By.xpath("//a[text() = '%s']".formatted(connectorName)).refreshUntil(Condition.visible); - By.xpath("//a[text() = '%s']".formatted(topicName)).refreshUntil(Condition.visible); - return this; - } - - public ConnectorsList connectorIsUpdatedInList(String connectorName, String topicName) { - $(By.xpath("//a[text() = '%s']".formatted(connectorName))).shouldBe(Condition.visible); - By.xpath("//a[text() = '%s']".formatted(topicName)).refreshUntil(Condition.visible); - return this; - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/ConnectorsView.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/ConnectorsView.java deleted file mode 100644 index 8fc0bbbf9a9..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/ConnectorsView.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.provectus.kafka.ui.pages; - -import com.codeborne.selenide.Condition; -import com.codeborne.selenide.Selenide; -import com.provectus.kafka.ui.base.TestConfiguration; -import com.provectus.kafka.ui.extensions.WaitUtils; -import io.qameta.allure.Step; -import lombok.experimental.ExtensionMethod; -import org.openqa.selenium.By; - -import static com.codeborne.selenide.Selenide.$; - -@ExtensionMethod(WaitUtils.class) -public class ConnectorsView { - private static final String path = "ui/clusters/%s/connects/first/connectors/%s"; - - @Step - public ConnectorsView goTo(String cluster, String connector) { - Selenide.open(TestConfiguration.BASE_URL + path.formatted(cluster, connector)); - return this; - } - - @Step - public ConnectorUpdateView openEditConfig() { - $(By.xpath("//a/span[text()='Edit config']")).click(); - return new ConnectorUpdateView(); - } - - @Step - public void clickDeleteButton() { - $(By.xpath("//span[text()='Delete']")).click(); - $(By.xpath("//button[text()='Confirm']")).click(); - } - - @Step - public void connectorIsVisibleOnOverview() { - $(By.xpath("//a[text() ='Tasks']")).click(); - $(By.xpath("//a[text() ='Config']")).click(); - $(By.xpath("//span[text()='Edit config']")).waitUntil(Condition.visible, 100); - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/MainPage.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/MainPage.java deleted file mode 100644 index cb4f922688a..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/MainPage.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.provectus.kafka.ui.pages; - -import com.codeborne.selenide.Condition; -import com.codeborne.selenide.Selenide; -import com.provectus.kafka.ui.base.TestConfiguration; -import com.provectus.kafka.ui.extensions.WaitUtils; -import io.qameta.allure.Step; -import lombok.SneakyThrows; -import lombok.experimental.ExtensionMethod; -import org.openqa.selenium.By; - -import static com.codeborne.selenide.Selenide.*; - -@ExtensionMethod({WaitUtils.class}) -public class MainPage { - - private static final String path = ""; - - @Step - public MainPage goTo(){ - Selenide.open(TestConfiguration.BASE_URL+path); - return this; - } - @Step - public MainPage isOnPage() { - $(By.xpath("//*[contains(text(),'Loading')]")).shouldBe(Condition.disappear); - $(By.xpath("//h5[text()='Clusters']")).shouldBe(Condition.visible); - return this; - } - - @SneakyThrows - public void topicIsVisible(String topicName) { - By.xpath("//div[contains(@class,'section')]//table//a[text()='%s']".formatted(topicName)).refreshUntil(Condition.visible); - } - - public enum SideMenuOptions { - BROKERS("Brokers"), - TOPICS("Topics"), - CONSUMERS("Consumers"), - SCHEMA_REGISTRY("Schema registry"); - - String value; - - SideMenuOptions(String value) { - this.value = value; - } - } - - @Step - public MainPage goToSideMenu(String clusterName, SideMenuOptions option) { - $(By.xpath("//aside//*[a[text()='%s']]//a[text()='%s']".formatted(clusterName, option.value))) - .click(); - return this; - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/Pages.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/Pages.java deleted file mode 100644 index b46b2152373..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/Pages.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.provectus.kafka.ui.pages; - -public class Pages { - - public static Pages INSTANCE = new Pages(); - - public MainPage mainPage = new MainPage(); - public TopicsList topicsList = new TopicsList(); - public TopicView topicView = new TopicView(); - public ConnectorsList connectorsList = new ConnectorsList(); - public ConnectorsView connectorsView = new ConnectorsView(); - - public MainPage open() { - return openMainPage(); - } - - public MainPage openMainPage() { - return mainPage.goTo(); - } - - public TopicsList openTopicsList(String clusterName) { - return topicsList.goTo(clusterName); - } - - public TopicView openTopicView(String clusterName, String topicName) { - return topicView.goTo(clusterName, topicName); - } - - public ConnectorsList openConnectorsList(String clusterName) { - return connectorsList.goTo(clusterName); - } - - public ConnectorsView openConnectorsView(String clusterName, String connectorName) { - return connectorsView.goTo(clusterName, connectorName); - } - -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/TopicView.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/TopicView.java deleted file mode 100644 index 56196cb772b..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/TopicView.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.provectus.kafka.ui.pages; - -import com.codeborne.selenide.Condition; -import com.codeborne.selenide.Selenide; -import com.codeborne.selenide.SelenideElement; -import com.provectus.kafka.ui.base.TestConfiguration; -import com.provectus.kafka.ui.extensions.WaitUtils; -import io.qameta.allure.Step; -import lombok.SneakyThrows; -import lombok.experimental.ExtensionMethod; -import org.junit.jupiter.api.Assertions; -import org.openqa.selenium.By; - -import static com.codeborne.selenide.Selenide.*; - -@ExtensionMethod({WaitUtils.class}) -public class TopicView { - private static final String path = "ui/clusters/%s/topics/%s"; - private final SelenideElement cleanupPolicy = $(By.name("cleanupPolicy")); - private final SelenideElement timeToRetain = $(By.id("timeToRetain")); - private final SelenideElement maxSizeOnDisk = $(By.name("retentionBytes")); - private final SelenideElement maxMessageBytes = $(By.name("maxMessageBytes")); - - @Step - public TopicView goTo(String cluster,String topic){ - Selenide.open(TestConfiguration.BASE_URL+path.formatted(cluster,topic)); - return this; - } - - @Step - public TopicsList isOnTopicViewPage() { - $(By.xpath("//*[contains(text(),'Loading')]")).shouldBe(Condition.disappear); - $(By.xpath("//a[text()='All Topics']")).shouldBe(Condition.visible); - return new TopicsList(); - } - - @Step - public TopicsList isOnTopicListPage() { - $(By.xpath("//*[contains(text(),'Loading')]")).shouldBe(Condition.disappear); - $(By.xpath("//span[text()='All Topics']")).shouldBe(Condition.visible); - return new TopicsList(); - } - - @SneakyThrows - public TopicView openEditSettings() { - $(By.xpath("//a[@class=\"button\" and text()='Edit settings']")).click(); - return this; - } - - @SneakyThrows - public TopicView clickDeleteTopicButton() { - By.xpath("//*[text()='Delete Topic']").refreshUntil(Condition.visible); - $(By.xpath("//*[text()='Delete Topic']")).click(); - $(By.xpath("//*[text()='Confirm']")).click(); - return this; - } - - @SneakyThrows - public TopicView changeCleanupPolicy(String cleanupPolicyValue) { - cleanupPolicy.click(); - $(By.xpath("//select/option[@value = '%s']".formatted(cleanupPolicyValue))).click(); - return this; - } - - @SneakyThrows - public TopicView changeTimeToRetainValue(String timeToRetainValue) { - timeToRetain.clear(); - timeToRetain.sendKeys(String.valueOf(timeToRetainValue)); - return this; - } - - @SneakyThrows - public TopicView changeMaxSizeOnDisk(String maxSizeOnDiskValue) { - maxSizeOnDisk.click(); - $(By.xpath("//select/option[text() = '%s']".formatted(maxSizeOnDiskValue))).click(); - return this; - } - - @SneakyThrows - public TopicView changeMaxMessageBytes(String maxMessageBytesValue) { - maxMessageBytes.clear(); - maxMessageBytes.sendKeys(String.valueOf(maxMessageBytesValue)); - return this; - } - - @SneakyThrows - public TopicView submitSettingChanges() { - $(By.xpath("//input[@type='submit']")).click(); - return this; - } - - public TopicView cleanupPolicyIs(String value) { - cleanupPolicy.waitForSelectedValue(value); - return this; - } - - public TopicView timeToRetainIs(String time) { - Assertions.assertEquals(time, timeToRetain.getValue()); - return this; - } - - public TopicView maxSizeOnDiskIs(String size) { - Assertions.assertEquals(size, maxSizeOnDisk.getSelectedText()); - return this; - } - - public TopicView maxMessageBytesIs(String bytes) { - Assertions.assertEquals(bytes, maxMessageBytes.getValue()); - return this; - } -} \ No newline at end of file diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/TopicsList.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/TopicsList.java deleted file mode 100644 index 341bfea9fd9..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/pages/TopicsList.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.provectus.kafka.ui.pages; - -import com.codeborne.selenide.Condition; -import com.codeborne.selenide.Selenide; -import com.provectus.kafka.ui.base.TestConfiguration; -import com.provectus.kafka.ui.extensions.WaitUtils; -import io.qameta.allure.Step; -import lombok.SneakyThrows; -import lombok.experimental.ExtensionMethod; -import org.openqa.selenium.By; - -import static com.codeborne.selenide.Selenide.$; - -@ExtensionMethod(WaitUtils.class) -public class TopicsList { - private static final String path = "ui/clusters/%s/topics"; - - @Step - public TopicsList goTo(String cluster) { - Selenide.open(TestConfiguration.BASE_URL+path.formatted(cluster)); - return this; - } - - @Step - public TopicsList isOnPage() { - $(By.xpath("//*[contains(text(),'Loading')]")).shouldBe(Condition.disappear); - $(By.xpath("//span[text()='All Topics']")).shouldBe(Condition.visible); - return this; - } - - @SneakyThrows - public TopicsList openTopic(String topicName) { - By.xpath("//div[contains(@class,'section')]//table//a[text()='%s']" - .formatted(topicName)).refreshUntil(Condition.visible); - $(By.xpath("//div[contains(@class,'section')]//table//a[text()='%s']".formatted(topicName))) - .click(); - return this; - } - - @SneakyThrows - public TopicsList isNotVisible(String topicName) { - By.xpath("//div[contains(@class,'section')]//table").refreshUntil(Condition.visible); - $(By.xpath("//a[text()='%s']".formatted(topicName))).shouldNotBe(Condition.visible); - return this; - } - -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qasesuite/BaseQaseTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qasesuite/BaseQaseTest.java new file mode 100644 index 00000000000..1a195a3631d --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qasesuite/BaseQaseTest.java @@ -0,0 +1,25 @@ +package com.provectus.kafka.ui.qasesuite; + +import static com.provectus.kafka.ui.utilities.qase.QaseSetup.qaseIntegrationSetup; + +import com.provectus.kafka.ui.settings.listeners.QaseCreateListener; +import org.testng.annotations.BeforeSuite; +import org.testng.annotations.Listeners; + +@Listeners(QaseCreateListener.class) +public abstract class BaseQaseTest { + + public static final long BROKERS_SUITE_ID = 1; + public static final long CONNECTORS_SUITE_ID = 10; + public static final long KSQL_DB_SUITE_ID = 8; + public static final long SANITY_SUITE_ID = 19; + public static final long SCHEMAS_SUITE_ID = 11; + public static final long TOPICS_SUITE_ID = 2; + public static final long TOPICS_CREATE_SUITE_ID = 4; + public static final long TOPICS_PROFILE_SUITE_ID = 5; + + @BeforeSuite + public void beforeSuite() { + qaseIntegrationSetup(); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qasesuite/Template.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qasesuite/Template.java new file mode 100644 index 00000000000..d5a47eccdf7 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qasesuite/Template.java @@ -0,0 +1,58 @@ +package com.provectus.kafka.ui.qasesuite; + +import static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED; +import static com.provectus.kafka.ui.utilities.qase.enums.Status.DRAFT; + +import com.provectus.kafka.ui.utilities.qase.annotations.Automation; +import com.provectus.kafka.ui.utilities.qase.annotations.Status; +import com.provectus.kafka.ui.utilities.qase.annotations.Suite; +import io.qase.api.annotation.QaseTitle; +import io.qase.api.annotation.Step; + +public class Template extends BaseQaseTest { + + /** + * this class is a kind of placeholder or example, use is as template to create new one + * copy Template into kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/ + * place it into regarding folder and rename according to test case summary from Qase.io + * uncomment @Test and set all annotations according to kafka-ui-e2e-checks/QASE.md + */ + + @Automation(state = NOT_AUTOMATED) + @QaseTitle("testCaseA title") + @Status(status = DRAFT) + @Suite(id = 0) + // @org.testng.annotations.Test + public void testCaseA() { + stepA(); + stepB(); + stepC(); + stepD(); + stepE(); + stepF(); + } + + @Step("stepA action") + private void stepA() { + } + + @Step("stepB action") + private void stepB() { + } + + @Step("stepC action") + private void stepC() { + } + + @Step("stepD action") + private void stepD() { + } + + @Step("stepE action") + private void stepE() { + } + + @Step("stepF action") + private void stepF() { + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/sanitysuite/TopicsTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/sanitysuite/TopicsTest.java new file mode 100644 index 00000000000..40a6d67800d --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/sanitysuite/TopicsTest.java @@ -0,0 +1,66 @@ +package com.provectus.kafka.ui.sanitysuite; + +import static com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue.COMPACT; +import static com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue.DELETE; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; + +import com.provectus.kafka.ui.BaseTest; +import com.provectus.kafka.ui.models.Topic; +import io.qase.api.annotation.QaseId; +import java.util.ArrayList; +import java.util.List; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.Test; + +public class TopicsTest extends BaseTest { + + private static final List TOPIC_LIST = new ArrayList<>(); + + @QaseId(285) + @Test() + public void verifyClearMessagesMenuStateAfterTopicUpdate() { + Topic topic = new Topic() + .setName("topic-" + randomAlphabetic(5)) + .setNumberOfPartitions(1) + .setCleanupPolicyValue(DELETE); + navigateToTopics(); + topicsList + .clickAddTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady() + .setTopicName(topic.getName()) + .setNumberOfPartitions(topic.getNumberOfPartitions()) + .selectCleanupPolicy(topic.getCleanupPolicyValue()) + .clickSaveTopicBtn(); + topicDetails + .waitUntilScreenReady(); + TOPIC_LIST.add(topic); + topicDetails + .openDotMenu(); + Assert.assertTrue(topicDetails.isClearMessagesMenuEnabled(), "isClearMessagesMenuEnabled"); + topic.setCleanupPolicyValue(COMPACT); + editCleanUpPolicyAndOpenDotMenu(topic); + Assert.assertFalse(topicDetails.isClearMessagesMenuEnabled(), "isClearMessagesMenuEnabled"); + topic.setCleanupPolicyValue(DELETE); + editCleanUpPolicyAndOpenDotMenu(topic); + Assert.assertTrue(topicDetails.isClearMessagesMenuEnabled(), "isClearMessagesMenuEnabled"); + } + + private void editCleanUpPolicyAndOpenDotMenu(Topic topic) { + topicDetails + .clickEditSettingsMenu(); + topicCreateEditForm + .waitUntilScreenReady() + .selectCleanupPolicy(topic.getCleanupPolicyValue()) + .clickSaveTopicBtn(); + topicDetails + .waitUntilScreenReady() + .openDotMenu(); + } + + @AfterClass(alwaysRun = true) + public void afterClass() { + TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName())); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/screenshots/NoReferenceScreenshotFoundException.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/screenshots/NoReferenceScreenshotFoundException.java deleted file mode 100644 index 1c2df03bc30..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/screenshots/NoReferenceScreenshotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.provectus.kafka.ui.screenshots; - -public class NoReferenceScreenshotFoundException extends Throwable { - public NoReferenceScreenshotFoundException(String name) { - super(("no reference screenshot found for %s".formatted(name))); - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/screenshots/Screenshooter.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/screenshots/Screenshooter.java deleted file mode 100644 index ff07c84befe..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/screenshots/Screenshooter.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.provectus.kafka.ui.screenshots; - -import io.qameta.allure.Allure; -import io.qameta.allure.Attachment; -import lombok.SneakyThrows; -import org.junit.jupiter.api.Assertions; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import ru.yandex.qatools.ashot.AShot; -import ru.yandex.qatools.ashot.Screenshot; -import ru.yandex.qatools.ashot.comparison.ImageDiff; -import ru.yandex.qatools.ashot.comparison.ImageDiffer; -import ru.yandex.qatools.ashot.coordinates.WebDriverCoordsProvider; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.nio.file.FileSystems; -import java.util.List; - -import static com.codeborne.selenide.WebDriverRunner.getWebDriver; -import static org.junit.jupiter.api.Assertions.fail; - -public class Screenshooter { - - public static Logger log = LoggerFactory.getLogger(Screenshooter.class); - - private static int PIXELS_THRESHOLD = - Integer.parseInt(System.getProperty("PIXELS_THRESHOLD", "200")); - private static String SCREENSHOTS_FOLDER = - System.getProperty("SCREENSHOTS_FOLDER", "screenshots/"); - private static String DIFF_SCREENSHOTS_FOLDER = - System.getProperty("DIFF_SCREENSHOTS_FOLDER", "build/__diff__/"); - private static String ACTUAL_SCREENSHOTS_FOLDER = - System.getProperty("ACTUAL_SCREENSHOTS_FOLDER", "build/__actual__/"); - private static boolean SHOULD_SAVE_SCREENSHOTS_IF_NOT_EXIST = - Boolean.parseBoolean(System.getProperty("SHOULD_SAVE_SCREENSHOTS_IF_NOT_EXIST", "true")); - private static boolean TURN_OFF_SCREENSHOTS = - Boolean.parseBoolean(System.getProperty("TURN_OFF_SCREENSHOTS", "false")); - private static boolean USE_LOCAL_BROWSER = - Boolean.parseBoolean(System.getProperty("USE_LOCAL_BROWSER", "false")); - - private File newFile(String name) { - var file = new File(name); - if (!file.exists()) { - file.mkdirs(); - } - return file; - } - - public Screenshooter() { - List.of(SCREENSHOTS_FOLDER, DIFF_SCREENSHOTS_FOLDER, ACTUAL_SCREENSHOTS_FOLDER) - .forEach(this::newFile); - } - - public void compareScreenshots(String name) { - compareScreenshots(name, false); - } - - @SneakyThrows - public void compareScreenshots(String name, boolean shouldUpdateScreenshotIfDiffer) { - if (TURN_OFF_SCREENSHOTS || USE_LOCAL_BROWSER) { - log.warn("compareScreenshots turned off due TURN_OFF_SCREENSHOTS || USE_LOCAL_BROWSER: %b || %b" - .formatted(TURN_OFF_SCREENSHOTS,USE_LOCAL_BROWSER)); - return; - } - if (!doesScreenshotExist(name)) { - if (SHOULD_SAVE_SCREENSHOTS_IF_NOT_EXIST) { - updateActualScreenshot(name); - } else { - throw new NoReferenceScreenshotFoundException(name); - } - } else { - makeImageDiff(name, shouldUpdateScreenshotIfDiffer); - } - } - - @SneakyThrows - private void updateActualScreenshot(String name) { - Screenshot actual = - new AShot().coordsProvider(new WebDriverCoordsProvider()).takeScreenshot(getWebDriver()); - File file= newFile(SCREENSHOTS_FOLDER + name + ".png"); - ImageIO.write(actual.getImage(), "png", file); - log.debug("created screenshot: %s \n at $s".formatted(name,file.getAbsolutePath())); - } - - private static boolean doesScreenshotExist(String name) { - return new File(SCREENSHOTS_FOLDER + name + ".png").exists(); - } - - @SneakyThrows - private void makeImageDiff(String expectedName, boolean shouldUpdateScreenshotIfDiffer) { - String fullPathNameExpected = SCREENSHOTS_FOLDER + expectedName + ".png"; - String fullPathNameActual = ACTUAL_SCREENSHOTS_FOLDER + expectedName + ".png"; - String fullPathNameDiff = DIFF_SCREENSHOTS_FOLDER + expectedName + ".png"; - - // activating allure plugin for showing diffs in report - Allure.label("testType", "screenshotDiff"); - - Screenshot actual = - new AShot().coordsProvider(new WebDriverCoordsProvider()).takeScreenshot(getWebDriver()); - ImageIO.write(actual.getImage(), "png", newFile(fullPathNameActual)); - - Screenshot expected = new Screenshot(ImageIO.read(newFile(fullPathNameExpected))); - ImageDiff diff = new ImageDiffer().makeDiff(actual, expected); - BufferedImage diffImage = diff.getMarkedImage(); - ImageIO.write(diffImage, "png", newFile(fullPathNameDiff)); - // adding to report - diff(fullPathNameDiff); - // adding to report - actual(fullPathNameActual); - // adding to report - expected(fullPathNameExpected); - - if (shouldUpdateScreenshotIfDiffer) { - if (diff.getDiffSize() > PIXELS_THRESHOLD) { - updateActualScreenshot(expectedName); - } - } else { - Assertions.assertTrue( - PIXELS_THRESHOLD >= diff.getDiffSize(), - ("Amount of differing pixels should be less or equals than %s, actual %s\n"+ - "diff file: %s") - .formatted(PIXELS_THRESHOLD, diff.getDiffSize(), FileSystems.getDefault().getPath(fullPathNameDiff).normalize().toAbsolutePath().toString())); - } - } - - @SneakyThrows - private byte[] imgToBytes(String filename) { - BufferedImage bImage2 = ImageIO.read(new File(filename)); - var bos2 = new ByteArrayOutputStream(); - ImageIO.write(bImage2, "png", bos2); - return bos2.toByteArray(); - } - - @SneakyThrows - @Attachment - private byte[] actual(String actualFileName) { - return imgToBytes(actualFileName); - } - - @SneakyThrows - @Attachment - private byte[] expected(String expectedFileName) { - return imgToBytes(expectedFileName); - } - - @SneakyThrows - @Attachment - private byte[] diff(String diffFileName) { - return imgToBytes(diffFileName); - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/SmokeTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/SmokeTest.java new file mode 100644 index 00000000000..5193ecb25e4 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/SmokeTest.java @@ -0,0 +1,115 @@ +package com.provectus.kafka.ui.smokesuite; + +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.BROKERS; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KAFKA_CONNECT; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.SCHEMA_REGISTRY; +import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.TOPICS; +import static com.provectus.kafka.ui.settings.BaseSource.BASE_HOST; +import static com.provectus.kafka.ui.utilities.FileUtils.getResourceAsString; +import static com.provectus.kafka.ui.variables.Url.BROKERS_LIST_URL; +import static com.provectus.kafka.ui.variables.Url.CONSUMERS_LIST_URL; +import static com.provectus.kafka.ui.variables.Url.KAFKA_CONNECT_LIST_URL; +import static com.provectus.kafka.ui.variables.Url.KSQL_DB_LIST_URL; +import static com.provectus.kafka.ui.variables.Url.SCHEMA_REGISTRY_LIST_URL; +import static com.provectus.kafka.ui.variables.Url.TOPICS_LIST_URL; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.WebDriverRunner; +import com.provectus.kafka.ui.BaseTest; +import com.provectus.kafka.ui.models.Connector; +import com.provectus.kafka.ui.models.Schema; +import com.provectus.kafka.ui.models.Topic; +import com.provectus.kafka.ui.pages.panels.enums.MenuItem; +import io.qameta.allure.Step; +import io.qase.api.annotation.QaseId; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +public class SmokeTest extends BaseTest { + + private static final int BROKER_ID = 1; + private static final Schema TEST_SCHEMA = Schema.createSchemaAvro(); + private static final Topic TEST_TOPIC = new Topic() + .setName("new-topic-" + randomAlphabetic(5)) + .setNumberOfPartitions(1); + private static final Connector TEST_CONNECTOR = new Connector() + .setName("new-connector-" + randomAlphabetic(5)) + .setConfig(getResourceAsString("testData/connectors/config_for_create_connector_via_api.json")); + + @BeforeClass(alwaysRun = true) + public void beforeClass() { + apiService + .createTopic(TEST_TOPIC) + .createSchema(TEST_SCHEMA) + .createConnector(TEST_CONNECTOR); + } + + @QaseId(198) + @Test + public void checkBasePageElements() { + verifyElementsCondition( + Stream.concat(topPanel.getAllVisibleElements().stream(), naviSideBar.getAllMenuButtons().stream()) + .collect(Collectors.toList()), Condition.visible); + verifyElementsCondition( + Stream.concat(topPanel.getAllEnabledElements().stream(), naviSideBar.getAllMenuButtons().stream()) + .collect(Collectors.toList()), Condition.enabled); + } + + @QaseId(45) + @Test + public void checkUrlWhileNavigating() { + navigateToBrokers(); + verifyCurrentUrl(BROKERS_LIST_URL); + navigateToTopics(); + verifyCurrentUrl(TOPICS_LIST_URL); + navigateToConsumers(); + verifyCurrentUrl(CONSUMERS_LIST_URL); + navigateToSchemaRegistry(); + verifyCurrentUrl(SCHEMA_REGISTRY_LIST_URL); + navigateToConnectors(); + verifyCurrentUrl(KAFKA_CONNECT_LIST_URL); + navigateToKsqlDb(); + verifyCurrentUrl(KSQL_DB_LIST_URL); + } + + @QaseId(46) + @Test + public void checkPathWhileNavigating() { + navigateToBrokersAndOpenDetails(BROKER_ID); + verifyComponentsPath(BROKERS, String.format("Broker %d", BROKER_ID)); + navigateToTopicsAndOpenDetails(TEST_TOPIC.getName()); + verifyComponentsPath(TOPICS, TEST_TOPIC.getName()); + navigateToSchemaRegistryAndOpenDetails(TEST_SCHEMA.getName()); + verifyComponentsPath(SCHEMA_REGISTRY, TEST_SCHEMA.getName()); + navigateToConnectorsAndOpenDetails(TEST_CONNECTOR.getName()); + verifyComponentsPath(KAFKA_CONNECT, TEST_CONNECTOR.getName()); + } + + @Step + private void verifyCurrentUrl(String expectedUrl) { + String urlWithoutParameters = WebDriverRunner.getWebDriver().getCurrentUrl(); + if (urlWithoutParameters.contains("?")) { + urlWithoutParameters = urlWithoutParameters.substring(0, urlWithoutParameters.indexOf("?")); + } + Assert.assertEquals(urlWithoutParameters, String.format(expectedUrl, BASE_HOST), "getCurrentUrl()"); + } + + @Step + private void verifyComponentsPath(MenuItem menuItem, String expectedPath) { + Assert.assertEquals(naviSideBar.getPagePath(menuItem), expectedPath, + String.format("getPagePath() for %s", menuItem.getPageTitle().toUpperCase())); + } + + @AfterClass(alwaysRun = true) + public void afterClass() { + apiService + .deleteTopic(TEST_TOPIC.getName()) + .deleteSchema(TEST_SCHEMA.getName()) + .deleteConnector(TEST_CONNECTOR.getName()); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/brokers/BrokersTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/brokers/BrokersTest.java new file mode 100644 index 00000000000..ec1bbc2313e --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/brokers/BrokersTest.java @@ -0,0 +1,186 @@ +package com.provectus.kafka.ui.smokesuite.brokers; + +import static com.provectus.kafka.ui.pages.brokers.BrokersDetails.DetailsTab.CONFIGS; +import static com.provectus.kafka.ui.utilities.StringUtils.getMixedCase; +import static com.provectus.kafka.ui.variables.Expected.BROKER_SOURCE_INFO_TOOLTIP; + +import com.codeborne.selenide.Condition; +import com.provectus.kafka.ui.BaseTest; +import com.provectus.kafka.ui.pages.brokers.BrokersConfigTab; +import io.qameta.allure.Issue; +import io.qase.api.annotation.QaseId; +import java.util.List; +import org.testng.Assert; +import org.testng.annotations.Ignore; +import org.testng.annotations.Test; +import org.testng.asserts.SoftAssert; + +public class BrokersTest extends BaseTest { + + public static final int DEFAULT_BROKER_ID = 1; + + @QaseId(1) + @Test + public void checkBrokersOverview() { + navigateToBrokers(); + Assert.assertTrue(brokersList.getAllBrokers().size() > 0, "getAllBrokers()"); + verifyElementsCondition(brokersList.getAllVisibleElements(), Condition.visible); + verifyElementsCondition(brokersList.getAllEnabledElements(), Condition.enabled); + } + + @QaseId(85) + @Test + public void checkExistingBrokersInCluster() { + navigateToBrokers(); + Assert.assertTrue(brokersList.getAllBrokers().size() > 0, "getAllBrokers()"); + brokersList + .openBroker(DEFAULT_BROKER_ID); + brokersDetails + .waitUntilScreenReady(); + verifyElementsCondition(brokersDetails.getAllVisibleElements(), Condition.visible); + verifyElementsCondition(brokersDetails.getAllEnabledElements(), Condition.enabled); + brokersDetails + .openDetailsTab(CONFIGS); + brokersConfigTab + .waitUntilScreenReady(); + verifyElementsCondition(brokersConfigTab.getColumnHeaders(), Condition.visible); + verifyElementsCondition(brokersConfigTab.getEditButtons(), Condition.enabled); + Assert.assertTrue(brokersConfigTab.isSearchByKeyVisible(), "isSearchByKeyVisible()"); + } + + @Ignore + @Issue("https://github.com/provectus/kafka-ui/issues/3347") + @QaseId(330) + @Test + public void brokersConfigFirstPageSearchCheck() { + navigateToBrokersAndOpenDetails(DEFAULT_BROKER_ID); + brokersDetails + .openDetailsTab(CONFIGS); + String anyConfigKeyFirstPage = brokersConfigTab + .getAllConfigs().stream() + .findAny().orElseThrow() + .getKey(); + brokersConfigTab + .clickNextButton(); + Assert.assertFalse(brokersConfigTab.getAllConfigs().stream() + .map(BrokersConfigTab.BrokersConfigItem::getKey) + .toList().contains(anyConfigKeyFirstPage), + String.format("getAllConfigs().contains(%s)", anyConfigKeyFirstPage)); + brokersConfigTab + .searchConfig(anyConfigKeyFirstPage); + Assert.assertTrue(brokersConfigTab.getAllConfigs().stream() + .map(BrokersConfigTab.BrokersConfigItem::getKey) + .toList().contains(anyConfigKeyFirstPage), + String.format("getAllConfigs().contains(%s)", anyConfigKeyFirstPage)); + } + + @Ignore + @Issue("https://github.com/provectus/kafka-ui/issues/3347") + @QaseId(350) + @Test + public void brokersConfigSecondPageSearchCheck() { + navigateToBrokersAndOpenDetails(DEFAULT_BROKER_ID); + brokersDetails + .openDetailsTab(CONFIGS); + brokersConfigTab + .clickNextButton(); + String anyConfigKeySecondPage = brokersConfigTab + .getAllConfigs().stream() + .findAny().orElseThrow() + .getKey(); + brokersConfigTab + .clickPreviousButton(); + Assert.assertFalse(brokersConfigTab.getAllConfigs().stream() + .map(BrokersConfigTab.BrokersConfigItem::getKey) + .toList().contains(anyConfigKeySecondPage), + String.format("getAllConfigs().contains(%s)", anyConfigKeySecondPage)); + brokersConfigTab + .searchConfig(anyConfigKeySecondPage); + Assert.assertTrue(brokersConfigTab.getAllConfigs().stream() + .map(BrokersConfigTab.BrokersConfigItem::getKey) + .toList().contains(anyConfigKeySecondPage), + String.format("getAllConfigs().contains(%s)", anyConfigKeySecondPage)); + } + + @Ignore + @Issue("https://github.com/provectus/kafka-ui/issues/3347") + @QaseId(348) + @Test + public void brokersConfigCaseInsensitiveSearchCheck() { + navigateToBrokersAndOpenDetails(DEFAULT_BROKER_ID); + brokersDetails + .openDetailsTab(CONFIGS); + String anyConfigKeyFirstPage = brokersConfigTab + .getAllConfigs().stream() + .findAny().orElseThrow() + .getKey(); + brokersConfigTab + .clickNextButton(); + Assert.assertFalse(brokersConfigTab.getAllConfigs().stream() + .map(BrokersConfigTab.BrokersConfigItem::getKey) + .toList().contains(anyConfigKeyFirstPage), + String.format("getAllConfigs().contains(%s)", anyConfigKeyFirstPage)); + SoftAssert softly = new SoftAssert(); + List.of(anyConfigKeyFirstPage.toLowerCase(), anyConfigKeyFirstPage.toUpperCase(), + getMixedCase(anyConfigKeyFirstPage)) + .forEach(configCase -> { + brokersConfigTab + .searchConfig(configCase); + softly.assertTrue(brokersConfigTab.getAllConfigs().stream() + .map(BrokersConfigTab.BrokersConfigItem::getKey) + .toList().contains(anyConfigKeyFirstPage), + String.format("getAllConfigs().contains(%s)", configCase)); + }); + softly.assertAll(); + } + + @QaseId(331) + @Test + public void brokersSourceInfoCheck() { + navigateToBrokersAndOpenDetails(DEFAULT_BROKER_ID); + brokersDetails + .openDetailsTab(CONFIGS); + String sourceInfoTooltip = brokersConfigTab + .hoverOnSourceInfoIcon() + .getSourceInfoTooltipText(); + Assert.assertEquals(sourceInfoTooltip, BROKER_SOURCE_INFO_TOOLTIP, "brokerSourceInfoTooltip"); + } + + @QaseId(332) + @Test + public void brokersConfigEditCheck() { + navigateToBrokersAndOpenDetails(DEFAULT_BROKER_ID); + brokersDetails + .openDetailsTab(CONFIGS); + String configKey = "log.cleaner.min.compaction.lag.ms"; + BrokersConfigTab.BrokersConfigItem configItem = brokersConfigTab + .searchConfig(configKey) + .getConfig(configKey); + int defaultValue = Integer.parseInt(configItem.getValue()); + configItem + .clickEditBtn(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(configItem.getSaveBtn().isDisplayed(), "getSaveBtn().isDisplayed()"); + softly.assertTrue(configItem.getCancelBtn().isDisplayed(), "getCancelBtn().isDisplayed()"); + softly.assertTrue(configItem.getValueFld().isEnabled(), "getValueFld().isEnabled()"); + softly.assertAll(); + int newValue = defaultValue + 1; + configItem + .setValue(String.valueOf(newValue)) + .clickCancelBtn(); + Assert.assertEquals(Integer.parseInt(configItem.getValue()), defaultValue, "getValue()"); + configItem + .clickEditBtn() + .setValue(String.valueOf(newValue)) + .clickSaveBtn() + .clickConfirm(); + configItem = brokersConfigTab + .searchConfig(configKey) + .getConfig(configKey); + softly.assertFalse(configItem.getSaveBtn().isDisplayed(), "getSaveBtn().isDisplayed()"); + softly.assertFalse(configItem.getCancelBtn().isDisplayed(), "getCancelBtn().isDisplayed()"); + softly.assertTrue(configItem.getEditBtn().isDisplayed(), "getEditBtn().isDisplayed()"); + softly.assertEquals(Integer.parseInt(configItem.getValue()), newValue, "getValue()"); + softly.assertAll(); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/connectors/ConnectorsTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/connectors/ConnectorsTest.java new file mode 100644 index 00000000000..9ca3526c710 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/connectors/ConnectorsTest.java @@ -0,0 +1,107 @@ +package com.provectus.kafka.ui.smokesuite.connectors; + +import static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS; +import static com.provectus.kafka.ui.utilities.FileUtils.getResourceAsString; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; + +import com.provectus.kafka.ui.BaseTest; +import com.provectus.kafka.ui.models.Connector; +import com.provectus.kafka.ui.models.Topic; +import io.qase.api.annotation.QaseId; +import java.util.ArrayList; +import java.util.List; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +public class ConnectorsTest extends BaseTest { + + private static final List TOPIC_LIST = new ArrayList<>(); + private static final List CONNECTOR_LIST = new ArrayList<>(); + private static final String MESSAGE_CONTENT = "testData/topics/message_content_create_topic.json"; + private static final String MESSAGE_KEY = " "; + private static final Topic TOPIC_FOR_CREATE = new Topic() + .setName("topic-for-create-connector-" + randomAlphabetic(5)) + .setMessageValue(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); + private static final Topic TOPIC_FOR_DELETE = new Topic() + .setName("topic-for-delete-connector-" + randomAlphabetic(5)) + .setMessageValue(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); + private static final Topic TOPIC_FOR_UPDATE = new Topic() + .setName("topic-for-update-connector-" + randomAlphabetic(5)) + .setMessageValue(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY); + private static final Connector CONNECTOR_FOR_DELETE = new Connector() + .setName("connector-for-delete-" + randomAlphabetic(5)) + .setConfig(getResourceAsString("testData/connectors/delete_connector_config.json")); + private static final Connector CONNECTOR_FOR_UPDATE = new Connector() + .setName("connector-for-update-and-delete-" + randomAlphabetic(5)) + .setConfig(getResourceAsString("testData/connectors/config_for_create_connector_via_api.json")); + + @BeforeClass(alwaysRun = true) + public void beforeClass() { + TOPIC_LIST.addAll(List.of(TOPIC_FOR_CREATE, TOPIC_FOR_DELETE, TOPIC_FOR_UPDATE)); + TOPIC_LIST.forEach(topic -> apiService + .createTopic(topic) + .sendMessage(topic) + ); + CONNECTOR_LIST.addAll(List.of(CONNECTOR_FOR_DELETE, CONNECTOR_FOR_UPDATE)); + CONNECTOR_LIST.forEach(connector -> apiService.createConnector(connector)); + } + + @QaseId(42) + @Test + public void createConnector() { + Connector connectorForCreate = new Connector() + .setName("connector-for-create-" + randomAlphabetic(5)) + .setConfig(getResourceAsString("testData/connectors/config_for_create_connector.json")); + navigateToConnectors(); + kafkaConnectList + .clickCreateConnectorBtn(); + connectorCreateForm + .waitUntilScreenReady() + .setConnectorDetails(connectorForCreate.getName(), connectorForCreate.getConfig()) + .clickSubmitButton(); + connectorDetails + .waitUntilScreenReady(); + navigateToConnectorsAndOpenDetails(connectorForCreate.getName()); + Assert.assertTrue(connectorDetails.isConnectorHeaderVisible(connectorForCreate.getName()), + "isConnectorTitleVisible()"); + navigateToConnectors(); + Assert.assertTrue(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_DELETE.getName()), "isConnectorVisible()"); + CONNECTOR_LIST.add(connectorForCreate); + } + + @QaseId(196) + @Test + public void updateConnector() { + navigateToConnectorsAndOpenDetails(CONNECTOR_FOR_UPDATE.getName()); + connectorDetails + .openConfigTab() + .setConfig(CONNECTOR_FOR_UPDATE.getConfig()) + .clickSubmitButton(); + Assert.assertTrue(connectorDetails.isAlertWithMessageVisible(SUCCESS, "Config successfully updated."), + "isAlertWithMessageVisible()"); + navigateToConnectors(); + Assert.assertTrue(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_UPDATE.getName()), "isConnectorVisible()"); + } + + @QaseId(195) + @Test + public void deleteConnector() { + navigateToConnectorsAndOpenDetails(CONNECTOR_FOR_DELETE.getName()); + connectorDetails + .openDotMenu() + .clickDeleteBtn() + .clickConfirmBtn(); + navigateToConnectors(); + Assert.assertFalse(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_DELETE.getName()), "isConnectorVisible()"); + CONNECTOR_LIST.remove(CONNECTOR_FOR_DELETE); + } + + @AfterClass(alwaysRun = true) + public void afterClass() { + CONNECTOR_LIST.forEach(connector -> + apiService.deleteConnector(connector.getName())); + TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName())); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/ksqldb/KsqlDbTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/ksqldb/KsqlDbTest.java new file mode 100644 index 00000000000..e3d17177b5d --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/ksqldb/KsqlDbTest.java @@ -0,0 +1,146 @@ +package com.provectus.kafka.ui.smokesuite.ksqldb; + +import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlMenuTabs.STREAMS; +import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SELECT_ALL_FROM; +import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SHOW_STREAMS; +import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SHOW_TABLES; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; + +import com.provectus.kafka.ui.BaseTest; +import com.provectus.kafka.ui.pages.ksqldb.models.Stream; +import com.provectus.kafka.ui.pages.ksqldb.models.Table; +import io.qameta.allure.Step; +import io.qase.api.annotation.QaseId; +import java.util.ArrayList; +import java.util.List; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.testng.asserts.SoftAssert; + +public class KsqlDbTest extends BaseTest { + + private static final Stream DEFAULT_STREAM = new Stream() + .setName("DEFAULT_STREAM_" + randomAlphabetic(4).toUpperCase()) + .setTopicName("DEFAULT_TOPIC_" + randomAlphabetic(4).toUpperCase()); + private static final Table FIRST_TABLE = new Table() + .setName("FIRST_TABLE_" + randomAlphabetic(4).toUpperCase()) + .setStreamName(DEFAULT_STREAM.getName()); + private static final Table SECOND_TABLE = new Table() + .setName("SECOND_TABLE_" + randomAlphabetic(4).toUpperCase()) + .setStreamName(DEFAULT_STREAM.getName()); + private static final List TOPIC_NAMES_LIST = new ArrayList<>(); + + @BeforeClass(alwaysRun = true) + public void beforeClass() { + apiService + .createStream(DEFAULT_STREAM) + .createTables(FIRST_TABLE, SECOND_TABLE); + TOPIC_NAMES_LIST.addAll(List.of(DEFAULT_STREAM.getTopicName(), + FIRST_TABLE.getName(), SECOND_TABLE.getName())); + } + + @QaseId(284) + @Test(priority = 1) + public void streamsAndTablesVisibilityCheck() { + navigateToKsqlDb(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(ksqlDbList.getTableByName(FIRST_TABLE.getName()).isVisible(), "getTableByName()"); + softly.assertTrue(ksqlDbList.getTableByName(SECOND_TABLE.getName()).isVisible(), "getTableByName()"); + softly.assertAll(); + ksqlDbList + .openDetailsTab(STREAMS) + .waitUntilScreenReady(); + Assert.assertTrue(ksqlDbList.getStreamByName(DEFAULT_STREAM.getName()).isVisible(), "getStreamByName()"); + } + + @QaseId(276) + @Test(priority = 2) + public void clearEnteredQueryCheck() { + navigateToKsqlDbAndExecuteRequest(SHOW_TABLES.getQuery()); + Assert.assertFalse(ksqlQueryForm.getEnteredQuery().isEmpty(), "getEnteredQuery()"); + ksqlQueryForm + .clickClearBtn(); + Assert.assertTrue(ksqlQueryForm.getEnteredQuery().isEmpty(), "getEnteredQuery()"); + } + + @QaseId(344) + @Test(priority = 3) + public void clearResultsButtonCheck() { + String notValidQuery = "some not valid request"; + navigateToKsqlDb(); + ksqlDbList + .clickExecuteKsqlRequestBtn(); + ksqlQueryForm + .waitUntilScreenReady() + .setQuery(notValidQuery); + Assert.assertFalse(ksqlQueryForm.isClearResultsBtnEnabled(), "isClearResultsBtnEnabled()"); + ksqlQueryForm + .clickExecuteBtn(notValidQuery); + Assert.assertFalse(ksqlQueryForm.isClearResultsBtnEnabled(), "isClearResultsBtnEnabled()"); + } + + @QaseId(41) + @Test(priority = 4) + public void checkShowTablesRequestExecution() { + navigateToKsqlDbAndExecuteRequest(SHOW_TABLES.getQuery()); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(ksqlQueryForm.areResultsVisible(), "areResultsVisible()"); + softly.assertTrue(ksqlQueryForm.getItemByName(FIRST_TABLE.getName()).isVisible(), + String.format("getItemByName(%s)", FIRST_TABLE.getName())); + softly.assertTrue(ksqlQueryForm.getItemByName(SECOND_TABLE.getName()).isVisible(), + String.format("getItemByName(%s)", SECOND_TABLE.getName())); + softly.assertAll(); + } + + @QaseId(278) + @Test(priority = 5) + public void checkShowStreamsRequestExecution() { + navigateToKsqlDbAndExecuteRequest(SHOW_STREAMS.getQuery()); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(ksqlQueryForm.areResultsVisible(), "areResultsVisible()"); + softly.assertTrue(ksqlQueryForm.getItemByName(DEFAULT_STREAM.getName()).isVisible(), + String.format("getItemByName(%s)", FIRST_TABLE.getName())); + softly.assertAll(); + } + + @QaseId(86) + @Test(priority = 6) + public void clearResultsForExecutedRequest() { + navigateToKsqlDbAndExecuteRequest(SHOW_TABLES.getQuery()); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(ksqlQueryForm.areResultsVisible(), "areResultsVisible()"); + softly.assertAll(); + ksqlQueryForm + .clickClearResultsBtn(); + softly.assertFalse(ksqlQueryForm.areResultsVisible(), "areResultsVisible()"); + softly.assertAll(); + } + + @QaseId(277) + @Test(priority = 7) + public void stopQueryFunctionalCheck() { + navigateToKsqlDbAndExecuteRequest(String.format(SELECT_ALL_FROM.getQuery(), FIRST_TABLE.getName())); + Assert.assertTrue(ksqlQueryForm.isAbortBtnVisible(), "isAbortBtnVisible()"); + ksqlQueryForm + .clickAbortBtn(); + Assert.assertTrue(ksqlQueryForm.isCancelledAlertVisible(), "isCancelledAlertVisible()"); + } + + @AfterClass(alwaysRun = true) + public void afterClass() { + TOPIC_NAMES_LIST.forEach(topicName -> apiService.deleteTopic(topicName)); + } + + @Step + private void navigateToKsqlDbAndExecuteRequest(String query) { + navigateToKsqlDb(); + ksqlDbList + .clickExecuteKsqlRequestBtn(); + ksqlQueryForm + .waitUntilScreenReady() + .setQuery(query) + .clickExecuteBtn(query); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/schemas/SchemasTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/schemas/SchemasTest.java new file mode 100644 index 00000000000..0fc77e1f4b7 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/schemas/SchemasTest.java @@ -0,0 +1,190 @@ +package com.provectus.kafka.ui.smokesuite.schemas; + +import static com.provectus.kafka.ui.utilities.FileUtils.fileToString; + +import com.codeborne.selenide.Condition; +import com.provectus.kafka.ui.BaseTest; +import com.provectus.kafka.ui.api.model.CompatibilityLevel; +import com.provectus.kafka.ui.models.Schema; +import io.qase.api.annotation.QaseId; +import java.util.ArrayList; +import java.util.List; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.testng.asserts.SoftAssert; + +public class SchemasTest extends BaseTest { + + private static final List SCHEMA_LIST = new ArrayList<>(); + private static final Schema AVRO_API = Schema.createSchemaAvro(); + private static final Schema JSON_API = Schema.createSchemaJson(); + private static final Schema PROTOBUF_API = Schema.createSchemaProtobuf(); + + @BeforeClass(alwaysRun = true) + public void beforeClass() { + SCHEMA_LIST.addAll(List.of(AVRO_API, JSON_API, PROTOBUF_API)); + SCHEMA_LIST.forEach(schema -> apiService.createSchema(schema)); + } + + @QaseId(43) + @Test(priority = 1) + public void createSchemaAvro() { + Schema schemaAvro = Schema.createSchemaAvro(); + navigateToSchemaRegistry(); + schemaRegistryList + .clickCreateSchema(); + schemaCreateForm + .setSubjectName(schemaAvro.getName()) + .setSchemaField(fileToString(schemaAvro.getValuePath())) + .selectSchemaTypeFromDropdown(schemaAvro.getType()) + .clickSubmitButton(); + schemaDetails + .waitUntilScreenReady(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(schemaDetails.isSchemaHeaderVisible(schemaAvro.getName()), "isSchemaHeaderVisible()"); + softly.assertEquals(schemaDetails.getSchemaType(), schemaAvro.getType().getValue(), "getSchemaType()"); + softly.assertEquals(schemaDetails.getCompatibility(), CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue(), + "getCompatibility()"); + softly.assertAll(); + navigateToSchemaRegistry(); + Assert.assertTrue(schemaRegistryList.isSchemaVisible(AVRO_API.getName()), "isSchemaVisible()"); + SCHEMA_LIST.add(schemaAvro); + } + + @QaseId(186) + @Test(priority = 2) + public void updateSchemaAvro() { + AVRO_API.setValuePath( + System.getProperty("user.dir") + "/src/main/resources/testData/schemas/schema_avro_for_update.json"); + navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName()); + schemaDetails + .openEditSchema(); + schemaCreateForm + .waitUntilScreenReady(); + verifyElementsCondition(schemaCreateForm.getAllDetailsPageElements(), Condition.visible); + SoftAssert softly = new SoftAssert(); + softly.assertFalse(schemaCreateForm.isSubmitBtnEnabled(), "isSubmitBtnEnabled()"); + softly.assertFalse(schemaCreateForm.isSchemaDropDownEnabled(), "isSchemaDropDownEnabled()"); + softly.assertAll(); + schemaCreateForm + .selectCompatibilityLevelFromDropdown(CompatibilityLevel.CompatibilityEnum.NONE) + .setNewSchemaValue(fileToString(AVRO_API.getValuePath())) + .clickSubmitButton(); + schemaDetails + .waitUntilScreenReady(); + Assert.assertEquals(schemaDetails.getCompatibility(), CompatibilityLevel.CompatibilityEnum.NONE.toString(), + "getCompatibility()"); + } + + @QaseId(44) + @Test(priority = 3) + public void compareVersionsOperation() { + navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName()); + int latestVersion = schemaDetails + .waitUntilScreenReady() + .getLatestVersion(); + schemaDetails + .openCompareVersionMenu(); + int versionsNumberFromDdl = schemaCreateForm + .waitUntilScreenReady() + .openLeftVersionDdl() + .getVersionsNumberFromList(); + Assert.assertEquals(versionsNumberFromDdl, latestVersion, "Versions number is not matched"); + schemaCreateForm + .selectVersionFromDropDown(1); + Assert.assertEquals(schemaCreateForm.getMarkedLinesNumber(), 42, "getAllMarkedLines()"); + } + + @QaseId(187) + @Test(priority = 4) + public void deleteSchemaAvro() { + navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName()); + schemaDetails + .removeSchema(); + schemaRegistryList + .waitUntilScreenReady(); + Assert.assertFalse(schemaRegistryList.isSchemaVisible(AVRO_API.getName()), "isSchemaVisible()"); + SCHEMA_LIST.remove(AVRO_API); + } + + @QaseId(89) + @Test(priority = 5) + public void createSchemaJson() { + Schema schemaJson = Schema.createSchemaJson(); + navigateToSchemaRegistry(); + schemaRegistryList + .clickCreateSchema(); + schemaCreateForm + .setSubjectName(schemaJson.getName()) + .setSchemaField(fileToString(schemaJson.getValuePath())) + .selectSchemaTypeFromDropdown(schemaJson.getType()) + .clickSubmitButton(); + schemaDetails + .waitUntilScreenReady(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(schemaDetails.isSchemaHeaderVisible(schemaJson.getName()), "isSchemaHeaderVisible()"); + softly.assertEquals(schemaDetails.getSchemaType(), schemaJson.getType().getValue(), "getSchemaType()"); + softly.assertEquals(schemaDetails.getCompatibility(), CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue(), + "getCompatibility()"); + softly.assertAll(); + navigateToSchemaRegistry(); + Assert.assertTrue(schemaRegistryList.isSchemaVisible(JSON_API.getName()), "isSchemaVisible()"); + SCHEMA_LIST.add(schemaJson); + } + + @QaseId(189) + @Test(priority = 6) + public void deleteSchemaJson() { + navigateToSchemaRegistryAndOpenDetails(JSON_API.getName()); + schemaDetails + .removeSchema(); + schemaRegistryList + .waitUntilScreenReady(); + Assert.assertFalse(schemaRegistryList.isSchemaVisible(JSON_API.getName()), "isSchemaVisible()"); + SCHEMA_LIST.remove(JSON_API); + } + + @QaseId(91) + @Test(priority = 7) + public void createSchemaProtobuf() { + Schema schemaProtobuf = Schema.createSchemaProtobuf(); + navigateToSchemaRegistry(); + schemaRegistryList + .clickCreateSchema(); + schemaCreateForm + .setSubjectName(schemaProtobuf.getName()) + .setSchemaField(fileToString(schemaProtobuf.getValuePath())) + .selectSchemaTypeFromDropdown(schemaProtobuf.getType()) + .clickSubmitButton(); + schemaDetails + .waitUntilScreenReady(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(schemaDetails.isSchemaHeaderVisible(schemaProtobuf.getName()), "isSchemaHeaderVisible()"); + softly.assertEquals(schemaDetails.getSchemaType(), schemaProtobuf.getType().getValue(), "getSchemaType()"); + softly.assertEquals(schemaDetails.getCompatibility(), CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue(), + "getCompatibility()"); + softly.assertAll(); + navigateToSchemaRegistry(); + Assert.assertTrue(schemaRegistryList.isSchemaVisible(PROTOBUF_API.getName()), "isSchemaVisible()"); + SCHEMA_LIST.add(schemaProtobuf); + } + + @QaseId(223) + @Test(priority = 8) + public void deleteSchemaProtobuf() { + navigateToSchemaRegistryAndOpenDetails(PROTOBUF_API.getName()); + schemaDetails + .removeSchema(); + schemaRegistryList + .waitUntilScreenReady(); + Assert.assertFalse(schemaRegistryList.isSchemaVisible(PROTOBUF_API.getName()), "isSchemaVisible()"); + SCHEMA_LIST.remove(PROTOBUF_API); + } + + @AfterClass(alwaysRun = true) + public void afterClass() { + SCHEMA_LIST.forEach(schema -> apiService.deleteSchema(schema.getName())); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/MessagesTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/MessagesTest.java new file mode 100644 index 00000000000..508a3b95be8 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/MessagesTest.java @@ -0,0 +1,269 @@ +package com.provectus.kafka.ui.smokesuite.topics; + +import static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS; +import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.MESSAGES; +import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.OVERVIEW; +import static com.provectus.kafka.ui.utilities.TimeUtils.waitUntilNewMinuteStarted; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; + +import com.provectus.kafka.ui.BaseTest; +import com.provectus.kafka.ui.models.Topic; +import io.qameta.allure.Issue; +import io.qameta.allure.Step; +import io.qase.api.annotation.QaseId; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Ignore; +import org.testng.annotations.Test; +import org.testng.asserts.SoftAssert; + +public class MessagesTest extends BaseTest { + + private static final Topic TOPIC_FOR_MESSAGES = new Topic() + .setName("topic-with-clean-message-attribute-" + randomAlphabetic(5)) + .setMessageKey(randomAlphabetic(5)) + .setMessageValue(randomAlphabetic(10)); + private static final Topic TOPIC_TO_CLEAR_AND_PURGE_MESSAGES = new Topic() + .setName("topic-to-clear-and-purge-messages-" + randomAlphabetic(5)) + .setMessageKey(randomAlphabetic(5)) + .setMessageValue(randomAlphabetic(10)); + private static final Topic TOPIC_FOR_CHECK_FILTERS = new Topic() + .setName("topic-for-check-filters-" + randomAlphabetic(5)) + .setMessageKey(randomAlphabetic(5)) + .setMessageValue(randomAlphabetic(10)); + private static final Topic TOPIC_TO_RECREATE = new Topic() + .setName("topic-to-recreate-attribute-" + randomAlphabetic(5)) + .setMessageKey(randomAlphabetic(5)) + .setMessageValue(randomAlphabetic(10)); + private static final Topic TOPIC_FOR_CHECK_MESSAGES_COUNT = new Topic() + .setName("topic-for-check-messages-count" + randomAlphabetic(5)) + .setMessageKey(randomAlphabetic(5)) + .setMessageValue(randomAlphabetic(10)); + private static final List TOPIC_LIST = new ArrayList<>(); + + @BeforeClass(alwaysRun = true) + public void beforeClass() { + TOPIC_LIST.addAll(List.of(TOPIC_FOR_MESSAGES, TOPIC_FOR_CHECK_FILTERS, TOPIC_TO_CLEAR_AND_PURGE_MESSAGES, + TOPIC_TO_RECREATE, TOPIC_FOR_CHECK_MESSAGES_COUNT)); + TOPIC_LIST.forEach(topic -> apiService.createTopic(topic)); + IntStream.range(1, 3).forEach(i -> apiService.sendMessage(TOPIC_FOR_CHECK_FILTERS)); + waitUntilNewMinuteStarted(); + IntStream.range(1, 3).forEach(i -> apiService.sendMessage(TOPIC_FOR_CHECK_FILTERS)); + IntStream.range(1, 110).forEach(i -> apiService.sendMessage(TOPIC_FOR_CHECK_MESSAGES_COUNT)); + } + + @QaseId(222) + @Test(priority = 1) + public void produceMessageCheck() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_MESSAGES.getName()); + topicDetails + .openDetailsTab(MESSAGES); + produceMessage(TOPIC_FOR_MESSAGES); + Assert.assertEquals(topicDetails.getMessageByKey(TOPIC_FOR_MESSAGES.getMessageKey()).getValue(), + TOPIC_FOR_MESSAGES.getMessageValue(), "message.getValue()"); + } + + @QaseId(19) + @Test(priority = 2) + public void clearMessageCheck() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_MESSAGES.getName()); + topicDetails + .openDetailsTab(OVERVIEW); + int messageAmount = topicDetails.getMessageCountAmount(); + produceMessage(TOPIC_FOR_MESSAGES); + Assert.assertEquals(topicDetails.getMessageCountAmount(), messageAmount + 1, "getMessageCountAmount()"); + topicDetails + .openDotMenu() + .clickClearMessagesMenu() + .clickConfirmBtnMdl() + .waitUntilScreenReady(); + Assert.assertEquals(topicDetails.getMessageCountAmount(), 0, "getMessageCountAmount()"); + } + + @QaseId(239) + @Test(priority = 3) + public void checkClearTopicMessage() { + navigateToTopicsAndOpenDetails(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()); + topicDetails + .openDetailsTab(OVERVIEW); + produceMessage(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES); + navigateToTopics(); + Assert.assertEquals(topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(), 1, + "getNumberOfMessages()"); + topicsList + .openDotMenuByTopicName(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()) + .clickClearMessagesBtn() + .clickConfirmBtnMdl(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicsList.isAlertWithMessageVisible(SUCCESS, + String.format("%s messages have been successfully cleared!", TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName())), + "isAlertWithMessageVisible()"); + softly.assertEquals(topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(), 0, + "getNumberOfMessages()"); + softly.assertAll(); + } + + @QaseId(10) + @Test(priority = 4) + public void checkPurgeMessagePossibility() { + navigateToTopics(); + int messageAmount = topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(); + topicsList + .openTopic(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()); + topicDetails + .openDetailsTab(OVERVIEW); + produceMessage(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES); + navigateToTopics(); + Assert.assertEquals(topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(), + messageAmount + 1, "getNumberOfMessages()"); + topicsList + .getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()) + .selectItem(true) + .clickPurgeMessagesOfSelectedTopicsBtn(); + Assert.assertTrue(topicsList.isConfirmationMdlVisible(), "isConfirmationMdlVisible()"); + topicsList + .clickCancelBtnMdl() + .clickPurgeMessagesOfSelectedTopicsBtn() + .clickConfirmBtnMdl(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicsList.isAlertWithMessageVisible(SUCCESS, + String.format("%s messages have been successfully cleared!", TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName())), + "isAlertWithMessageVisible()"); + softly.assertEquals(topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(), 0, + "getNumberOfMessages()"); + softly.assertAll(); + } + + @QaseId(15) + @Test(priority = 6) + public void checkMessageFilteringByOffset() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); + int nextOffset = topicDetails + .openDetailsTab(MESSAGES) + .getAllMessages().stream() + .findFirst().orElseThrow().getOffset() + 1; + topicDetails + .selectSeekTypeDdlMessagesTab("Offset") + .setSeekTypeValueFldMessagesTab(String.valueOf(nextOffset)) + .clickSubmitFiltersBtnMessagesTab(); + SoftAssert softly = new SoftAssert(); + topicDetails.getAllMessages().forEach(message -> + softly.assertTrue(message.getOffset() >= nextOffset, + String.format("Expected offset not less: %s, but found: %s", nextOffset, message.getOffset()))); + softly.assertAll(); + } + + @Ignore + @Issue("https://github.com/provectus/kafka-ui/issues/3215") + @Issue("https://github.com/provectus/kafka-ui/issues/2345") + @QaseId(16) + @Test(priority = 7) + public void checkMessageFilteringByTimestamp() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); + LocalDateTime firstTimestamp = topicDetails + .openDetailsTab(MESSAGES) + .getMessageByOffset(0).getTimestamp(); + LocalDateTime nextTimestamp = topicDetails.getAllMessages().stream() + .filter(message -> message.getTimestamp().getMinute() != firstTimestamp.getMinute()) + .findFirst().orElseThrow().getTimestamp(); + topicDetails + .selectSeekTypeDdlMessagesTab("Timestamp") + .openCalendarSeekType() + .selectDateAndTimeByCalendar(nextTimestamp) + .clickSubmitFiltersBtnMessagesTab(); + SoftAssert softly = new SoftAssert(); + topicDetails.getAllMessages().forEach(message -> + softly.assertFalse(message.getTimestamp().isBefore(nextTimestamp), + String.format("Expected that %s is not before %s.", message.getTimestamp(), nextTimestamp))); + softly.assertAll(); + } + + @QaseId(246) + @Test(priority = 8) + public void checkClearTopicMessageFromOverviewTab() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); + topicDetails + .openDetailsTab(OVERVIEW) + .openDotMenu() + .clickClearMessagesMenu() + .clickConfirmBtnMdl(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, + String.format("%s messages have been successfully cleared!", TOPIC_FOR_CHECK_FILTERS.getName())), + "isAlertWithMessageVisible()"); + softly.assertEquals(topicDetails.getMessageCountAmount(), 0, + "getMessageCountAmount()= " + topicDetails.getMessageCountAmount()); + softly.assertAll(); + } + + @QaseId(240) + @Test(priority = 9) + public void checkRecreateTopic() { + navigateToTopicsAndOpenDetails(TOPIC_TO_RECREATE.getName()); + topicDetails + .openDetailsTab(OVERVIEW); + produceMessage(TOPIC_TO_RECREATE); + navigateToTopics(); + Assert.assertEquals(topicsList.getTopicItem(TOPIC_TO_RECREATE.getName()).getNumberOfMessages(), 1, + "getNumberOfMessages()"); + topicsList + .openDotMenuByTopicName(TOPIC_TO_RECREATE.getName()) + .clickRecreateTopicBtn() + .clickConfirmBtnMdl(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, + String.format("Topic %s successfully recreated!", TOPIC_TO_RECREATE.getName())), + "isAlertWithMessageVisible()"); + softly.assertEquals(topicsList.getTopicItem(TOPIC_TO_RECREATE.getName()).getNumberOfMessages(), 0, + "getNumberOfMessages()"); + softly.assertAll(); + } + + @Ignore + @Issue("https://github.com/provectus/kafka-ui/issues/3129") + @QaseId(267) + @Test(priority = 10) + public void checkMessagesCountPerPageWithinTopic() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_MESSAGES_COUNT.getName()); + topicDetails + .openDetailsTab(MESSAGES); + int messagesPerPage = topicDetails.getAllMessages().size(); + SoftAssert softly = new SoftAssert(); + softly.assertEquals(messagesPerPage, 100, "getAllMessages()"); + softly.assertFalse(topicDetails.isBackButtonEnabled(), "isBackButtonEnabled()"); + softly.assertTrue(topicDetails.isNextButtonEnabled(), "isNextButtonEnabled()"); + softly.assertAll(); + int lastOffsetOnPage = topicDetails.getAllMessages() + .get(messagesPerPage - 1).getOffset(); + topicDetails + .clickNextButton(); + softly.assertEquals(topicDetails.getAllMessages().stream().findFirst().orElseThrow().getOffset(), + lastOffsetOnPage + 1, "findFirst().getOffset()"); + softly.assertTrue(topicDetails.isBackButtonEnabled(), "isBackButtonEnabled()"); + softly.assertFalse(topicDetails.isNextButtonEnabled(), "isNextButtonEnabled()"); + softly.assertAll(); + } + + @Step + private void produceMessage(Topic topic) { + topicDetails + .clickProduceMessageBtn(); + produceMessagePanel + .waitUntilScreenReady() + .setKeyField(topic.getMessageKey()) + .setValueFiled(topic.getMessageValue()) + .submitProduceMessage(); + topicDetails + .waitUntilScreenReady(); + } + + @AfterClass(alwaysRun = true) + public void afterClass() { + TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName())); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/TopicsTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/TopicsTest.java new file mode 100644 index 00000000000..f166b92e113 --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/TopicsTest.java @@ -0,0 +1,527 @@ +package com.provectus.kafka.ui.smokesuite.topics; + +import static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS; +import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.CONSUMERS; +import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.MESSAGES; +import static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.SETTINGS; +import static com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue.COMPACT; +import static com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue.DELETE; +import static com.provectus.kafka.ui.pages.topics.enums.CustomParameterType.COMPRESSION_TYPE; +import static com.provectus.kafka.ui.pages.topics.enums.MaxSizeOnDisk.NOT_SET; +import static com.provectus.kafka.ui.pages.topics.enums.MaxSizeOnDisk.SIZE_1_GB; +import static com.provectus.kafka.ui.pages.topics.enums.MaxSizeOnDisk.SIZE_50_GB; +import static com.provectus.kafka.ui.pages.topics.enums.TimeToRetain.BTN_2_DAYS; +import static com.provectus.kafka.ui.pages.topics.enums.TimeToRetain.BTN_7_DAYS; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; +import static org.apache.commons.lang3.RandomUtils.nextInt; + +import com.codeborne.selenide.Condition; +import com.provectus.kafka.ui.BaseTest; +import com.provectus.kafka.ui.models.Topic; +import io.qameta.allure.Issue; +import io.qase.api.annotation.QaseId; +import java.util.ArrayList; +import java.util.List; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Ignore; +import org.testng.annotations.Test; +import org.testng.asserts.SoftAssert; + +public class TopicsTest extends BaseTest { + + private static final Topic TOPIC_TO_CREATE = new Topic() + .setName("new-topic-" + randomAlphabetic(5)) + .setNumberOfPartitions(1) + .setCustomParameterType(COMPRESSION_TYPE) + .setCustomParameterValue("producer") + .setCleanupPolicyValue(DELETE); + private static final Topic TOPIC_TO_UPDATE_AND_DELETE = new Topic() + .setName("topic-to-update-and-delete-" + randomAlphabetic(5)) + .setNumberOfPartitions(1) + .setCleanupPolicyValue(DELETE) + .setTimeToRetain(BTN_7_DAYS) + .setMaxSizeOnDisk(NOT_SET) + .setMaxMessageBytes("1048588") + .setMessageKey(randomAlphabetic(5)) + .setMessageValue(randomAlphabetic(10)); + private static final Topic TOPIC_TO_CHECK_SETTINGS = new Topic() + .setName("new-topic-" + randomAlphabetic(5)) + .setNumberOfPartitions(1) + .setMaxMessageBytes("1000012") + .setMaxSizeOnDisk(NOT_SET); + private static final Topic TOPIC_FOR_CHECK_FILTERS = new Topic() + .setName("topic-for-check-filters-" + randomAlphabetic(5)); + private static final Topic TOPIC_FOR_DELETE = new Topic() + .setName("topic-to-delete-" + randomAlphabetic(5)); + private static final List TOPIC_LIST = new ArrayList<>(); + + @BeforeClass(alwaysRun = true) + public void beforeClass() { + TOPIC_LIST.addAll(List.of(TOPIC_TO_UPDATE_AND_DELETE, TOPIC_FOR_DELETE, TOPIC_FOR_CHECK_FILTERS)); + TOPIC_LIST.forEach(topic -> apiService.createTopic(topic)); + } + + @QaseId(199) + @Test(priority = 1) + public void createTopic() { + navigateToTopics(); + topicsList + .clickAddTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady() + .setTopicName(TOPIC_TO_CREATE.getName()) + .setNumberOfPartitions(TOPIC_TO_CREATE.getNumberOfPartitions()) + .selectCleanupPolicy(TOPIC_TO_CREATE.getCleanupPolicyValue()) + .clickSaveTopicBtn(); + navigateToTopicsAndOpenDetails(TOPIC_TO_CREATE.getName()); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(topicDetails.isTopicHeaderVisible(TOPIC_TO_CREATE.getName()), "isTopicHeaderVisible()"); + softly.assertEquals(topicDetails.getCleanUpPolicy(), TOPIC_TO_CREATE.getCleanupPolicyValue().toString(), + "getCleanUpPolicy()"); + softly.assertEquals(topicDetails.getPartitions(), TOPIC_TO_CREATE.getNumberOfPartitions(), "getPartitions()"); + softly.assertAll(); + navigateToTopics(); + Assert.assertTrue(topicsList.isTopicVisible(TOPIC_TO_CREATE.getName()), "isTopicVisible()"); + TOPIC_LIST.add(TOPIC_TO_CREATE); + } + + @QaseId(7) + @Test(priority = 2) + void checkAvailableOperations() { + navigateToTopics(); + topicsList + .getTopicItem(TOPIC_TO_UPDATE_AND_DELETE.getName()) + .selectItem(true); + verifyElementsCondition(topicsList.getActionButtons(), Condition.enabled); + topicsList + .getTopicItem(TOPIC_FOR_CHECK_FILTERS.getName()) + .selectItem(true); + Assert.assertFalse(topicsList.isCopySelectedTopicBtnEnabled(), "isCopySelectedTopicBtnEnabled()"); + } + + @Ignore + @Issue("https://github.com/provectus/kafka-ui/issues/3071") + @QaseId(268) + @Test(priority = 3) + public void checkCustomParametersWithinEditExistingTopic() { + navigateToTopicsAndOpenDetails(TOPIC_TO_UPDATE_AND_DELETE.getName()); + topicDetails + .openDotMenu() + .clickEditSettingsMenu(); + SoftAssert softly = new SoftAssert(); + topicCreateEditForm + .waitUntilScreenReady() + .clickAddCustomParameterTypeButton() + .openCustomParameterTypeDdl() + .getAllDdlOptions() + .forEach(option -> + softly.assertTrue(!option.is(Condition.attribute("disabled")), + option.getText() + " is enabled:")); + softly.assertAll(); + } + + @QaseId(197) + @Test(priority = 4) + public void updateTopic() { + navigateToTopicsAndOpenDetails(TOPIC_TO_UPDATE_AND_DELETE.getName()); + topicDetails + .openDotMenu() + .clickEditSettingsMenu(); + topicCreateEditForm + .waitUntilScreenReady(); + SoftAssert softly = new SoftAssert(); + softly.assertEquals(topicCreateEditForm.getCleanupPolicy(), + TOPIC_TO_UPDATE_AND_DELETE.getCleanupPolicyValue().getVisibleText(), "getCleanupPolicy()"); + softly.assertEquals(topicCreateEditForm.getTimeToRetain(), + TOPIC_TO_UPDATE_AND_DELETE.getTimeToRetain().getValue(), "getTimeToRetain()"); + softly.assertEquals(topicCreateEditForm.getMaxSizeOnDisk(), + TOPIC_TO_UPDATE_AND_DELETE.getMaxSizeOnDisk().getVisibleText(), "getMaxSizeOnDisk()"); + softly.assertEquals(topicCreateEditForm.getMaxMessageBytes(), + TOPIC_TO_UPDATE_AND_DELETE.getMaxMessageBytes(), "getMaxMessageBytes()"); + softly.assertAll(); + TOPIC_TO_UPDATE_AND_DELETE + .setCleanupPolicyValue(COMPACT) + .setTimeToRetain(BTN_2_DAYS) + .setMaxSizeOnDisk(SIZE_50_GB).setMaxMessageBytes("1048589"); + topicCreateEditForm + .selectCleanupPolicy((TOPIC_TO_UPDATE_AND_DELETE.getCleanupPolicyValue())) + .setTimeToRetainDataByButtons(TOPIC_TO_UPDATE_AND_DELETE.getTimeToRetain()) + .setMaxSizeOnDiskInGB(TOPIC_TO_UPDATE_AND_DELETE.getMaxSizeOnDisk()) + .setMaxMessageBytes(TOPIC_TO_UPDATE_AND_DELETE.getMaxMessageBytes()) + .clickSaveTopicBtn(); + softly.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, "Topic successfully updated."), + "isAlertWithMessageVisible()"); + softly.assertTrue(topicDetails.isTopicHeaderVisible(TOPIC_TO_UPDATE_AND_DELETE.getName()), + "isTopicHeaderVisible()"); + softly.assertAll(); + topicDetails + .waitUntilScreenReady(); + navigateToTopicsAndOpenDetails(TOPIC_TO_UPDATE_AND_DELETE.getName()); + topicDetails + .openDotMenu() + .clickEditSettingsMenu(); + softly.assertFalse(topicCreateEditForm.isNameFieldEnabled(), "isNameFieldEnabled()"); + softly.assertEquals(topicCreateEditForm.getCleanupPolicy(), + TOPIC_TO_UPDATE_AND_DELETE.getCleanupPolicyValue().getVisibleText(), "getCleanupPolicy()"); + softly.assertEquals(topicCreateEditForm.getTimeToRetain(), + TOPIC_TO_UPDATE_AND_DELETE.getTimeToRetain().getValue(), "getTimeToRetain()"); + softly.assertEquals(topicCreateEditForm.getMaxSizeOnDisk(), + TOPIC_TO_UPDATE_AND_DELETE.getMaxSizeOnDisk().getVisibleText(), "getMaxSizeOnDisk()"); + softly.assertEquals(topicCreateEditForm.getMaxMessageBytes(), + TOPIC_TO_UPDATE_AND_DELETE.getMaxMessageBytes(), "getMaxMessageBytes()"); + softly.assertAll(); + } + + @QaseId(242) + @Test(priority = 5) + public void removeTopicFromTopicList() { + navigateToTopics(); + topicsList + .openDotMenuByTopicName(TOPIC_TO_UPDATE_AND_DELETE.getName()) + .clickRemoveTopicBtn() + .clickConfirmBtnMdl(); + Assert.assertTrue(topicsList.isAlertWithMessageVisible(SUCCESS, + String.format("Topic %s successfully deleted!", TOPIC_TO_UPDATE_AND_DELETE.getName())), + "isAlertWithMessageVisible()"); + TOPIC_LIST.remove(TOPIC_TO_UPDATE_AND_DELETE); + } + + @QaseId(207) + @Test(priority = 6) + public void deleteTopic() { + navigateToTopicsAndOpenDetails(TOPIC_FOR_DELETE.getName()); + topicDetails + .openDotMenu() + .clickDeleteTopicMenu() + .clickConfirmBtnMdl(); + navigateToTopics(); + Assert.assertFalse(topicsList.isTopicVisible(TOPIC_FOR_DELETE.getName()), "isTopicVisible"); + TOPIC_LIST.remove(TOPIC_FOR_DELETE); + } + + @QaseId(20) + @Test(priority = 7) + public void redirectToConsumerFromTopic() { + String topicName = "source-activities"; + String consumerGroupId = "connect-sink_postgres_activities"; + navigateToTopicsAndOpenDetails(topicName); + topicDetails + .openDetailsTab(CONSUMERS) + .openConsumerGroup(consumerGroupId); + consumersDetails + .waitUntilScreenReady(); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(consumersDetails.isRedirectedConsumerTitleVisible(consumerGroupId), + "isRedirectedConsumerTitleVisible()"); + softly.assertTrue(consumersDetails.isTopicInConsumersDetailsVisible(topicName), + "isTopicInConsumersDetailsVisible()"); + softly.assertAll(); + } + + @QaseId(4) + @Test(priority = 8) + public void checkTopicCreatePossibility() { + navigateToTopics(); + topicsList + .clickAddTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady(); + Assert.assertFalse(topicCreateEditForm.isCreateTopicButtonEnabled(), "isCreateTopicButtonEnabled()"); + topicCreateEditForm + .setTopicName("testName"); + Assert.assertFalse(topicCreateEditForm.isCreateTopicButtonEnabled(), "isCreateTopicButtonEnabled()"); + topicCreateEditForm + .setTopicName(null) + .setNumberOfPartitions(nextInt(1, 10)); + Assert.assertFalse(topicCreateEditForm.isCreateTopicButtonEnabled(), "isCreateTopicButtonEnabled()"); + topicCreateEditForm + .setTopicName("testName"); + Assert.assertTrue(topicCreateEditForm.isCreateTopicButtonEnabled(), "isCreateTopicButtonEnabled()"); + } + + @QaseId(266) + @Test(priority = 9) + public void checkTimeToRetainDataCustomValueWithEditingTopic() { + Topic topicToRetainData = new Topic() + .setName("topic-to-retain-data-" + randomAlphabetic(5)) + .setTimeToRetainData("86400000"); + navigateToTopics(); + topicsList + .clickAddTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady() + .setTopicName(topicToRetainData.getName()) + .setNumberOfPartitions(1) + .setTimeToRetainDataInMs("604800000"); + Assert.assertEquals(topicCreateEditForm.getTimeToRetain(), "604800000", "getTimeToRetain()"); + topicCreateEditForm + .setTimeToRetainDataInMs(topicToRetainData.getTimeToRetainData()) + .clickSaveTopicBtn(); + topicDetails + .waitUntilScreenReady() + .openDotMenu() + .clickEditSettingsMenu(); + Assert.assertEquals(topicCreateEditForm.getTimeToRetain(), topicToRetainData.getTimeToRetainData(), + "getTimeToRetain()"); + topicDetails + .openDetailsTab(SETTINGS); + Assert.assertEquals(topicDetails.getSettingsGridValueByKey("retention.ms"), topicToRetainData.getTimeToRetainData(), + "getSettingsGridValueByKey()"); + TOPIC_LIST.add(topicToRetainData); + } + + @QaseId(6) + @Test(priority = 10) + public void checkCustomParametersWithinCreateNewTopic() { + navigateToTopics(); + topicsList + .clickAddTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady() + .setTopicName(TOPIC_TO_CREATE.getName()) + .clickAddCustomParameterTypeButton() + .setCustomParameterType(TOPIC_TO_CREATE.getCustomParameterType()); + Assert.assertTrue(topicCreateEditForm.isDeleteCustomParameterButtonEnabled(), + "isDeleteCustomParameterButtonEnabled()"); + topicCreateEditForm + .clearCustomParameterValue(); + Assert.assertTrue(topicCreateEditForm.isValidationMessageCustomParameterValueVisible(), + "isValidationMessageCustomParameterValueVisible()"); + } + + @QaseId(2) + @Test(priority = 11) + public void checkTopicListElements() { + navigateToTopics(); + verifyElementsCondition(topicsList.getAllVisibleElements(), Condition.visible); + verifyElementsCondition(topicsList.getAllEnabledElements(), Condition.enabled); + } + + @QaseId(12) + @Test(priority = 12) + public void addNewFilterWithinTopic() { + String filterName = randomAlphabetic(5); + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); + topicDetails + .openDetailsTab(MESSAGES) + .clickMessagesAddFiltersBtn() + .waitUntilAddFiltersMdlVisible(); + verifyElementsCondition(topicDetails.getAllAddFilterModalVisibleElements(), Condition.visible); + verifyElementsCondition(topicDetails.getAllAddFilterModalEnabledElements(), Condition.enabled); + verifyElementsCondition(topicDetails.getAllAddFilterModalDisabledElements(), Condition.disabled); + Assert.assertFalse(topicDetails.isSaveThisFilterCheckBoxSelected(), "isSaveThisFilterCheckBoxSelected()"); + topicDetails + .setFilterCodeFldAddFilterMdl(filterName); + Assert.assertTrue(topicDetails.isAddFilterBtnAddFilterMdlEnabled(), "isAddFilterBtnAddFilterMdlEnabled()"); + topicDetails.clickAddFilterBtnAndCloseMdl(true); + Assert.assertTrue(topicDetails.isActiveFilterVisible(filterName), "isActiveFilterVisible()"); + } + + @QaseId(352) + @Test(priority = 13) + public void editActiveSmartFilterCheck() { + String filterName = randomAlphabetic(5); + String filterCode = randomAlphabetic(5); + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); + topicDetails + .openDetailsTab(MESSAGES) + .clickMessagesAddFiltersBtn() + .waitUntilAddFiltersMdlVisible() + .setFilterCodeFldAddFilterMdl(filterCode) + .setDisplayNameFldAddFilterMdl(filterName) + .clickAddFilterBtnAndCloseMdl(true) + .clickEditActiveFilterBtn(filterName) + .waitUntilAddFiltersMdlVisible(); + SoftAssert softly = new SoftAssert(); + softly.assertEquals(topicDetails.getFilterCodeValue(), filterCode, "getFilterCodeValue()"); + softly.assertEquals(topicDetails.getFilterNameValue(), filterName, "getFilterNameValue()"); + softly.assertAll(); + String newFilterName = randomAlphabetic(5); + String newFilterCode = randomAlphabetic(5); + topicDetails + .setFilterCodeFldAddFilterMdl(newFilterCode) + .setDisplayNameFldAddFilterMdl(newFilterName) + .clickSaveFilterBtnAndCloseMdl(true); + softly.assertTrue(topicDetails.isActiveFilterVisible(newFilterName), "isActiveFilterVisible()"); + softly.assertEquals(topicDetails.getSearchFieldValue(), newFilterCode, "getSearchFieldValue()"); + softly.assertAll(); + } + + @QaseId(13) + @Test(priority = 14) + public void checkFilterSavingWithinSavedFilters() { + String displayName = randomAlphabetic(5); + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); + topicDetails + .openDetailsTab(MESSAGES) + .clickMessagesAddFiltersBtn() + .waitUntilAddFiltersMdlVisible() + .setFilterCodeFldAddFilterMdl(randomAlphabetic(4)) + .selectSaveThisFilterCheckboxMdl(true) + .setDisplayNameFldAddFilterMdl(displayName); + Assert.assertTrue(topicDetails.isAddFilterBtnAddFilterMdlEnabled(), + "isAddFilterBtnAddFilterMdlEnabled()"); + topicDetails + .clickAddFilterBtnAndCloseMdl(false) + .openSavedFiltersListMdl(); + Assert.assertTrue(topicDetails.isFilterVisibleAtSavedFiltersMdl(displayName), + "isFilterVisibleAtSavedFiltersMdl()"); + } + + @QaseId(14) + @Test(priority = 15) + public void checkApplyingSavedFilterWithinTopicMessages() { + String displayName = randomAlphabetic(5); + navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName()); + topicDetails + .openDetailsTab(MESSAGES) + .clickMessagesAddFiltersBtn() + .waitUntilAddFiltersMdlVisible() + .setFilterCodeFldAddFilterMdl(randomAlphabetic(4)) + .selectSaveThisFilterCheckboxMdl(true) + .setDisplayNameFldAddFilterMdl(displayName) + .clickAddFilterBtnAndCloseMdl(false) + .openSavedFiltersListMdl() + .selectFilterAtSavedFiltersMdl(displayName) + .clickSelectFilterBtnAtSavedFiltersMdl(); + Assert.assertTrue(topicDetails.isActiveFilterVisible(displayName), "isActiveFilterVisible()"); + } + + @QaseId(11) + @Test(priority = 16) + public void checkShowInternalTopicsButton() { + navigateToTopics(); + topicsList + .setShowInternalRadioButton(true); + Assert.assertTrue(topicsList.getInternalTopics().size() > 0, "getInternalTopics()"); + topicsList + .goToLastPage(); + Assert.assertTrue(topicsList.getNonInternalTopics().size() > 0, "getNonInternalTopics()"); + topicsList + .setShowInternalRadioButton(false); + SoftAssert softly = new SoftAssert(); + softly.assertEquals(topicsList.getInternalTopics().size(), 0, "getInternalTopics()"); + softly.assertTrue(topicsList.getNonInternalTopics().size() > 0, "getNonInternalTopics()"); + softly.assertAll(); + } + + @QaseId(334) + @Test(priority = 17) + public void checkInternalTopicsNaming() { + navigateToTopics(); + SoftAssert softly = new SoftAssert(); + topicsList + .setShowInternalRadioButton(true) + .getInternalTopics() + .forEach(topic -> softly.assertTrue(topic.getName().startsWith("_"), + String.format("'%s' starts with '_'", topic.getName()))); + softly.assertAll(); + } + + @QaseId(56) + @Test(priority = 18) + public void checkRetentionBytesAccordingToMaxSizeOnDisk() { + navigateToTopics(); + topicsList + .clickAddTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady() + .setTopicName(TOPIC_TO_CHECK_SETTINGS.getName()) + .setNumberOfPartitions(TOPIC_TO_CHECK_SETTINGS.getNumberOfPartitions()) + .setMaxMessageBytes(TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes()) + .clickSaveTopicBtn(); + topicDetails + .waitUntilScreenReady(); + TOPIC_LIST.add(TOPIC_TO_CHECK_SETTINGS); + topicDetails + .openDetailsTab(SETTINGS); + topicSettingsTab + .waitUntilScreenReady(); + SoftAssert softly = new SoftAssert(); + softly.assertEquals(topicSettingsTab.getValueByKey("retention.bytes"), + TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk().getOptionValue(), "getValueOfKey(retention.bytes)"); + softly.assertEquals(topicSettingsTab.getValueByKey("max.message.bytes"), + TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes(), "getValueOfKey(max.message.bytes)"); + softly.assertAll(); + TOPIC_TO_CHECK_SETTINGS + .setMaxSizeOnDisk(SIZE_1_GB) + .setMaxMessageBytes("1000056"); + topicDetails + .openDotMenu() + .clickEditSettingsMenu(); + topicCreateEditForm + .waitUntilScreenReady() + .setMaxSizeOnDiskInGB(TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk()) + .setMaxMessageBytes(TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes()) + .clickSaveTopicBtn(); + topicDetails + .waitUntilScreenReady() + .openDetailsTab(SETTINGS); + topicSettingsTab + .waitUntilScreenReady(); + softly.assertEquals(topicSettingsTab.getValueByKey("retention.bytes"), + TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk().getOptionValue(), "getValueOfKey(retention.bytes)"); + softly.assertEquals(topicSettingsTab.getValueByKey("max.message.bytes"), + TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes(), "getValueOfKey(max.message.bytes)"); + softly.assertAll(); + } + + @QaseId(247) + @Test(priority = 19) + public void recreateTopicFromTopicProfile() { + Topic topicToRecreate = new Topic() + .setName("topic-to-recreate-" + randomAlphabetic(5)) + .setNumberOfPartitions(1); + navigateToTopics(); + topicsList + .clickAddTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady() + .setTopicName(topicToRecreate.getName()) + .setNumberOfPartitions(topicToRecreate.getNumberOfPartitions()) + .clickSaveTopicBtn(); + topicDetails + .waitUntilScreenReady(); + TOPIC_LIST.add(topicToRecreate); + topicDetails + .openDotMenu() + .clickRecreateTopicMenu(); + Assert.assertTrue(topicDetails.isConfirmationMdlVisible(), "isConfirmationMdlVisible()"); + topicDetails + .clickConfirmBtnMdl(); + Assert.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, + String.format("Topic %s successfully recreated!", topicToRecreate.getName())), + "isAlertWithMessageVisible()"); + } + + @QaseId(8) + @Test(priority = 20) + public void checkCopyTopicPossibility() { + Topic topicToCopy = new Topic() + .setName("topic-to-copy-" + randomAlphabetic(5)) + .setNumberOfPartitions(1); + navigateToTopics(); + topicsList + .getAnyNonInternalTopic() + .selectItem(true) + .clickCopySelectedTopicBtn(); + topicCreateEditForm + .waitUntilScreenReady(); + Assert.assertFalse(topicCreateEditForm.isCreateTopicButtonEnabled(), "isCreateTopicButtonEnabled()"); + topicCreateEditForm + .setTopicName(topicToCopy.getName()) + .setNumberOfPartitions(topicToCopy.getNumberOfPartitions()) + .clickSaveTopicBtn(); + topicDetails + .waitUntilScreenReady(); + TOPIC_LIST.add(topicToCopy); + Assert.assertTrue(topicDetails.isTopicHeaderVisible(topicToCopy.getName()), "isTopicHeaderVisible()"); + } + + @AfterClass(alwaysRun = true) + public void afterClass() { + TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName())); + } +} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/steps/Steps.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/steps/Steps.java deleted file mode 100644 index 2f3fae81a90..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/steps/Steps.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.provectus.kafka.ui.steps; - -import com.provectus.kafka.ui.steps.kafka.KafkaSteps; - -public class Steps { - - public static final Steps INSTANCE = new Steps(); - - private Steps(){} - - public KafkaSteps kafka = new KafkaSteps(); -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/steps/kafka/KafkaSteps.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/steps/kafka/KafkaSteps.java deleted file mode 100644 index ccaaa19b806..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/steps/kafka/KafkaSteps.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.provectus.kafka.ui.steps.kafka; - -import lombok.SneakyThrows; -import org.apache.kafka.clients.admin.*; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class KafkaSteps { - - int partitions = 2; - short replicationFactor = 1; - public enum Cluster{ - SECOND_LOCAL("secondLocal","localhost:9093"),LOCAL("local","localhost:9092"); - private String name; - private String server; - private Map config = new HashMap<>(); - Cluster(String name,String server) { - this.name = name; - this.server = server; - this.config.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, server); - this.config.put(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, "5000"); - } - - public String getName() { - return name; - } - } - - - @SneakyThrows - public void createTopic(Cluster cluster,String topicName) { - try (AdminClient client = AdminClient.create(cluster.config)) { - client - .createTopics( - Collections.singleton(new NewTopic(topicName, partitions, replicationFactor)), - new CreateTopicsOptions().timeoutMs(1000)) - .all() - .get(); - - assertTrue(client - .listTopics() - .names().get().contains(topicName)); - - } - } - - @SneakyThrows - public void deleteTopic(Cluster cluster,String topicName) { - try (AdminClient client = AdminClient.create(cluster.config)) { - assertTrue(client.listTopics().names().get().contains(topicName)); - client - .deleteTopics( - Collections.singleton(topicName), new DeleteTopicsOptions().timeoutMs(1000)) - .all() - .get(); - } - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/tests/ConnectorsTests.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/tests/ConnectorsTests.java deleted file mode 100644 index 468c087d495..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/tests/ConnectorsTests.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.provectus.kafka.ui.tests; - -import com.provectus.kafka.ui.base.BaseTest; -import com.provectus.kafka.ui.extensions.FileUtils; -import com.provectus.kafka.ui.helpers.ApiHelper; -import com.provectus.kafka.ui.helpers.Helpers; -import lombok.SneakyThrows; -import org.junit.Ignore; -import org.junit.jupiter.api.*; - -@Disabled // TODO #1480 -public class ConnectorsTests extends BaseTest { - - public static final String LOCAL = "local"; - public static final String SINK_CONNECTOR = "sink_postgres_activities_e2e_checks"; - public static final String TOPIC_FOR_CONNECTOR = "topic_for_connector"; - public static final String TOPIC_FOR_UPDATE_CONNECTOR = "topic_for_update_connector"; - public static final String FIRST = "first"; - public static final String CONNECTOR_FOR_DELETE = "sink_postgres_activities_e2e_checks_for_delete"; - public static final String CONNECTOR_FOR_UPDATE = "sink_postgres_activities_e2e_checks_for_update"; - - @BeforeAll - @SneakyThrows - public static void beforeAll() { - ApiHelper apiHelper = Helpers.INSTANCE.apiHelper; - - String message = FileUtils.getResourceAsString("message_content_create_topic.json"); - apiHelper.createTopic(LOCAL, TOPIC_FOR_CONNECTOR); - apiHelper.sendMessage(LOCAL, TOPIC_FOR_CONNECTOR, message, " "); - } - - @AfterAll - @SneakyThrows - public static void afterAll() { - ApiHelper apiHelper = Helpers.INSTANCE.apiHelper; - apiHelper.deleteConnector(LOCAL, FIRST, SINK_CONNECTOR); - apiHelper.deleteTopic(LOCAL, TOPIC_FOR_CONNECTOR); - } - - @SneakyThrows - @DisplayName("should create a connector") - @Test - void createConnector() { - pages.openConnectorsList(LOCAL) - .isOnPage() - .clickCreateConnectorButton() - .isOnConnectorCreatePage() - .setConnectorConfig( - SINK_CONNECTOR, - FileUtils.getResourceAsString("config_for_create_connector.json") - ); - pages.openConnectorsList(LOCAL).connectorIsVisibleInList(SINK_CONNECTOR, TOPIC_FOR_CONNECTOR); - } - - //disable test due 500 error during create connector via api - @SneakyThrows - @DisplayName("should update a connector") - @Test - @Disabled - void updateConnector() { - pages.openConnectorsList(LOCAL) - .isOnPage() - .openConnector(CONNECTOR_FOR_UPDATE); - pages.openConnectorsView(LOCAL, CONNECTOR_FOR_UPDATE) - .openEditConfig() - .updateConnectorConfig( - FileUtils.getResourceAsString("config_for_update_connector.json")); - pages.openConnectorsList(LOCAL).connectorIsVisibleInList(CONNECTOR_FOR_UPDATE, TOPIC_FOR_UPDATE_CONNECTOR); - } - - @SneakyThrows - @DisplayName("should delete connector") - @Test - @Disabled - void deleteConnector() { - pages.openConnectorsList(LOCAL) - .isOnPage() - .openConnector(CONNECTOR_FOR_DELETE); - pages.openConnectorsView(LOCAL, CONNECTOR_FOR_DELETE) - .clickDeleteButton(); - pages.openConnectorsList(LOCAL).isNotVisible(CONNECTOR_FOR_DELETE); - } -} diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/tests/TopicTests.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/tests/TopicTests.java deleted file mode 100644 index 670e0bcbe92..00000000000 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/tests/TopicTests.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.provectus.kafka.ui.tests; - -import com.provectus.kafka.ui.base.BaseTest; -import com.provectus.kafka.ui.helpers.Helpers; -import com.provectus.kafka.ui.pages.MainPage; -import lombok.SneakyThrows; -import org.junit.jupiter.api.*; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Disabled; - -@Disabled // TODO #1480 -public class TopicTests extends BaseTest { - - public static final String NEW_TOPIC = "new-topic"; - public static final String TOPIC_TO_UPDATE = "topic-to-update"; - public static final String TOPIC_TO_DELETE = "topic-to-delete"; - public static final String SECOND_LOCAL = "secondLocal"; - public static final String COMPACT_POLICY_VALUE = "compact"; - public static final String UPDATED_TIME_TO_RETAIN_VALUE = "604800001"; - public static final String UPDATED_MAX_SIZE_ON_DISK = "20 GB"; - public static final String UPDATED_MAX_MESSAGE_BYTES = "1000020"; - - @BeforeAll - @SneakyThrows - public static void beforeAll() { - Helpers.INSTANCE.apiHelper.createTopic(SECOND_LOCAL, TOPIC_TO_UPDATE); - Helpers.INSTANCE.apiHelper.createTopic(SECOND_LOCAL, TOPIC_TO_DELETE); - } - - @AfterAll - @SneakyThrows - public static void afterAll() { - Helpers.INSTANCE.apiHelper.deleteTopic(SECOND_LOCAL, TOPIC_TO_UPDATE); - Helpers.INSTANCE.apiHelper.deleteTopic(SECOND_LOCAL, TOPIC_TO_DELETE); - } - - @SneakyThrows - @DisplayName("should create a topic") - @Test - void createTopic() { - try { - helpers.apiHelper.createTopic(SECOND_LOCAL, NEW_TOPIC); - pages.open() - .isOnPage() - .goToSideMenu(SECOND_LOCAL, MainPage.SideMenuOptions.TOPICS) - .topicIsVisible(NEW_TOPIC); - } finally { - helpers.apiHelper.deleteTopic(SECOND_LOCAL, NEW_TOPIC); - } - } - - @SneakyThrows - @DisplayName("should update a topic") - @Test - void updateTopic() { - pages.openTopicsList(SECOND_LOCAL) - .isOnPage() - .openTopic(TOPIC_TO_UPDATE); - pages.openTopicView(SECOND_LOCAL, TOPIC_TO_UPDATE) - .openEditSettings() - .changeCleanupPolicy(COMPACT_POLICY_VALUE) - .changeTimeToRetainValue(UPDATED_TIME_TO_RETAIN_VALUE) - .changeMaxSizeOnDisk(UPDATED_MAX_SIZE_ON_DISK) - .changeMaxMessageBytes(UPDATED_MAX_MESSAGE_BYTES) - .submitSettingChanges() - .isOnTopicViewPage(); - pages.openTopicView(SECOND_LOCAL, TOPIC_TO_UPDATE) - .openEditSettings() - // Assertions - .cleanupPolicyIs(COMPACT_POLICY_VALUE) - .timeToRetainIs(UPDATED_TIME_TO_RETAIN_VALUE) - .maxSizeOnDiskIs(UPDATED_MAX_SIZE_ON_DISK) - .maxMessageBytesIs(UPDATED_MAX_MESSAGE_BYTES); - } - - @SneakyThrows - @DisplayName("should delete topic") - @Test - void deleteTopic() { - pages.openTopicsList(SECOND_LOCAL) - .isOnPage() - .openTopic(TOPIC_TO_DELETE); - pages.openTopicView(SECOND_LOCAL, TOPIC_TO_DELETE) - .clickDeleteTopicButton() - .isOnTopicListPage() - .isNotVisible(TOPIC_TO_DELETE); - } -} diff --git a/kafka-ui-e2e-checks/src/test/resources/manual.xml b/kafka-ui-e2e-checks/src/test/resources/manual.xml new file mode 100644 index 00000000000..f9ea5e5b0fa --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/resources/manual.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/kafka-ui-e2e-checks/src/test/resources/qase.xml b/kafka-ui-e2e-checks/src/test/resources/qase.xml new file mode 100644 index 00000000000..df31931718b --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/resources/qase.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/kafka-ui-e2e-checks/src/test/resources/regression.xml b/kafka-ui-e2e-checks/src/test/resources/regression.xml new file mode 100644 index 00000000000..c6461ea14ca --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/resources/regression.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/kafka-ui-e2e-checks/src/test/resources/sanity.xml b/kafka-ui-e2e-checks/src/test/resources/sanity.xml new file mode 100644 index 00000000000..bb67922402c --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/resources/sanity.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/kafka-ui-e2e-checks/src/test/resources/smoke.xml b/kafka-ui-e2e-checks/src/test/resources/smoke.xml new file mode 100644 index 00000000000..db93607727f --- /dev/null +++ b/kafka-ui-e2e-checks/src/test/resources/smoke.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/kafka-ui-react-app/.eslintrc.json b/kafka-ui-react-app/.eslintrc.json index 5116aad770f..4d524ef20a8 100644 --- a/kafka-ui-react-app/.eslintrc.json +++ b/kafka-ui-react-app/.eslintrc.json @@ -21,6 +21,7 @@ ] }, "plugins": [ + "react", "@typescript-eslint", "prettier", "react-hooks" @@ -31,6 +32,8 @@ "plugin:@typescript-eslint/recommended", "plugin:jest-dom/recommended", "plugin:prettier/recommended", + "eslint:recommended", + "plugin:react/recommended", "prettier" ], "rules": { @@ -83,7 +86,8 @@ "unnamedComponents": "arrow-function" } ], - "react/jsx-no-constructed-context-values": "off" + "react/jsx-no-constructed-context-values": "off", + "react/display-name": "off" }, "overrides": [ { diff --git a/kafka-ui-react-app/.gitignore b/kafka-ui-react-app/.gitignore index c9cfc78ef77..e712ee71d08 100644 --- a/kafka-ui-react-app/.gitignore +++ b/kafka-ui-react-app/.gitignore @@ -6,6 +6,8 @@ node_modules .pnp.js node +package-lock.json + # testing coverage @@ -19,6 +21,7 @@ build .env.test.local .env.production.local +pnpm-debug.log* npm-debug.log* yarn-debug.log* yarn-error.log* diff --git a/kafka-ui-react-app/.husky/.gitignore b/kafka-ui-react-app/.husky/.gitignore deleted file mode 100644 index 31354ec1389..00000000000 --- a/kafka-ui-react-app/.husky/.gitignore +++ /dev/null @@ -1 +0,0 @@ -_ diff --git a/kafka-ui-react-app/.husky/pre-commit b/kafka-ui-react-app/.husky/pre-commit deleted file mode 100755 index b10cf37b9c2..00000000000 --- a/kafka-ui-react-app/.husky/pre-commit +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - - -if git diff --cached --name-only | grep --quiet "kafka-ui-react-app" -then - cd kafka-ui-react-app && npm run pre-commit -else - echo "Skipping frontend tests" - exit 0 -fi diff --git a/kafka-ui-react-app/.jest/cssTransform.js b/kafka-ui-react-app/.jest/cssTransform.js new file mode 100644 index 00000000000..a8b6e023e8d --- /dev/null +++ b/kafka-ui-react-app/.jest/cssTransform.js @@ -0,0 +1,16 @@ +'use strict'; + +// This is a custom Jest transformer turning style imports into empty objects. +// http://facebook.github.io/jest/docs/en/webpack.html + +module.exports = { + process() { + return { + code: 'module.exports = {};', + }; + }, + getCacheKey() { + // The output is always the same. + return 'cssTransform'; + }, +}; diff --git a/kafka-ui-react-app/.jest/resolver.js b/kafka-ui-react-app/.jest/resolver.js new file mode 100644 index 00000000000..24504e48883 --- /dev/null +++ b/kafka-ui-react-app/.jest/resolver.js @@ -0,0 +1,26 @@ +module.exports = (path, options) => { + // Call the defaultResolver, so we leverage its cache, error handling, etc. + return options.defaultResolver(path, { + ...options, + // Use packageFilter to process parsed `package.json` before + // the resolution (see https://www.npmjs.com/package/resolve#resolveid-opts-cb) + packageFilter: (pkg) => { + // jest-environment-jsdom 28+ tries to use browser exports instead of default exports, + // but @hookform/resolvers only offers an ESM browser export and not a CommonJS one. Jest does not yet + // support ESM modules natively, so this causes a Jest error related to trying to parse + // "export" syntax. + // + // This workaround prevents Jest from considering @hookform/resolvers module-based exports at all; + // it falls back to CommonJS+node "main" property. + if (pkg.name === '@hookform/resolvers') { + delete pkg['exports']; + delete pkg['module']; + } + if (pkg.name === 'jsonpath-plus') { + delete pkg['exports']; + delete pkg['module']; + } + return pkg; + }, + }); +}; diff --git a/kafka-ui-react-app/.nvmrc b/kafka-ui-react-app/.nvmrc index 7fd023741b2..860cc5000ae 100644 --- a/kafka-ui-react-app/.nvmrc +++ b/kafka-ui-react-app/.nvmrc @@ -1 +1 @@ -v16.15.0 +v18.17.1 diff --git a/kafka-ui-react-app/.prettierrc b/kafka-ui-react-app/.prettierrc index c1a6f667131..e88e08a44ea 100644 --- a/kafka-ui-react-app/.prettierrc +++ b/kafka-ui-react-app/.prettierrc @@ -1,4 +1,10 @@ { + "trailingComma": "es5", + "semi": true, "singleQuote": true, - "trailingComma": "es5" + "quoteProps": "as-needed", + "jsxSingleQuote": false, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always" } diff --git a/kafka-ui-react-app/LICENSE b/kafka-ui-react-app/LICENSE deleted file mode 100644 index 261eeb9e9f8..00000000000 --- a/kafka-ui-react-app/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/kafka-ui-react-app/README.md b/kafka-ui-react-app/README.md index dfd8e75dfe3..a66c4d9459d 100644 --- a/kafka-ui-react-app/README.md +++ b/kafka-ui-react-app/README.md @@ -21,19 +21,19 @@ Go to react app folder cd ./kafka-ui-react-app ``` -Install Husky +Install [pnpm](https://pnpm.io/installation) ``` -npm install -g husky +npm install -g pnpm ``` Install dependencies ``` -npm install +pnpm install ``` Generate API clients from OpenAPI document ```sh -npm run gen:sources +pnpm gen:sources ``` ## Start application @@ -41,13 +41,12 @@ npm run gen:sources Create or update existing `.env.local` file with ``` -HTTPS=true # if needed -DEV_PROXY= https://api.server # your API server +VITE_DEV_PROXY= https://api.server # your API server ``` Run the application ```sh -npm start +pnpm dev ``` ### Docker way @@ -63,9 +62,8 @@ Make sure that none of the `.env*` files contain `DEV_PROXY` variable Run the application ```sh -npm start +pnpm dev ``` ## Links -* [Bulma](https://bulma.io/documentation/) - free, open source CSS framework based on Flexbox -* [Create React App](https://github.com/facebook/create-react-app) +* [Vite](https://github.com/vitejs/vite) diff --git a/kafka-ui-react-app/docker-compose.yaml b/kafka-ui-react-app/docker-compose.yaml deleted file mode 100644 index 8dacb5cb201..00000000000 --- a/kafka-ui-react-app/docker-compose.yaml +++ /dev/null @@ -1,57 +0,0 @@ -version: '3' - -services: - zookeeper: - image: zookeeper:3.4.13 - ports: - - 2181:2181 - restart: always - - kafka: - image: confluentinc/cp-kafka:5.3.1 - ports: - - 9093:9093 - environment: - KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_LISTENERS: INTERNAL://0.0.0.0:9092,PLAINTEXT://0.0.0.0:9093 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT - KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka:9092,PLAINTEXT://localhost:9093 - KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 - KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 - KAFKA_AUTO_CREATE_TOPICS_ENABLE: "false" - depends_on: - - zookeeper - restart: always - - schema-registry: - image: confluentinc/cp-schema-registry:5.5.0 - hostname: schema-registry - ports: - - "8081:8081" - environment: - SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka:9092 - SCHEMA_REGISTRY_HOST_NAME: schema-registry - SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8081 - depends_on: - - zookeeper - - kafka - - rest: - image: confluentinc/cp-kafka-rest:5.3.1 - hostname: rest-proxy - ports: - - "8082:8082" - environment: - KAFKA_REST_LISTENERS: http://0.0.0.0:8082/ - KAFKA_REST_SCHEMA_REGISTRY_URL: http://schema-registry:8081/ - KAFKA_REST_HOST_NAME: rest-proxy - KAFKA_REST_BOOTSTRAP_SERVERS: PLAINTEXT://kafka:9092 - KAFKA_REST_ACCESS_CONTROL_ALLOW_ORIGIN: "*" - KAFKA_REST_ACCESS_CONTROL_ALLOW_METHODS: "GET,POST,PUT,DELETE,OPTIONS,HEAD" - depends_on: - - zookeeper - - kafka - - schema-registry diff --git a/kafka-ui-react-app/index.html b/kafka-ui-react-app/index.html new file mode 100644 index 00000000000..be10fc78a3a --- /dev/null +++ b/kafka-ui-react-app/index.html @@ -0,0 +1,57 @@ + + + + + + + + + + + + + UI for Apache Kafka + + + + + + +
+ + + diff --git a/kafka-ui-react-app/jest.config.ts b/kafka-ui-react-app/jest.config.ts new file mode 100644 index 00000000000..d2fc35de857 --- /dev/null +++ b/kafka-ui-react-app/jest.config.ts @@ -0,0 +1,36 @@ +import type { Config } from '@jest/types'; + +export default { + roots: ['/src'], + collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'], + coveragePathIgnorePatterns: [ + '/node_modules/', + '/src/generated-sources/', + '/src/lib/fixtures/', + '/vite.config.ts', + '/src/index.tsx', + '/src/serviceWorker.ts', + ], + coverageReporters: ['json', 'lcov', 'text', 'clover'], + resolver: '/.jest/resolver.js', + setupFilesAfterEnv: ['/src/setupTests.ts'], + testMatch: [ + '/src/**/__{test,tests}__/**/*.{spec,test}.{js,jsx,ts,tsx}', + ], + testEnvironment: 'jsdom', + transform: { + '\\.[jt]sx?$': '@swc/jest', + '^.+\\.css$': '/.jest/cssTransform.js', + }, + transformIgnorePatterns: [ + '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$', + '^.+\\.module\\.(css|sass|scss)$', + ], + modulePaths: ['/src'], + watchPlugins: [ + 'jest-watch-typeahead/filename', + 'jest-watch-typeahead/testname', + ], + resetMocks: true, + reporters: ['default', 'github-actions'], +} as Config.InitialOptions; diff --git a/kafka-ui-react-app/package-lock.json b/kafka-ui-react-app/package-lock.json deleted file mode 100644 index 20f1c48d384..00000000000 --- a/kafka-ui-react-app/package-lock.json +++ /dev/null @@ -1,52218 +0,0 @@ -{ - "name": "kafka-ui", - "version": "0.4.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "kafka-ui", - "version": "0.4.0", - "dependencies": { - "@fortawesome/fontawesome-free": "^6.1.1", - "@hookform/error-message": "^2.0.0", - "@hookform/resolvers": "^2.7.1", - "@reduxjs/toolkit": "^1.8.1", - "@rooks/use-outside-click-ref": "^4.10.1", - "@testing-library/react": "^13.2.0", - "@types/yup": "^0.29.13", - "ace-builds": "^1.4.12", - "ajv": "^8.6.3", - "bulma": "^0.9.3", - "classnames": "^2.2.6", - "dayjs": "^1.11.2", - "eslint-import-resolver-node": "^0.3.6", - "eslint-import-resolver-typescript": "^2.7.1", - "fetch-mock": "^9.11.0", - "json-schema-faker": "^0.5.0-rcv.39", - "lodash": "^4.17.21", - "node-fetch": "^2.6.1", - "pretty-ms": "^7.0.1", - "react": "^18.1.0", - "react-ace": "^9.4.3", - "react-datepicker": "^4.2.0", - "react-dom": "^18.1.0", - "react-hook-form": "7.6.9", - "react-multi-select-component": "^4.0.6", - "react-redux": "^7.2.6", - "react-router-dom": "^6.3.0", - "redux": "^4.1.1", - "redux-thunk": "^2.3.0", - "sass": "^1.43.4", - "styled-components": "^5.3.1", - "use-debounce": "^8.0.1", - "uuid": "^8.3.1", - "yup": "^0.32.9" - }, - "devDependencies": { - "@jest/types": "^28.1.0", - "@openapitools/openapi-generator-cli": "^2.5.1", - "@testing-library/dom": "^8.11.1", - "@testing-library/jest-dom": "^5.16.4", - "@testing-library/user-event": "^13.5.0", - "@types/eventsource": "^1.1.8", - "@types/jest": "^27.5.1", - "@types/lodash": "^4.14.172", - "@types/node": "^16.4.13", - "@types/react": "^18.0.9", - "@types/react-datepicker": "^4.1.4", - "@types/react-dom": "^18.0.3", - "@types/react-redux": "^7.1.18", - "@types/react-router-dom": "^5.3.3", - "@types/redux-mock-store": "^1.0.3", - "@types/styled-components": "^5.1.13", - "@types/uuid": "^8.3.1", - "@typescript-eslint/eslint-plugin": "^5.10.0", - "@typescript-eslint/parser": "^5.27.0", - "dotenv": "^16.0.1", - "eslint": "^8.15.0", - "eslint-config-airbnb": "^19.0.4", - "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-jest-dom": "^4.0.2", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.29.4", - "eslint-plugin-react-hooks": "^4.5.0", - "fetch-mock-jest": "^1.5.1", - "http-proxy-middleware": "^2.0.6", - "husky": "^7.0.1", - "jest-sonar-reporter": "^2.0.0", - "jest-styled-components": "^7.0.8", - "lint-staged": "^12.1.2", - "prettier": "^2.3.1", - "react-scripts": "5.0.1", - "redux-mock-store": "^1.5.4", - "rimraf": "^3.0.2", - "ts-jest": "^28.0.3", - "ts-node": "^10.8.0", - "typescript": "^4.3.5" - }, - "engines": { - "node": "v16.15.0", - "npm": "8.5.5" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@apideck/better-ajv-errors": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.3.tgz", - "integrity": "sha512-9o+HO2MbJhJHjDYZaDxJmSDckvDpiuItEsrIShV0DXeCshXWRHhqYyU/PKHMkuClOmFnZhRd6wzv4vpDu/dRKg==", - "dev": true, - "dependencies": { - "json-schema": "^0.4.0", - "jsonpointer": "^5.0.0", - "leven": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "ajv": ">=8" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", - "dependencies": { - "@babel/highlight": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.5.tgz", - "integrity": "sha512-kixrYn4JwfAVPa0f2yfzc2AWti6WRRyO3XjWW5PJAvtE11qhSayrrcrEnee05KAtNaPC+EwehE8Qt1UedEVB8w==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.6.tgz", - "integrity": "sha512-gJnOEWSqTk96qG5BoIrl5bVtc23DCycmIePPYnamY9RboYdI4nFy5vAQMSl81O5K/W0sLDWfGysnOECC+KUUCA==", - "dependencies": { - "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.14.5", - "@babel/helper-compilation-targets": "^7.14.5", - "@babel/helper-module-transforms": "^7.14.5", - "@babel/helpers": "^7.14.6", - "@babel/parser": "^7.14.6", - "@babel/template": "^7.14.5", - "@babel/traverse": "^7.14.5", - "@babel/types": "^7.14.5", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.1.2", - "semver": "^6.3.0", - "source-map": "^0.5.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/core/node_modules/json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/core/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/core/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@babel/eslint-parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.17.0.tgz", - "integrity": "sha512-PUEJ7ZBXbRkbq3qqM/jZ2nIuakUBqCYc7Qf52Lj7dlZ6zERnqisdHioL0l4wwQZnmskMeasqUNzLBFKs3nylXA==", - "dev": true, - "dependencies": { - "eslint-scope": "^5.1.1", - "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || >=14.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.11.0", - "eslint": "^7.5.0 || ^8.0.0" - } - }, - "node_modules/@babel/eslint-parser/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz", - "integrity": "sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA==", - "dependencies": { - "@babel/types": "^7.14.5", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz", - "integrity": "sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA==", - "dependencies": { - "@babel/types": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.7.tgz", - "integrity": "sha512-C6FdbRaxYjwVu/geKW4ZeQ0Q31AftgRcdSnZ5/jsH6BzCJbtvXvhpfkbkThYSuutZA7nCXpPR6AD9zd1dprMkA==", - "dev": true, - "dependencies": { - "@babel/helper-explode-assignable-expression": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.5.tgz", - "integrity": "sha512-v+QtZqXEiOnpO6EYvlImB6zCD2Lel06RzOPzmkz/D/XgQiUu3C/Jb1LOqSt/AIA34TYi/Q+KlT8vTQrgdxkbLw==", - "dependencies": { - "@babel/compat-data": "^7.14.5", - "@babel/helper-validator-option": "^7.14.5", - "browserslist": "^4.16.6", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.9.tgz", - "integrity": "sha512-kUjip3gruz6AJKOq5i3nC6CoCEEF/oHH3cp6tOZhB+IyyyPyW0g1Gfsxn3mkk6S08pIA2y8GQh609v9G/5sHVQ==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-member-expression-to-functions": "^7.17.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/helper-replace-supers": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", - "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz", - "integrity": "sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-optimise-call-expression": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz", - "integrity": "sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-replace-supers": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz", - "integrity": "sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-member-expression-to-functions": "^7.16.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz", - "integrity": "sha512-awO2So99wG6KnlE+TPs6rn83gCz5WlEePJDTnLEqbchMVrBeAujURVphRdigsk094VhvZehFoNOihSlcBjwsXA==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "regexpu-core": "^5.0.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", - "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz", - "integrity": "sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA==", - "dev": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.13.0", - "@babel/helper-module-imports": "^7.12.13", - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/traverse": "^7.13.0", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0-0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/helper-define-polyfill-provider/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", - "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-environment-visitor/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-environment-visitor/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-explode-assignable-expression": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.7.tgz", - "integrity": "sha512-KyUenhWMC8VrxzkGP0Jizjo4/Zx+1nNZhgocs+gLzyZyB8SHidhoq9KK/8Ato4anhwsivfkBLftky7gvzbZMtQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-explode-assignable-expression/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-explode-assignable-expression/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz", - "integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==", - "dependencies": { - "@babel/helper-get-function-arity": "^7.14.5", - "@babel/template": "^7.14.5", - "@babel/types": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-get-function-arity": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz", - "integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==", - "dependencies": { - "@babel/types": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz", - "integrity": "sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==", - "dependencies": { - "@babel/types": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.5.tgz", - "integrity": "sha512-UxUeEYPrqH1Q/k0yRku1JE7dyfyehNwT6SVkMHvYvPDv4+uu627VXBckVj891BO8ruKBkiDoGnZf4qPDD8abDQ==", - "dependencies": { - "@babel/types": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz", - "integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==", - "dependencies": { - "@babel/types": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.5.tgz", - "integrity": "sha512-iXpX4KW8LVODuAieD7MzhNjmM6dzYY5tfRqT+R9HDXWl0jPn/djKmA+G9s/2C2T9zggw5tK1QNqZ70USfedOwA==", - "dependencies": { - "@babel/helper-module-imports": "^7.14.5", - "@babel/helper-replace-supers": "^7.14.5", - "@babel/helper-simple-access": "^7.14.5", - "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/helper-validator-identifier": "^7.14.5", - "@babel/template": "^7.14.5", - "@babel/traverse": "^7.14.5", - "@babel/types": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz", - "integrity": "sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==", - "dependencies": { - "@babel/types": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", - "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz", - "integrity": "sha512-fm0gH7Flb8H51LqJHy3HJ3wnE1+qtYR2A99K06ahwrawLdOFsCEWjZOrYricXJHoPSudNKxrMBUPEIPxiIIvBw==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-wrap-function": "^7.16.8", - "@babel/types": "^7.16.8" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", - "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.5.tgz", - "integrity": "sha512-3i1Qe9/8x/hCHINujn+iuHy+mMRLoc77b2nI9TB0zjH1hvn9qGlXjWlggdwUcju36PkPCy/lpM7LLUdcTyH4Ow==", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.14.5", - "@babel/helper-optimise-call-expression": "^7.14.5", - "@babel/traverse": "^7.14.5", - "@babel/types": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.14.5.tgz", - "integrity": "sha512-nfBN9xvmCt6nrMZjfhkl7i0oTV3yxR4/FztsbOASyTvVcoYd0TRHh7eMLdlEcCqobydC0LAF3LtC92Iwxo0wyw==", - "dependencies": { - "@babel/types": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz", - "integrity": "sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz", - "integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==", - "dependencies": { - "@babel/types": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", - "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", - "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.16.8.tgz", - "integrity": "sha512-8RpyRVIAW1RcDDGTA+GpPAwV22wXCfKOoM9bet6TLkGIFTkRQSkH1nMQ5Yet4MpoXe1ZwHPVtNasc2w0uZMqnw==", - "dev": true, - "dependencies": { - "@babel/helper-function-name": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.8", - "@babel/types": "^7.16.8" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@babel/helper-wrap-function/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helpers": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.6.tgz", - "integrity": "sha512-yesp1ENQBiLI+iYHSJdoZKUtRpfTlL1grDIX9NRlAVppljLw/4tTyYupIB7uIYmC3stW/imAv8EqaKaS/ibmeA==", - "dependencies": { - "@babel/template": "^7.14.5", - "@babel/traverse": "^7.14.5", - "@babel/types": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.14.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/parser": { - "version": "7.18.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz", - "integrity": "sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz", - "integrity": "sha512-anv/DObl7waiGEnC24O9zqL0pSuI9hljihqiDuFHC8d7/bjr/4RLGPWuc8rYOff/QPzbEPSkzG8wGG9aDuhHRg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.16.7.tgz", - "integrity": "sha512-di8vUHRdf+4aJ7ltXhaDbPoszdkh59AQtJM5soLsuHpQJdFQZOA4uGj0V2u/CZ8bJ/u8ULDL5yq6FO/bCXnKHw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0", - "@babel/plugin-proposal-optional-chaining": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.8.tgz", - "integrity": "sha512-71YHIvMuiuqWJQkebWJtdhQTfd4Q4mF76q2IX37uZPkG9+olBxsX+rH1vkhFto4UeJZ9dPY2s+mDvhDm1u2BGQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-remap-async-to-generator": "^7.16.8", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.7.tgz", - "integrity": "sha512-IobU0Xme31ewjYOShSIqd/ZGM/r/cuOz2z0MDbNrhF5FW+ZVgi0f2lyeoj9KFPDOAqsYxmLWZte1WOwlvY9aww==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-static-block": { - "version": "7.17.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.17.6.tgz", - "integrity": "sha512-X/tididvL2zbs7jZCeeRJ8167U/+Ac135AM6jCAx6gYXDUviZV5Ku9UDvWS2NCuWlFjIRXklYhwo6HhAC7ETnA==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.17.6", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.17.9.tgz", - "integrity": "sha512-EfH2LZ/vPa2wuPwJ26j+kYRkaubf89UlwxKXtxqEm57HrgSEYDB8t4swFP+p8LcI9yiP9ZRJJjo/58hS6BnaDA==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.17.9", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-replace-supers": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/plugin-syntax-decorators": "^7.17.0", - "charcodes": "^0.2.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-decorators/node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-proposal-decorators/node_modules/@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-proposal-decorators/node_modules/@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-proposal-decorators/node_modules/@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-proposal-decorators/node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz", - "integrity": "sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-proposal-decorators/node_modules/@babel/helper-optimise-call-expression": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz", - "integrity": "sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-proposal-decorators/node_modules/@babel/helper-replace-supers": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz", - "integrity": "sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-member-expression-to-functions": "^7.16.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-proposal-decorators/node_modules/@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-proposal-decorators/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-proposal-decorators/node_modules/@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-proposal-decorators/node_modules/@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-proposal-decorators/node_modules/@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-proposal-decorators/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-proposal-decorators/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-proposal-decorators/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-proposal-decorators/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/plugin-proposal-decorators/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-proposal-decorators/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-proposal-decorators/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@babel/plugin-proposal-decorators/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-proposal-dynamic-import": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz", - "integrity": "sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-export-namespace-from": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.7.tgz", - "integrity": "sha512-ZxdtqDXLRGBL64ocZcs7ovt71L3jhC1RGSyR996svrCi3PYqHNkb3SwPJCs8RIzD86s+WPpt2S73+EHCGO+NUA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-json-strings": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.16.7.tgz", - "integrity": "sha512-lNZ3EEggsGY78JavgbHsK9u5P3pQaW7k4axlgFLYkMd7UBsiNahCITShLjNQschPyjtO6dADrL24757IdhBrsQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.7.tgz", - "integrity": "sha512-K3XzyZJGQCr00+EtYtrDjmwX7o7PLK6U9bi1nCwkQioRFVUv6dJoxbQjtWVtP+bCPy82bONBKG8NPyQ4+i6yjg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.7.tgz", - "integrity": "sha512-aUOrYU3EVtjf62jQrCj63pYZ7k6vns2h/DQvHPWGmsJRYzWXZ6/AsfgpiRy6XiuIDADhJzP2Q9MwSMKauBQ+UQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-numeric-separator": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.7.tgz", - "integrity": "sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.17.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.17.3.tgz", - "integrity": "sha512-yuL5iQA/TbZn+RGAfxQXfi7CNLmKi1f8zInn4IgobuCWcAb7i+zj4TYzQ9l8cEzVyJ89PDGuqxK1xZpUDISesw==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.17.0", - "@babel/helper-compilation-targets": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread/node_modules/@babel/compat-data": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz", - "integrity": "sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread/node_modules/@babel/helper-compilation-targets": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz", - "integrity": "sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.17.10", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.20.2", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread/node_modules/@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread/node_modules/browserslist": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", - "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread/node_modules/electron-to-chromium": { - "version": "1.4.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", - "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", - "dev": true - }, - "node_modules/@babel/plugin-proposal-object-rest-spread/node_modules/node-releases": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", - "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", - "dev": true - }, - "node_modules/@babel/plugin-proposal-object-rest-spread/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/plugin-proposal-optional-catch-binding": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.16.7.tgz", - "integrity": "sha512-eMOH/L4OvWSZAE1VkHbr1vckLG1WUcHGJSLqqQwl2GaUqG6QjddvrOaTUMNYiv77H5IKPMZ9U9P7EaHwvAShfA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.7.tgz", - "integrity": "sha512-eC3xy+ZrUcBtP7x+sq62Q/HYd674pPTb/77XZMb5wbDPGWIdUbSr4Agr052+zaUPSb+gGRnjxXfKFvx5iMJ+DA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-methods": { - "version": "7.16.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.11.tgz", - "integrity": "sha512-F/2uAkPlXDr8+BHpZvo19w3hLFKge+k75XUprE6jaqKxjGkSYcK+4c+bup5PdW/7W/Rpjwql7FTVEDW+fRAQsw==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.16.10", - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.7.tgz", - "integrity": "sha512-rMQkjcOFbm+ufe3bTZLyOfsOUOxyvLXZJCTARhJr+8UMSoZmqTe1K1BgkFcrW37rAchWg57yI69ORxiWvUINuQ==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-create-class-features-plugin": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", - "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-proposal-unicode-property-regex": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.16.7.tgz", - "integrity": "sha512-QRK0YI/40VLhNVGIjRNAAQkEHws0cswSdFFjpFyt943YmJIU1da9uW63Iu6NFV6CxTZW5eTDCrwZUstBWgp/Rg==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.17.0.tgz", - "integrity": "sha512-qWe85yCXsvDEluNP0OyeQjH63DlhAR3W7K9BxxU1MvbDb48tgBG+Ao6IJJ6smPDrrVzSQZrbF6donpkFBMcs3A==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-flow": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.16.7.tgz", - "integrity": "sha512-UDo3YGQO0jH6ytzVwgSLv9i/CzMcUjbKenL67dTrAZPPv6GFAtDhe6jqnvmoKzC/7htNTohhos+onPtDMqJwaQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz", - "integrity": "sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.17.10.tgz", - "integrity": "sha512-xJefea1DWXW09pW4Tm9bjwVlPDyYA2it3fWlmEjpYz6alPvTUjL0EOzNzI/FEOyI3r4/J7uVH5UqKgl1TQ5hqQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz", - "integrity": "sha512-9ffkFFMbvzTvv+7dTp/66xvZAWASuPD5Tl9LK3Z9vhOmANo6j94rik+5YMBt4CwHVMWLWpMsriIc2zsa3WW3xQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.8.tgz", - "integrity": "sha512-MtmUmTJQHCnyJVrScNzNlofQJ3dLFuobYn3mwOTKHnSCMtbNsqvF71GQmJfFjdrXSsAA7iysFmYWw4bXZ20hOg==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-remap-async-to-generator": "^7.16.8" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator/node_modules/@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.7.tgz", - "integrity": "sha512-JUuzlzmF40Z9cXyytcbZEZKckgrQzChbQJw/5PuEHYeqzCsvebDx0K0jWnIIVcmmDOAVctCgnYs0pMcrYj2zJg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.7.tgz", - "integrity": "sha512-ObZev2nxVAYA4bhyusELdo9hb3H+A56bxH3FZMbEImZFiEDYVHXQSJ1hQKFlDnlt8G9bBrCZ5ZpURZUrV4G5qQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz", - "integrity": "sha512-WY7og38SFAGYRe64BrjKf8OrE6ulEHtr5jEYaZMwox9KebgqPi67Zqz8K53EKk1fFEJgm96r32rkKZ3qA2nCWQ==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-replace-supers": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", - "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz", - "integrity": "sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-optimise-call-expression": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz", - "integrity": "sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-replace-supers": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz", - "integrity": "sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-member-expression-to-functions": "^7.16.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@babel/plugin-transform-classes/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.7.tgz", - "integrity": "sha512-gN72G9bcmenVILj//sv1zLNaPyYcOzUho2lIJBMh/iakJ9ygCo/hEF9cpGb61SCMEDxbbyBoVQxrt+bWKu5KGw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.17.7.tgz", - "integrity": "sha512-XVh0r5yq9sLR4vZ6eVZe8FKfIcSgaTBxVBRSYokRj2qksf6QerYnTxz9/GTuKTH/n/HwLP7t6gtlybHetJ/6hQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.16.7.tgz", - "integrity": "sha512-Lyttaao2SjZF6Pf4vk1dVKv8YypMpomAbygW+mU5cYP3S5cWTfCJjG8xV6CFdzGFlfWK81IjL9viiTvpb6G7gQ==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.16.7.tgz", - "integrity": "sha512-03DvpbRfvWIXyK0/6QiR1KMTWeT6OcQ7tbhjrXyFS02kjuX/mu5Bvnh5SDSWHxyawit2g5aWhKwI86EE7GUnTw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.7.tgz", - "integrity": "sha512-8UYLSlyLgRixQvlYH3J2ekXFHDFLQutdy7FfFAMm3CPZ6q9wHCwnUyiXpQCe3gVVnQlHc5nsuiEVziteRNTXEA==", - "dev": true, - "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.16.7.tgz", - "integrity": "sha512-mzmCq3cNsDpZZu9FADYYyfZJIOrSONmHcop2XEKPdBNMa4PDC4eEvcOvzZaCNcjKu72v0XQlA5y1g58aLRXdYg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-flow": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.7.tgz", - "integrity": "sha512-/QZm9W92Ptpw7sjI9Nx1mbcsWz33+l8kuMIQnDwgQBG5s3fAfQvkRjQ7NqXhtNcKOnPkdICmUHyCaWW06HCsqg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.7.tgz", - "integrity": "sha512-SU/C68YVwTRxqWj5kgsbKINakGag0KTgq9f2iZEXdStoAbOzLHEBRYzImmA6yFo8YZhJVflvXmIHUO7GWHmxxA==", - "dev": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/compat-data": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz", - "integrity": "sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/helper-compilation-targets": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz", - "integrity": "sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.17.10", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.20.2", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/browserslist": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", - "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/electron-to-chromium": { - "version": "1.4.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", - "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", - "dev": true - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/node-releases": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", - "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", - "dev": true - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.16.7.tgz", - "integrity": "sha512-6tH8RTpTWI0s2sV6uq3e/C9wPo4PTqqZps4uF0kzQ9/xPLFQtipynvmT1g/dOfEJ+0EQsHhkQ/zyRId8J2b8zQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.7.tgz", - "integrity": "sha512-mBruRMbktKQwbxaJof32LT9KLy2f3gH+27a5XSuXo6h7R3vqltl0PgZ80C8ZMKw98Bf8bqt6BEVi3svOh2PzMw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.16.7.tgz", - "integrity": "sha512-KaaEtgBL7FKYwjJ/teH63oAmE3lP34N3kshz8mm4VMAw7U3PxjVwwUmxEFksbgsNUaO3wId9R2AVQYSEGRa2+g==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/helper-module-transforms": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", - "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/helper-simple-access": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", - "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-modules-amd/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-modules-amd/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/plugin-transform-modules-amd/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-modules-amd/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-modules-amd/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@babel/plugin-transform-modules-amd/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.17.9.tgz", - "integrity": "sha512-2TBFd/r2I6VlYn0YRTz2JdazS+FoUuQ2rIFHoAxtyP/0G3D82SBLaRq9rnUkpqlLg03Byfl/+M32mpxjO6KaPw==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/helper-module-transforms": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", - "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/helper-simple-access": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", - "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.17.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.17.8.tgz", - "integrity": "sha512-39reIkMTUVagzgA5x88zDYXPCMT6lcaRKs1+S9K6NKBPErbgO/w/kP8GlNQTC87b412ZTlmNgr3k2JrWgHH+Bw==", - "dev": true, - "dependencies": { - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-module-transforms": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/helper-module-transforms": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", - "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/helper-simple-access": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", - "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.16.7.tgz", - "integrity": "sha512-EMh7uolsC8O4xhudF2F6wedbSHm1HHZ0C6aJ7K67zcDNidMzVcxWdGr+htW9n21klm+bOn+Rx4CBsAntZd3rEQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/helper-module-transforms": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", - "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/helper-simple-access": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", - "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-modules-umd/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-modules-umd/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/plugin-transform-modules-umd/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-modules-umd/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-modules-umd/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@babel/plugin-transform-modules-umd/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.17.10.tgz", - "integrity": "sha512-v54O6yLaJySCs6mGzaVOUw9T967GnH38T6CQSAtnzdNPwu84l2qAjssKzo/WSO8Yi7NF+7ekm5cVbF/5qiIgNA==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.16.7.tgz", - "integrity": "sha512-xiLDzWNMfKoGOpc6t3U+etCE2yRnn3SM09BXqWPIZOBpL2gvVrBWUKnsJx0K/ADi5F5YC5f8APFfWrz25TdlGg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.7.tgz", - "integrity": "sha512-14J1feiQVWaGvRxj2WjyMuXS2jsBkgB3MdSN5HuC2G5nRspa5RK9COcs82Pwy5BuGcjb+fYaUj94mYcOj7rCvw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-replace-supers": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super/node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-object-super/node_modules/@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-object-super/node_modules/@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-object-super/node_modules/@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-object-super/node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz", - "integrity": "sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-object-super/node_modules/@babel/helper-optimise-call-expression": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz", - "integrity": "sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-object-super/node_modules/@babel/helper-replace-supers": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz", - "integrity": "sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-member-expression-to-functions": "^7.16.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-object-super/node_modules/@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-object-super/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-object-super/node_modules/@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-object-super/node_modules/@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-object-super/node_modules/@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-object-super/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-object-super/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-object-super/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-object-super/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/plugin-transform-object-super/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-object-super/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-object-super/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@babel/plugin-transform-object-super/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.7.tgz", - "integrity": "sha512-AT3MufQ7zZEhU2hwOA11axBnExW0Lszu4RL/tAlUJBuNoRak+wehQW8h6KcXOcgjY42fHtDxswuMhMjFEuv/aw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.7.tgz", - "integrity": "sha512-z4FGr9NMGdoIl1RqavCqGG+ZuYjfZ/hkCIeuH6Do7tXmSm0ls11nYVSJqFEUOSJbDab5wC6lRE/w6YjVcr6Hqw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-constant-elements": { - "version": "7.17.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.17.6.tgz", - "integrity": "sha512-OBv9VkyyKtsHZiHLoSfCn+h6yU7YKX8nrs32xUmOa1SRSk+t03FosB6fBZ0Yz4BpD1WV7l73Nsad+2Tz7APpqw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.16.7.tgz", - "integrity": "sha512-qgIg8BcZgd0G/Cz916D5+9kqX0c7nPZyXaP8R2tLNN5tkyIZdG5fEwBrxwplzSnjC1jvQmyMNVwUCZPcbGY7Pg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.17.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.17.3.tgz", - "integrity": "sha512-9tjBm4O07f7mzKSIlEmPdiE6ub7kfIe6Cd+w+oQebpATfTQMAgW+YOuWxogbKVTulA+MEO7byMeIUtQ1z+z+ZQ==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-jsx": "^7.16.7", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.16.7.tgz", - "integrity": "sha512-RMvQWvpla+xy6MlBpPlrKZCMRs2AGiHOGHY3xRwl0pEeim348dDyxeH4xBsMPbIMhujeq7ihE702eM2Ew0Wo+A==", - "dev": true, - "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", - "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.16.7.tgz", - "integrity": "sha512-hs71ToC97k3QWxswh2ElzMFABXHvGiJ01IB1TbYQDGeWRKWz/MPUTh5jGExdHvosYKpnJW5Pm3S4+TA3FyX+GA==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", - "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.17.9.tgz", - "integrity": "sha512-Lc2TfbxR1HOyn/c6b4Y/b6NHoTb67n/IoWLxTu4kC7h4KQnWlhCq2S8Tx0t2SVvv5Uu87Hs+6JEJ5kt2tYGylQ==", - "dev": true, - "dependencies": { - "regenerator-transform": "^0.15.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.16.7.tgz", - "integrity": "sha512-KQzzDnZ9hWQBjwi5lpY5v9shmm6IVG0U9pB18zvMu2i4H90xpT4gmqwPYsn8rObiadYe2M0gmgsiOIF5A/2rtg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.17.10.tgz", - "integrity": "sha512-6jrMilUAJhktTr56kACL8LnWC5hx3Lf27BS0R0DSyW/OoJfb/iTHeE96V3b1dgKG3FSFdd/0culnYWMkjcKCig==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "babel-plugin-polyfill-corejs2": "^0.3.0", - "babel-plugin-polyfill-corejs3": "^0.5.0", - "babel-plugin-polyfill-regenerator": "^0.3.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime/node_modules/@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-runtime/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-runtime/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz", - "integrity": "sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.7.tgz", - "integrity": "sha512-+pjJpgAngb53L0iaA5gU/1MLXJIfXcYepLgXB3esVRf4fqmj8f2cxM3/FKaHsZms08hFQJkFccEWuIpm429TXg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.7.tgz", - "integrity": "sha512-NJa0Bd/87QV5NZZzTuZG5BPJjLYadeSZ9fO6oOUoL4iQx+9EEuw/eEM92SrsT19Yc2jgB1u1hsjqDtH02c3Drw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.7.tgz", - "integrity": "sha512-VwbkDDUeenlIjmfNeDX/V0aWrQH2QiVyJtwymVQSzItFDTpxfyJh3EVaQiS0rIN/CqbLGr0VcGmuwyTdZtdIsA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.16.7.tgz", - "integrity": "sha512-p2rOixCKRJzpg9JB4gjnG4gjWkWa89ZoYUnl9snJ1cWIcTH/hvxZqfO+WjG6T8DRBpctEol5jw1O5rA8gkCokQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.16.8.tgz", - "integrity": "sha512-bHdQ9k7YpBDO2d0NVfkj51DpQcvwIzIusJ7mEUaMlbZq3Kt/U47j24inXZHQ5MDiYpCs+oZiwnXyKedE8+q7AQ==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-typescript": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.7.tgz", - "integrity": "sha512-TAV5IGahIz3yZ9/Hfv35TV2xEm+kaBDaZQCn2S/hG9/CZ0DktxJv9eKfPc7yYCvOYR4JGx1h8C+jcSOvgaaI/Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.7.tgz", - "integrity": "sha512-oC5tYYKw56HO75KZVLQ+R/Nl3Hro9kf8iG0hXoaHP7tjAyCpvqBiSNe6vGrZni1Z6MggmUOC6A7VP7AVmw225Q==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.17.10.tgz", - "integrity": "sha512-YNgyBHZQpeoBSRBg0xixsZzfT58Ze1iZrajvv0lJc70qDDGuGfonEnMGfWeSY0mQ3JTuCWFbMkzFRVafOyJx4g==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.17.10", - "@babel/helper-compilation-targets": "^7.17.10", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-validator-option": "^7.16.7", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.16.7", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.16.7", - "@babel/plugin-proposal-async-generator-functions": "^7.16.8", - "@babel/plugin-proposal-class-properties": "^7.16.7", - "@babel/plugin-proposal-class-static-block": "^7.17.6", - "@babel/plugin-proposal-dynamic-import": "^7.16.7", - "@babel/plugin-proposal-export-namespace-from": "^7.16.7", - "@babel/plugin-proposal-json-strings": "^7.16.7", - "@babel/plugin-proposal-logical-assignment-operators": "^7.16.7", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7", - "@babel/plugin-proposal-numeric-separator": "^7.16.7", - "@babel/plugin-proposal-object-rest-spread": "^7.17.3", - "@babel/plugin-proposal-optional-catch-binding": "^7.16.7", - "@babel/plugin-proposal-optional-chaining": "^7.16.7", - "@babel/plugin-proposal-private-methods": "^7.16.11", - "@babel/plugin-proposal-private-property-in-object": "^7.16.7", - "@babel/plugin-proposal-unicode-property-regex": "^7.16.7", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.16.7", - "@babel/plugin-transform-async-to-generator": "^7.16.8", - "@babel/plugin-transform-block-scoped-functions": "^7.16.7", - "@babel/plugin-transform-block-scoping": "^7.16.7", - "@babel/plugin-transform-classes": "^7.16.7", - "@babel/plugin-transform-computed-properties": "^7.16.7", - "@babel/plugin-transform-destructuring": "^7.17.7", - "@babel/plugin-transform-dotall-regex": "^7.16.7", - "@babel/plugin-transform-duplicate-keys": "^7.16.7", - "@babel/plugin-transform-exponentiation-operator": "^7.16.7", - "@babel/plugin-transform-for-of": "^7.16.7", - "@babel/plugin-transform-function-name": "^7.16.7", - "@babel/plugin-transform-literals": "^7.16.7", - "@babel/plugin-transform-member-expression-literals": "^7.16.7", - "@babel/plugin-transform-modules-amd": "^7.16.7", - "@babel/plugin-transform-modules-commonjs": "^7.17.9", - "@babel/plugin-transform-modules-systemjs": "^7.17.8", - "@babel/plugin-transform-modules-umd": "^7.16.7", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.17.10", - "@babel/plugin-transform-new-target": "^7.16.7", - "@babel/plugin-transform-object-super": "^7.16.7", - "@babel/plugin-transform-parameters": "^7.16.7", - "@babel/plugin-transform-property-literals": "^7.16.7", - "@babel/plugin-transform-regenerator": "^7.17.9", - "@babel/plugin-transform-reserved-words": "^7.16.7", - "@babel/plugin-transform-shorthand-properties": "^7.16.7", - "@babel/plugin-transform-spread": "^7.16.7", - "@babel/plugin-transform-sticky-regex": "^7.16.7", - "@babel/plugin-transform-template-literals": "^7.16.7", - "@babel/plugin-transform-typeof-symbol": "^7.16.7", - "@babel/plugin-transform-unicode-escapes": "^7.16.7", - "@babel/plugin-transform-unicode-regex": "^7.16.7", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.17.10", - "babel-plugin-polyfill-corejs2": "^0.3.0", - "babel-plugin-polyfill-corejs3": "^0.5.0", - "babel-plugin-polyfill-regenerator": "^0.3.0", - "core-js-compat": "^3.22.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env/node_modules/@babel/compat-data": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz", - "integrity": "sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/preset-env/node_modules/@babel/helper-compilation-targets": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz", - "integrity": "sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.17.10", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.20.2", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/preset-env/node_modules/@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/preset-env/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/preset-env/node_modules/browserslist": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", - "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/@babel/preset-env/node_modules/electron-to-chromium": { - "version": "1.4.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", - "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", - "dev": true - }, - "node_modules/@babel/preset-env/node_modules/node-releases": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", - "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", - "dev": true - }, - "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-react": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.16.7.tgz", - "integrity": "sha512-fWpyI8UM/HE6DfPBzD8LnhQ/OcH8AgTaqcqP2nGOXEUV+VKBR5JRN9hCk9ai+zQQ57vtm9oWeXguBCPNUjytgA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-validator-option": "^7.16.7", - "@babel/plugin-transform-react-display-name": "^7.16.7", - "@babel/plugin-transform-react-jsx": "^7.16.7", - "@babel/plugin-transform-react-jsx-development": "^7.16.7", - "@babel/plugin-transform-react-pure-annotations": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-react/node_modules/@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.16.7.tgz", - "integrity": "sha512-WbVEmgXdIyvzB77AQjGBEyYPZx+8tTsO50XtfozQrkW8QB2rLJpH2lgx0TRw5EJrBxOZQ+wCcyPVQvS8tjEHpQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-validator-option": "^7.16.7", - "@babel/plugin-transform-typescript": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-typescript/node_modules/@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz", - "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", - "dependencies": { - "regenerator-runtime": "^0.13.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/runtime-corejs3": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.14.6.tgz", - "integrity": "sha512-Xl8SPYtdjcMoCsIM4teyVRg7jIcgl8F2kRtoCcXuHzXswt9UxZCS6BzRo8fcnCuP6u2XtPgvyonmEPF57Kxo9Q==", - "dev": true, - "dependencies": { - "core-js-pure": "^3.14.0", - "regenerator-runtime": "^0.13.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", - "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", - "dependencies": { - "@babel/code-frame": "^7.14.5", - "@babel/parser": "^7.14.5", - "@babel/types": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.5.tgz", - "integrity": "sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg==", - "dependencies": { - "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.14.5", - "@babel/helper-function-name": "^7.14.5", - "@babel/helper-hoist-variables": "^7.14.5", - "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.14.5", - "@babel/types": "^7.14.5", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/traverse/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/@babel/types": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", - "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.14.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@csstools/normalize.css": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.0.0.tgz", - "integrity": "sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg==", - "dev": true - }, - "node_modules/@csstools/postcss-color-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.0.tgz", - "integrity": "sha512-5D5ND/mZWcQoSfYnSPsXtuiFxhzmhxt6pcjrFLJyldj+p0ZN2vvRpYNX+lahFTtMhAYOa2WmkdGINr0yP0CvGA==", - "dev": true, - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-color-function/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/@csstools/postcss-font-format-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.0.tgz", - "integrity": "sha512-oO0cZt8do8FdVBX8INftvIA4lUrKUSCcWUf9IwH9IPWOgKT22oAZFXeHLoDK7nhB2SmkNycp5brxfNMRLIhd6Q==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/@csstools/postcss-font-format-keywords/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/@csstools/postcss-hwb-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.0.tgz", - "integrity": "sha512-VSTd7hGjmde4rTj1rR30sokY3ONJph1reCBTUXqeW1fKwETPy1x4t/XIeaaqbMbC5Xg4SM/lyXZ2S8NELT2TaA==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/@csstools/postcss-hwb-function/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/@csstools/postcss-ic-unit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.0.tgz", - "integrity": "sha512-i4yps1mBp2ijrx7E96RXrQXQQHm6F4ym1TOD0D69/sjDjZvQ22tqiEvaNw7pFZTUO5b9vWRHzbHzP9+UKuw+bA==", - "dev": true, - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/@csstools/postcss-ic-unit/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/@csstools/postcss-is-pseudo-class": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.3.tgz", - "integrity": "sha512-wMQ3GMWrJyRQfvBJsD38ndF/nwHT32xevSn8w2X+iCoWqmhhoj0K7HgdGW8XQhah6sdENBa8yS9gRosdezaQZw==", - "dev": true, - "dependencies": { - "@csstools/selector-specificity": "^1.0.0", - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-normalize-display-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.0.tgz", - "integrity": "sha512-bX+nx5V8XTJEmGtpWTO6kywdS725t71YSLlxWt78XoHUbELWgoCXeOFymRJmL3SU1TLlKSIi7v52EWqe60vJTQ==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/@csstools/postcss-normalize-display-values/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/@csstools/postcss-oklab-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.0.tgz", - "integrity": "sha512-e/Q5HopQzmnQgqimG9v3w2IG4VRABsBq3itOcn4bnm+j4enTgQZ0nWsaH/m9GV2otWGQ0nwccYL5vmLKyvP1ww==", - "dev": true, - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-oklab-function/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/@csstools/postcss-progressive-custom-properties": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", - "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/@csstools/postcss-progressive-custom-properties/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/@csstools/postcss-stepped-value-functions": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.0.tgz", - "integrity": "sha512-q8c4bs1GumAiRenmFjASBcWSLKrbzHzWl6C2HcaAxAXIiL2rUlUWbqQZUjwVG5tied0rld19j/Mm90K3qI26vw==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/@csstools/postcss-stepped-value-functions/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/@csstools/postcss-unset-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.1.tgz", - "integrity": "sha512-f1G1WGDXEU/RN1TWAxBPQgQudtLnLQPyiWdtypkPC+mVYNKFKH/HYXSxH4MVNqwF8M0eDsoiU7HumJHCg/L/jg==", - "dev": true, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/@csstools/selector-specificity": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-1.0.0.tgz", - "integrity": "sha512-RkYG5KiGNX0fJ5YoI0f4Wfq2Yo74D25Hru4fxTOioYdQvHBxcrrtTTyT5Ozzh2ejcNrhFy7IEts2WyEY7yi5yw==", - "dev": true, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.3", - "postcss-selector-parser": "^6.0.10" - } - }, - "node_modules/@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "dependencies": { - "@emotion/memoize": "0.7.4" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" - }, - "node_modules/@emotion/stylis": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" - }, - "node_modules/@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" - }, - "node_modules/@eslint/eslintrc": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.3.tgz", - "integrity": "sha512-uGo44hIwoLGNyduRpjdEpovcbMdd+Nv7amtmJxnKmI8xj6yd5LncmSwDa5NgX/41lIFJtkjD6YdVfgEzPfJ5UA==", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.3.2", - "globals": "^13.9.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@eslint/eslintrc/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/@eslint/eslintrc/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/@fortawesome/fontawesome-free": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.1.1.tgz", - "integrity": "sha512-J/3yg2AIXc9wznaVqpHVX3Wa5jwKovVF0AMYSnbmcXTiL3PpRPfF58pzWucCwEiCJBp+hCNRLWClTomD8SseKg==", - "hasInstallScript": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@hookform/error-message": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@hookform/error-message/-/error-message-2.0.0.tgz", - "integrity": "sha512-Y90nHzjgL2MP7GFy75kscdvxrCTjtyxGmOLLxX14nd08OXRIh9lMH/y9Kpdo0p1IPowJBiZMHyueg7p+yrqynQ==", - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "react-hook-form": "^7.0.0" - } - }, - "node_modules/@hookform/resolvers": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.8.9.tgz", - "integrity": "sha512-IXwGpjewxScF4N2kuyYDip6ABqH4lCg9n1f1mp0vbmKik+u+nestpbtdEs6U1WQZxwaoK/2APv1+MEr4czX7XA==", - "peerDependencies": { - "react-hook-form": "^7.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", - "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.0.tgz", - "integrity": "sha512-tscn3dlJFGay47kb4qVruQg/XWlmvU0xp3EJOjzzY+sBaI+YgwKcvAmTcyYU7xEiLLIY5HCdWRooAL8dqkFlDA==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.0", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^28.1.0", - "jest-util": "^28.1.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/core": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-28.1.0.tgz", - "integrity": "sha512-/2PTt0ywhjZ4NwNO4bUqD9IVJfmFVhVKGlhvSpmEfUCuxYf/3NHcKmRFI+I71lYzbTT3wMuYpETDCTHo81gC/g==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/console": "^28.1.0", - "@jest/reporters": "^28.1.0", - "@jest/test-result": "^28.1.0", - "@jest/transform": "^28.1.0", - "@jest/types": "^28.1.0", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^28.0.2", - "jest-config": "^28.1.0", - "jest-haste-map": "^28.1.0", - "jest-message-util": "^28.1.0", - "jest-regex-util": "^28.0.2", - "jest-resolve": "^28.1.0", - "jest-resolve-dependencies": "^28.1.0", - "jest-runner": "^28.1.0", - "jest-runtime": "^28.1.0", - "jest-snapshot": "^28.1.0", - "jest-util": "^28.1.0", - "jest-validate": "^28.1.0", - "jest-watcher": "^28.1.0", - "micromatch": "^4.0.4", - "pretty-format": "^28.1.0", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/core/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/jest-haste-map": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.0.tgz", - "integrity": "sha512-xyZ9sXV8PtKi6NCrJlmq53PyNVHzxmcfXNVvIRHpHmh1j/HChC4pwKgyjj7Z9us19JMw8PpQTJsFWOsIfT93Dw==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.0", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.0", - "jest-worker": "^28.1.0", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/@jest/core/node_modules/jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "dev": true, - "peer": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/core/node_modules/jest-resolve": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-28.1.0.tgz", - "integrity": "sha512-vvfN7+tPNnnhDvISuzD1P+CRVP8cK0FHXRwPAcdDaQv4zgvwvag2n55/h5VjYcM5UJG7L4TwE5tZlzcI0X2Lhw==", - "dev": true, - "peer": true, - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^28.1.0", - "jest-validate": "^28.1.0", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/core/node_modules/jest-validate": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.0.tgz", - "integrity": "sha512-Lly7CJYih3vQBfjLeANGgBSBJ7pEa18cxpQfQEq2go2xyEzehnHfQTjoUia8xUv4x4J80XKFIDwJJThXtRFQXQ==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.0", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^28.0.2", - "leven": "^3.1.0", - "pretty-format": "^28.1.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/core/node_modules/jest-worker": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.0.tgz", - "integrity": "sha512-ZHwM6mNwaWBR52Snff8ZvsCTqQsvhCxP/bT1I6T6DAnb6ygkshsyLQIMxFwHpYxht0HOoqt23JlC01viI7T03A==", - "dev": true, - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/core/node_modules/pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/core/node_modules/react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - }, - "node_modules/@jest/core/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/@jest/environment": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-28.1.0.tgz", - "integrity": "sha512-S44WGSxkRngzHslhV6RoAExekfF7Qhwa6R5+IYFa81mpcj0YgdBnRSmvHe3SNwOt64yXaE5GG8Y2xM28ii5ssA==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/fake-timers": "^28.1.0", - "@jest/types": "^28.1.0", - "@types/node": "*", - "jest-mock": "^28.1.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-28.1.0.tgz", - "integrity": "sha512-be9ETznPLaHOmeJqzYNIXv1ADEzENuQonIoobzThOYPuK/6GhrWNIJDVTgBLCrz3Am73PyEU2urQClZp0hLTtA==", - "dev": true, - "peer": true, - "dependencies": { - "expect": "^28.1.0", - "jest-snapshot": "^28.1.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.0.tgz", - "integrity": "sha512-5BrG48dpC0sB80wpeIX5FU6kolDJI4K0n5BM9a5V38MGx0pyRvUBSS0u2aNTdDzmOrCjhOg8pGs6a20ivYkdmw==", - "dev": true, - "peer": true, - "dependencies": { - "jest-get-type": "^28.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-28.1.0.tgz", - "integrity": "sha512-Xqsf/6VLeAAq78+GNPzI7FZQRf5cCHj1qgQxCjws9n8rKw8r1UYoeaALwBvyuzOkpU3c1I6emeMySPa96rxtIg==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.0", - "@sinonjs/fake-timers": "^9.1.1", - "@types/node": "*", - "jest-message-util": "^28.1.0", - "jest-mock": "^28.1.0", - "jest-util": "^28.1.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-28.1.0.tgz", - "integrity": "sha512-3m7sTg52OTQR6dPhsEQSxAvU+LOBbMivZBwOvKEZ+Rb+GyxVnXi9HKgOTYkx/S99T8yvh17U4tNNJPIEQmtwYw==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/environment": "^28.1.0", - "@jest/expect": "^28.1.0", - "@jest/types": "^28.1.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-28.1.0.tgz", - "integrity": "sha512-qxbFfqap/5QlSpIizH9c/bFCDKsQlM4uAKSOvZrP+nIdrjqre3FmKzpTtYyhsaVcOSNK7TTt2kjm+4BJIjysFA==", - "dev": true, - "peer": true, - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^28.1.0", - "@jest/test-result": "^28.1.0", - "@jest/transform": "^28.1.0", - "@jest/types": "^28.1.0", - "@jridgewell/trace-mapping": "^0.3.7", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-util": "^28.1.0", - "jest-worker": "^28.1.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^9.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/reporters/node_modules/jest-worker": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.0.tgz", - "integrity": "sha512-ZHwM6mNwaWBR52Snff8ZvsCTqQsvhCxP/bT1I6T6DAnb6ygkshsyLQIMxFwHpYxht0HOoqt23JlC01viI7T03A==", - "dev": true, - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/@jest/schemas": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.0.2.tgz", - "integrity": "sha512-YVDJZjd4izeTDkij00vHHAymNXQ6WWsdChFRK86qck6Jpr3DCL5W3Is3vslviRlP+bLuMYRLbdp98amMvqudhA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.23.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-28.0.2.tgz", - "integrity": "sha512-Y9dxC8ZpN3kImkk0LkK5XCEneYMAXlZ8m5bflmSL5vrwyeUpJfentacCUg6fOb8NOpOO7hz2+l37MV77T6BFPw==", - "dev": true, - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.7", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.0.tgz", - "integrity": "sha512-sBBFIyoPzrZho3N+80P35A5oAkSKlGfsEFfXFWuPGBsW40UAjCkGakZhn4UQK4iQlW2vgCDMRDOob9FGKV8YoQ==", - "dev": true, - "dependencies": { - "@jest/console": "^28.1.0", - "@jest/types": "^28.1.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-28.1.0.tgz", - "integrity": "sha512-tZCEiVWlWNTs/2iK9yi6o3AlMfbbYgV4uuZInSVdzZ7ftpHZhCMuhvk2HLYhCZzLgPFQ9MnM1YaxMnh3TILFiQ==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/test-result": "^28.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/test-sequencer/node_modules/jest-haste-map": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.0.tgz", - "integrity": "sha512-xyZ9sXV8PtKi6NCrJlmq53PyNVHzxmcfXNVvIRHpHmh1j/HChC4pwKgyjj7Z9us19JMw8PpQTJsFWOsIfT93Dw==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.0", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.0", - "jest-worker": "^28.1.0", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/@jest/test-sequencer/node_modules/jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "dev": true, - "peer": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/test-sequencer/node_modules/jest-worker": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.0.tgz", - "integrity": "sha512-ZHwM6mNwaWBR52Snff8ZvsCTqQsvhCxP/bT1I6T6DAnb6ygkshsyLQIMxFwHpYxht0HOoqt23JlC01viI7T03A==", - "dev": true, - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/test-sequencer/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/@jest/transform": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-28.1.0.tgz", - "integrity": "sha512-omy2xe5WxlAfqmsTjTPxw+iXRTRnf+NtX0ToG+4S0tABeb4KsKmPUHq5UBuwunHg3tJRwgEQhEp0M/8oiatLEA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^28.1.0", - "@jridgewell/trace-mapping": "^0.3.7", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.0", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/transform/node_modules/jest-haste-map": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.0.tgz", - "integrity": "sha512-xyZ9sXV8PtKi6NCrJlmq53PyNVHzxmcfXNVvIRHpHmh1j/HChC4pwKgyjj7Z9us19JMw8PpQTJsFWOsIfT93Dw==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.0", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.0", - "jest-worker": "^28.1.0", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/@jest/transform/node_modules/jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "dev": true, - "peer": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/transform/node_modules/jest-worker": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.0.tgz", - "integrity": "sha512-ZHwM6mNwaWBR52Snff8ZvsCTqQsvhCxP/bT1I6T6DAnb6ygkshsyLQIMxFwHpYxht0HOoqt23JlC01viI7T03A==", - "dev": true, - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/transform/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/@jest/types": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.0.tgz", - "integrity": "sha512-xmEggMPr317MIOjjDoZ4ejCSr9Lpbt/u34+dvc99t7DS8YirW5rwZEhzKPC2BMUFkUhI48qs6qLUSGw5FuL0GA==", - "dev": true, - "dependencies": { - "@jest/schemas": "^28.0.2", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz", - "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.1.tgz", - "integrity": "sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", - "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.11.tgz", - "integrity": "sha512-RllI476aSMsxzeI9TtlSMoNTgHDxEmnl6GkkHwhr0vdL8W+0WuesyI8Vd3rBOfrwtPXbPxdT9ADJdiOKgzxPQA==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", - "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", - "dev": true - }, - "node_modules/@nestjs/common": { - "version": "8.4.4", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-8.4.4.tgz", - "integrity": "sha512-QHi7QcgH/5Jinz+SCfIZJkFHc6Cch1YsAEGFEhi6wSp6MILb0sJMQ1CX06e9tCOAjSlBwaJj4PH0eFCVau5v9Q==", - "dev": true, - "dependencies": { - "axios": "0.26.1", - "iterare": "1.2.1", - "tslib": "2.3.1", - "uuid": "8.3.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nest" - }, - "peerDependencies": { - "cache-manager": "*", - "class-transformer": "*", - "class-validator": "*", - "reflect-metadata": "^0.1.12", - "rxjs": "^7.1.0" - }, - "peerDependenciesMeta": { - "cache-manager": { - "optional": true - }, - "class-transformer": { - "optional": true - }, - "class-validator": { - "optional": true - } - } - }, - "node_modules/@nestjs/common/node_modules/tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true - }, - "node_modules/@nestjs/core": { - "version": "8.4.4", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-8.4.4.tgz", - "integrity": "sha512-Ef3yJPuzAttpNfehnGqIV5kHIL9SHptB5F4ERxoU7pT61H3xiYpZw6hSjx68cJO7cc6rm7/N+b4zeuJvFHtvBg==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "@nuxtjs/opencollective": "0.3.2", - "fast-safe-stringify": "2.1.1", - "iterare": "1.2.1", - "object-hash": "3.0.0", - "path-to-regexp": "3.2.0", - "tslib": "2.3.1", - "uuid": "8.3.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nest" - }, - "peerDependencies": { - "@nestjs/common": "^8.0.0", - "@nestjs/microservices": "^8.0.0", - "@nestjs/platform-express": "^8.0.0", - "@nestjs/websockets": "^8.0.0", - "reflect-metadata": "^0.1.12", - "rxjs": "^7.1.0" - }, - "peerDependenciesMeta": { - "@nestjs/microservices": { - "optional": true - }, - "@nestjs/platform-express": { - "optional": true - }, - "@nestjs/websockets": { - "optional": true - } - } - }, - "node_modules/@nestjs/core/node_modules/path-to-regexp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", - "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==", - "dev": true - }, - "node_modules/@nestjs/core/node_modules/tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.7.tgz", - "integrity": "sha512-BTIhocbPBSrRmHxOAJFtR18oLhxTtAFDAvL8hY1S3iU8k+E60W/YFs4jrixGzQjMpF4qPXxIQHcjVD9dz1C2QA==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nuxtjs/opencollective": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", - "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "consola": "^2.15.0", - "node-fetch": "^2.6.1" - }, - "bin": { - "opencollective": "bin/opencollective.js" - }, - "engines": { - "node": ">=8.0.0", - "npm": ">=5.0.0" - } - }, - "node_modules/@openapitools/openapi-generator-cli": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.5.1.tgz", - "integrity": "sha512-WSRQBU0dCSVD+0Qv8iCsv0C4iMaZe/NpJ/CT4SmrEYLH3txoKTE8wEfbdj/kqShS8Or0YEGDPUzhSIKY151L0w==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "@nestjs/common": "8.4.4", - "@nestjs/core": "8.4.4", - "@nuxtjs/opencollective": "0.3.2", - "chalk": "4.1.2", - "commander": "8.3.0", - "compare-versions": "4.1.3", - "concurrently": "6.5.1", - "console.table": "0.10.0", - "fs-extra": "10.0.1", - "glob": "7.1.6", - "inquirer": "8.2.2", - "lodash": "4.17.21", - "reflect-metadata": "0.1.13", - "rxjs": "7.5.5", - "tslib": "2.0.3" - }, - "bin": { - "openapi-generator-cli": "main.js" - }, - "engines": { - "node": ">=10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/openapi_generator" - } - }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/fs-extra": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz", - "integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/rxjs": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz", - "integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/rxjs/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true - }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/tslib": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", - "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", - "dev": true - }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.6.tgz", - "integrity": "sha512-IIWxofIYt/AbMwoeBgj+O2aAXLrlCQVg+A4a2zfpXFNHgP8o8rvi3v+oe5t787Lj+KXlKOh8BAiUp9bhuELXhg==", - "dev": true, - "dependencies": { - "ansi-html-community": "^0.0.8", - "common-path-prefix": "^3.0.0", - "core-js-pure": "^3.8.1", - "error-stack-parser": "^2.0.6", - "find-up": "^5.0.0", - "html-entities": "^2.1.0", - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0", - "source-map": "^0.7.3" - }, - "engines": { - "node": ">= 10.13" - }, - "peerDependencies": { - "@types/webpack": "4.x || 5.x", - "react-refresh": ">=0.10.0 <1.0.0", - "sockjs-client": "^1.4.0", - "type-fest": ">=0.17.0 <3.0.0", - "webpack": ">=4.43.0 <6.0.0", - "webpack-dev-server": "3.x || 4.x", - "webpack-hot-middleware": "2.x", - "webpack-plugin-serve": "0.x || 1.x" - }, - "peerDependenciesMeta": { - "@types/webpack": { - "optional": true - }, - "sockjs-client": { - "optional": true - }, - "type-fest": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - }, - "webpack-hot-middleware": { - "optional": true - }, - "webpack-plugin-serve": { - "optional": true - } - } - }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@popperjs/core": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.2.tgz", - "integrity": "sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@reduxjs/toolkit": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.1.tgz", - "integrity": "sha512-Q6mzbTpO9nOYRnkwpDlFOAbQnd3g7zj7CtHAZWz5SzE5lcV97Tf8f3SzOO8BoPOMYBFgfZaqTUZqgGu+a0+Fng==", - "dependencies": { - "immer": "^9.0.7", - "redux": "^4.1.2", - "redux-thunk": "^2.4.1", - "reselect": "^4.1.5" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18", - "react-redux": "^7.2.1 || ^8.0.0-beta" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-babel": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", - "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.10.4", - "@rollup/pluginutils": "^3.1.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "@types/babel__core": "^7.1.9", - "rollup": "^1.20.0||^2.0.0" - }, - "peerDependenciesMeta": { - "@types/babel__core": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "11.2.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", - "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^3.1.0", - "@types/resolve": "1.17.1", - "builtin-modules": "^3.1.0", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" - } - }, - "node_modules/@rollup/plugin-replace": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", - "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^3.1.0", - "magic-string": "^0.25.7" - }, - "peerDependencies": { - "rollup": "^1.20.0 || ^2.0.0" - } - }, - "node_modules/@rollup/pluginutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", - "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", - "dev": true, - "dependencies": { - "@types/estree": "0.0.39", - "estree-walker": "^1.0.1", - "picomatch": "^2.2.2" - }, - "engines": { - "node": ">= 8.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" - } - }, - "node_modules/@rollup/pluginutils/node_modules/@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", - "dev": true - }, - "node_modules/@rooks/use-outside-click-ref": { - "version": "4.11.2", - "resolved": "https://registry.npmjs.org/@rooks/use-outside-click-ref/-/use-outside-click-ref-4.11.2.tgz", - "integrity": "sha512-w2bCW69zcpLh0KmN/odAuBsQ3sps+73KEu7zMOi0o4YMfDo+tXcqwlTJiLYysd0BEoQC9pNIklzZmI9zZep69g==", - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.3.tgz", - "integrity": "sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==", - "dev": true - }, - "node_modules/@sinclair/typebox": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.23.5.tgz", - "integrity": "sha512-AFBVi/iT4g20DHoujvMH1aEDn8fGJh4xsRGCP6d8RpLPMqsNPvW01Jcn0QysXTsg++/xj25NmJsGyH9xug/wKg==", - "dev": true - }, - "node_modules/@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", - "dev": true, - "peer": true, - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/@surma/rollup-plugin-off-main-thread": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", - "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", - "dev": true, - "dependencies": { - "ejs": "^3.1.6", - "json5": "^2.2.0", - "magic-string": "^0.25.0", - "string.prototype.matchall": "^4.0.6" - } - }, - "node_modules/@surma/rollup-plugin-off-main-thread/node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@svgr/babel-plugin-add-jsx-attribute": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz", - "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz", - "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz", - "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz", - "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-svg-dynamic-title": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz", - "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-svg-em-dimensions": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz", - "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-transform-react-native-svg": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz", - "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-transform-svg-component": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz", - "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-preset": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz", - "integrity": "sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==", - "dev": true, - "dependencies": { - "@svgr/babel-plugin-add-jsx-attribute": "^5.4.0", - "@svgr/babel-plugin-remove-jsx-attribute": "^5.4.0", - "@svgr/babel-plugin-remove-jsx-empty-expression": "^5.0.1", - "@svgr/babel-plugin-replace-jsx-attribute-value": "^5.0.1", - "@svgr/babel-plugin-svg-dynamic-title": "^5.4.0", - "@svgr/babel-plugin-svg-em-dimensions": "^5.4.0", - "@svgr/babel-plugin-transform-react-native-svg": "^5.4.0", - "@svgr/babel-plugin-transform-svg-component": "^5.5.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/core": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz", - "integrity": "sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==", - "dev": true, - "dependencies": { - "@svgr/plugin-jsx": "^5.5.0", - "camelcase": "^6.2.0", - "cosmiconfig": "^7.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/hast-util-to-babel-ast": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz", - "integrity": "sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.12.6" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/plugin-jsx": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz", - "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.12.3", - "@svgr/babel-preset": "^5.5.0", - "@svgr/hast-util-to-babel-ast": "^5.5.0", - "svg-parser": "^2.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/plugin-svgo": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz", - "integrity": "sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==", - "dev": true, - "dependencies": { - "cosmiconfig": "^7.0.0", - "deepmerge": "^4.2.2", - "svgo": "^1.2.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/webpack": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz", - "integrity": "sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==", - "dev": true, - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/plugin-transform-react-constant-elements": "^7.12.1", - "@babel/preset-env": "^7.12.1", - "@babel/preset-react": "^7.12.5", - "@svgr/core": "^5.5.0", - "@svgr/plugin-jsx": "^5.5.0", - "@svgr/plugin-svgo": "^5.5.0", - "loader-utils": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@testing-library/dom": { - "version": "8.11.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.11.1.tgz", - "integrity": "sha512-3KQDyx9r0RKYailW2MiYrSSKEfH0GTkI51UGEvJenvcoDoeRYs0PZpi2SXqtnMClQvCqdtTTpOfFETDTVADpAg==", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^4.2.0", - "aria-query": "^5.0.0", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.4.4", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@testing-library/dom/node_modules/aria-query": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz", - "integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.10.tgz", - "integrity": "sha512-Xu9mD0UjrJisTmv7lmVSDMagQcU9R5hwAbxsaAE/35XPnPLJobbuREfV/rraiSaEj/UOvgrzQs66zyTWTlyd+g==" - }, - "node_modules/@testing-library/jest-dom": { - "version": "5.16.4", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.4.tgz", - "integrity": "sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.9.2", - "@types/testing-library__jest-dom": "^5.9.1", - "aria-query": "^5.0.0", - "chalk": "^3.0.0", - "css": "^3.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.5.6", - "lodash": "^4.17.15", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=8", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/aria-query": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz", - "integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==", - "dev": true, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@testing-library/react": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.2.0.tgz", - "integrity": "sha512-Bprbz/SZVONCJy5f7hcihNCv313IJXdYiv0nSJklIs1SQCIHHNlnGNkosSXnGZTmesyGIcBGNppYhXcc11pb7g==", - "dependencies": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^8.5.0", - "@types/react-dom": "^18.0.0" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@testing-library/react/node_modules/@types/react-dom": { - "version": "18.0.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.3.tgz", - "integrity": "sha512-1RRW9kst+67gveJRYPxGmVy8eVJ05O43hg77G2j5m76/RFJtMbcfAs2viQ2UNsvvDg8F7OfQZx8qQcl6ymygaQ==", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@testing-library/user-event": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", - "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", - "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", - "dev": true - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", - "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", - "dev": true - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", - "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", - "dev": true - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", - "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", - "dev": true - }, - "node_modules/@types/aria-query": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==" - }, - "node_modules/@types/babel__core": { - "version": "7.1.19", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz", - "integrity": "sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.17.1", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.17.1.tgz", - "integrity": "sha512-kVzjari1s2YVi77D3w1yuvohV2idweYXMCDzqBiVNN63TcDWrIlTVOYpqVrvbbyOE/IyzBoTKF0fdnLPEORFxA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.3.0" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "dev": true, - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/bonjour": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", - "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.34", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", - "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect-history-api-fallback": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", - "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", - "dev": true, - "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } - }, - "node_modules/@types/eslint": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", - "integrity": "sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==", - "dev": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz", - "integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==", - "dev": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", - "dev": true - }, - "node_modules/@types/eventsource": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.1.8.tgz", - "integrity": "sha512-fJQNt9LijJCZwYvM6O30uLzdpAK9zs52Uc9iUW9M2Zsg0HQM6DLf6QysjC/wuFX+0798B8AppVMvgdO6IftPKQ==", - "dev": true - }, - "node_modules/@types/express": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", - "dev": true, - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.22", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.22.tgz", - "integrity": "sha512-WdqmrUsRS4ootGha6tVwk/IVHM1iorU8tGehftQD2NWiPniw/sm7xdJOIlXLwqdInL9wBw/p7oO8vaYEF3NDmA==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", - "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/history": { - "version": "4.7.11", - "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", - "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", - "dev": true - }, - "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", - "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", - "dependencies": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" - } - }, - "node_modules/@types/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", - "dev": true - }, - "node_modules/@types/http-proxy": { - "version": "1.17.8", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.8.tgz", - "integrity": "sha512-5kPLG5BKpWYkw/LVOGWpiq3nEVqxiN32rTgI53Sk12/xHFQ2rG3ehI9IO+O3W2QoKeyB92dJkoka8SUm6BX1pA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", - "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", - "dev": true - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.1.tgz", - "integrity": "sha512-fUy7YRpT+rHXto1YlL+J9rs0uLGyiqVt3ZOTQR+4ROc47yNl8WLdVLgUloBRhOxP1PZvguHl44T3H0wAWxahYQ==", - "dev": true, - "dependencies": { - "jest-matcher-utils": "^27.0.0", - "pretty-format": "^27.0.0" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "dev": true - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" - }, - "node_modules/@types/lodash": { - "version": "4.14.177", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.177.tgz", - "integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==", - "dev": true - }, - "node_modules/@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", - "dev": true - }, - "node_modules/@types/node": { - "version": "16.11.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz", - "integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==", - "dev": true - }, - "node_modules/@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "dev": true - }, - "node_modules/@types/prettier": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.6.3.tgz", - "integrity": "sha512-ymZk3LEC/fsut+/Q5qejp6R9O1rMxz3XaRHDV6kX8MrGAhOSPqVARbDi+EZvInBpw+BnCX3TD240byVkOfQsHg==", - "dev": true - }, - "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" - }, - "node_modules/@types/q": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz", - "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==", - "dev": true - }, - "node_modules/@types/qs": { - "version": "6.9.6", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz", - "integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==", - "dev": true - }, - "node_modules/@types/range-parser": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", - "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", - "dev": true - }, - "node_modules/@types/react": { - "version": "18.0.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.9.tgz", - "integrity": "sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw==", - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-datepicker": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-4.3.4.tgz", - "integrity": "sha512-5nTTz37KdTUgMZ1AAxztMWNtEnIMVRo8oCAEhIv0a6uUqDjvSKaMyPRpBV+8chi6f/A8wlTKJIpojpXca2dx3A==", - "dev": true, - "dependencies": { - "@popperjs/core": "^2.9.2", - "@types/react": "*", - "date-fns": "^2.0.1", - "react-popper": "^2.2.5" - } - }, - "node_modules/@types/react-dom": { - "version": "18.0.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.3.tgz", - "integrity": "sha512-1RRW9kst+67gveJRYPxGmVy8eVJ05O43hg77G2j5m76/RFJtMbcfAs2viQ2UNsvvDg8F7OfQZx8qQcl6ymygaQ==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/react-redux": { - "version": "7.1.22", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.22.tgz", - "integrity": "sha512-GxIA1kM7ClU73I6wg9IRTVwSO9GS+SAKZKe0Enj+82HMU6aoESFU2HNAdNi3+J53IaOHPiUfT3kSG4L828joDQ==", - "dev": true, - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, - "node_modules/@types/react-router": { - "version": "5.1.18", - "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.18.tgz", - "integrity": "sha512-YYknwy0D0iOwKQgz9v8nOzt2J6l4gouBmDnWqUUznltOTaon+r8US8ky8HvN0tXvc38U9m6z/t2RsVsnd1zM0g==", - "dev": true, - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*" - } - }, - "node_modules/@types/react-router-dom": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", - "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", - "dev": true, - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router": "*" - } - }, - "node_modules/@types/redux-mock-store": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.3.tgz", - "integrity": "sha512-Wqe3tJa6x9MxMN4DJnMfZoBRBRak1XTPklqj4qkVm5VBpZnC8PSADf4kLuFQ9NAdHaowfWoEeUMz7NWc2GMtnA==", - "dev": true, - "dependencies": { - "redux": "^4.0.5" - } - }, - "node_modules/@types/resolve": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", - "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "dev": true - }, - "node_modules/@types/scheduler": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" - }, - "node_modules/@types/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", - "dev": true, - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.13.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", - "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", - "dev": true, - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/sockjs": { - "version": "0.3.33", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", - "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", - "dev": true - }, - "node_modules/@types/styled-components": { - "version": "5.1.18", - "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.18.tgz", - "integrity": "sha512-xPTYmWP7Mxk5TAD3pYsqjwA9G5fAI8e/S51QUJEl7EQD1siKCdiYXIWiH2lzoHRl+QqbQCJMcGv3YTF3OmyPdQ==", - "dev": true, - "dependencies": { - "@types/hoist-non-react-statics": "*", - "@types/react": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/testing-library__jest-dom": { - "version": "5.14.1", - "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.1.tgz", - "integrity": "sha512-Gk9vaXfbzc5zCXI9eYE9BI5BNHEp4D3FWjgqBE/ePGYElLAP+KvxBcsdkwfIVvezs605oiyd/VrpiHe3Oeg+Aw==", - "dev": true, - "dependencies": { - "@types/jest": "*" - } - }, - "node_modules/@types/trusted-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", - "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==", - "dev": true - }, - "node_modules/@types/uuid": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", - "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", - "dev": true - }, - "node_modules/@types/yargs": { - "version": "17.0.10", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.10.tgz", - "integrity": "sha512-gmEaFwpj/7f/ROdtIlci1R1VYU1J4j95m8T+Tj3iBgiBFKg1foE/PSl93bBd5T9LDXNPo8UlNN6W0qwD8O5OaA==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "20.2.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz", - "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", - "dev": true - }, - "node_modules/@types/yup": { - "version": "0.29.13", - "resolved": "https://registry.npmjs.org/@types/yup/-/yup-0.29.13.tgz", - "integrity": "sha512-qRyuv+P/1t1JK1rA+elmK1MmCL1BapEzKKfbEhDBV/LMMse4lmhZ/XbgETI39JveDJRpLjmToOI6uFtMW/WR2g==" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.10.0.tgz", - "integrity": "sha512-XXVKnMsq2fuu9K2KsIxPUGqb6xAImz8MEChClbXmE3VbveFtBUU5bzM6IPVWqzyADIgdkS2Ws/6Xo7W2TeZWjQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "5.10.0", - "@typescript-eslint/type-utils": "5.10.0", - "@typescript-eslint/utils": "5.10.0", - "debug": "^4.3.2", - "functional-red-black-tree": "^1.0.1", - "ignore": "^5.1.8", - "regexpp": "^3.2.0", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/experimental-utils": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.23.0.tgz", - "integrity": "sha512-I+3YGQztH1DM9kgWzjslpZzJCBMRz0KhYG2WP62IwpooeZ1L6Qt0mNK8zs+uP+R2HOsr+TeDW35Pitc3PfVv8Q==", - "dev": true, - "dependencies": { - "@typescript-eslint/utils": "5.23.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/scope-manager": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.23.0.tgz", - "integrity": "sha512-EhjaFELQHCRb5wTwlGsNMvzK9b8Oco4aYNleeDlNuL6qXWDF47ch4EhVNPh8Rdhf9tmqbN4sWDk/8g+Z/J8JVw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/visitor-keys": "5.23.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/types": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.23.0.tgz", - "integrity": "sha512-NfBsV/h4dir/8mJwdZz7JFibaKC3E/QdeMEDJhiAE3/eMkoniZ7MjbEMCGXw6MZnZDMN3G9S0mH/6WUIj91dmw==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.23.0.tgz", - "integrity": "sha512-xE9e0lrHhI647SlGMl+m+3E3CKPF1wzvvOEWnuE3CCjjT7UiRnDGJxmAcVKJIlFgK6DY9RB98eLr1OPigPEOGg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/visitor-keys": "5.23.0", - "debug": "^4.3.2", - "globby": "^11.0.4", - "is-glob": "^4.0.3", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/utils": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.23.0.tgz", - "integrity": "sha512-dbgaKN21drqpkbbedGMNPCtRPZo1IOUr5EI9Jrrh99r5UW5Q0dz46RKXeSBoPV+56R6dFKpbrdhgUNSJsDDRZA==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.23.0", - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/typescript-estree": "5.23.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.23.0.tgz", - "integrity": "sha512-Vd4mFNchU62sJB8pX19ZSPog05B0Y0CE2UxAZPT5k4iqhRYjPnqyY3woMxCd0++t9OTqkgjST+1ydLBi7e2Fvg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.23.0", - "eslint-visitor-keys": "^3.0.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.27.0.tgz", - "integrity": "sha512-8oGjQF46c52l7fMiPPvX4It3u3V3JipssqDfHQ2hcR0AeR8Zge+OYyKUCm5b70X72N1qXt0qgHenwN6Gc2SXZA==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "5.27.0", - "@typescript-eslint/types": "5.27.0", - "@typescript-eslint/typescript-estree": "5.27.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.27.0.tgz", - "integrity": "sha512-VnykheBQ/sHd1Vt0LJ1JLrMH1GzHO+SzX6VTXuStISIsvRiurue/eRkTqSrG0CexHQgKG8shyJfR4o5VYioB9g==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.27.0", - "@typescript-eslint/visitor-keys": "5.27.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.27.0.tgz", - "integrity": "sha512-lY6C7oGm9a/GWhmUDOs3xAVRz4ty/XKlQ2fOLr8GAIryGn0+UBOoJDWyHer3UgrHkenorwvBnphhP+zPmzmw0A==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.27.0.tgz", - "integrity": "sha512-QywPMFvgZ+MHSLRofLI7BDL+UczFFHyj0vF5ibeChDAJgdTV8k4xgEwF0geFhVlPc1p8r70eYewzpo6ps+9LJQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.27.0", - "@typescript-eslint/visitor-keys": "5.27.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.27.0.tgz", - "integrity": "sha512-46cYrteA2MrIAjv9ai44OQDUoCZyHeGIc4lsjCUX2WT6r4C+kidz1bNiR4017wHOPUythYeH+Sc7/cFP97KEAA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.27.0", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@typescript-eslint/parser/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.10.0.tgz", - "integrity": "sha512-tgNgUgb4MhqK6DoKn3RBhyZ9aJga7EQrw+2/OiDk5hKf3pTVZWyqBi7ukP+Z0iEEDMF5FDa64LqODzlfE4O/Dg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.10.0", - "@typescript-eslint/visitor-keys": "5.10.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.10.0.tgz", - "integrity": "sha512-TzlyTmufJO5V886N+hTJBGIfnjQDQ32rJYxPaeiyWKdjsv2Ld5l8cbS7pxim4DeNs62fKzRSt8Q14Evs4JnZyQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/utils": "5.10.0", - "debug": "^4.3.2", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@typescript-eslint/types": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.10.0.tgz", - "integrity": "sha512-wUljCgkqHsMZbw60IbOqT/puLfyqqD5PquGiBo1u1IS3PLxdi3RDGlyf032IJyh+eQoGhz9kzhtZa+VC4eWTlQ==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.10.0.tgz", - "integrity": "sha512-x+7e5IqfwLwsxTdliHRtlIYkgdtYXzE0CkFeV6ytAqq431ZyxCFzNMNR5sr3WOlIG/ihVZr9K/y71VHTF/DUQA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.10.0", - "@typescript-eslint/visitor-keys": "5.10.0", - "debug": "^4.3.2", - "globby": "^11.0.4", - "is-glob": "^4.0.3", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.10.0.tgz", - "integrity": "sha512-IGYwlt1CVcFoE2ueW4/ioEwybR60RAdGeiJX/iDAw0t5w0wK3S7QncDwpmsM70nKgGTuVchEWB8lwZwHqPAWRg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.10.0", - "@typescript-eslint/types": "5.10.0", - "@typescript-eslint/typescript-estree": "5.10.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.10.0.tgz", - "integrity": "sha512-GMxj0K1uyrFLPKASLmZzCuSddmjZVbVj3Ouy5QVuIGKZopxvOr24JsS7gruz6C3GExE01mublZ3mIBOaon9zuQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.10.0", - "eslint-visitor-keys": "^3.0.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz", - "integrity": "sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", - "dev": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", - "dev": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", - "dev": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", - "dev": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", - "dev": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true - }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "dev": true - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ace-builds": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.13.tgz", - "integrity": "sha512-SOLzdaQkY6ecPKYRDDg+MY1WoGgXA34cIvYJNNoBMGGUswHmlauU2Hy0UL96vW0Fs/LgFbMUjD+6vqzWTldIYQ==" - }, - "node_modules/acorn": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", - "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", - "dev": true, - "dependencies": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" - } - }, - "node_modules/acorn-globals/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true, - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "dev": true, - "dependencies": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - } - }, - "node_modules/acorn-node/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/address": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/address/-/address-1.2.0.tgz", - "integrity": "sha512-tNEZYz5G/zYunxFm7sfhAxkXEuLj3K6BKwv6ZURlsF6yiUQ65z0Q2wZW9L5cPUl9ocofGvXOdFYbFHp0+6MOig==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/adjust-sourcemap-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", - "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", - "dev": true, - "dependencies": { - "loader-utils": "^2.0.0", - "regex-parser": "^2.2.11" - }, - "engines": { - "node": ">=8.9" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/agent-base/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/agent-base/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ajv": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", - "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "dev": true, - "engines": [ - "node >= 0.8.0" - ], - "bin": { - "ansi-html": "bin/ansi-html" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ansi-styles/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/ansi-styles/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/aria-query": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.10.2", - "@babel/runtime-corejs3": "^7.10.2" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", - "dev": true - }, - "node_modules/array-includes": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", - "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5", - "get-intrinsic": "^1.1.1", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes/node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes/node_modules/es-abstract": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.0.tgz", - "integrity": "sha512-URbD8tgRthKD3YcC39vbvSDrX23upXnPcnGAjQfgxXF5ID75YcENawc9ZX/9iTP9ptUyfCLIxTTuMYoRfiOVKA==", - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.1", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes/node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes/node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes/node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes/node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes/node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes/node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes/node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes/node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes/node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes/node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes/node_modules/string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes/node_modules/string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes/node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", - "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.2", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat/node_modules/es-abstract": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.0.tgz", - "integrity": "sha512-URbD8tgRthKD3YcC39vbvSDrX23upXnPcnGAjQfgxXF5ID75YcENawc9ZX/9iTP9ptUyfCLIxTTuMYoRfiOVKA==", - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.1", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat/node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat/node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat/node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat/node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat/node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat/node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat/node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat/node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat/node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat/node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat/node_modules/string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat/node_modules/string.prototype.trimend/node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat/node_modules/string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat/node_modules/string.prototype.trimstart/node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat/node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz", - "integrity": "sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.2", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap/node_modules/es-abstract": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.0.tgz", - "integrity": "sha512-URbD8tgRthKD3YcC39vbvSDrX23upXnPcnGAjQfgxXF5ID75YcENawc9ZX/9iTP9ptUyfCLIxTTuMYoRfiOVKA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.1", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap/node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap/node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap/node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap/node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap/node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap/node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap/node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap/node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap/node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap/node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap/node_modules/string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap/node_modules/string.prototype.trimend/node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap/node_modules/string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap/node_modules/string.prototype.trimstart/node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap/node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", - "dev": true - }, - "node_modules/ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", - "dev": true - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", - "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", - "dev": true - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true, - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.7", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.7.tgz", - "integrity": "sha512-ypHju4Y2Oav95SipEcCcI5J7CGPuvz8oat7sUtYj3ClK44bldfvtvcxK6IEK++7rqB7YchDGzweZIBG+SD0ZAA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - } - ], - "dependencies": { - "browserslist": "^4.20.3", - "caniuse-lite": "^1.0.30001335", - "fraction.js": "^4.2.0", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/autoprefixer/node_modules/browserslist": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", - "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/autoprefixer/node_modules/electron-to-chromium": { - "version": "1.4.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", - "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", - "dev": true - }, - "node_modules/autoprefixer/node_modules/node-releases": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", - "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", - "dev": true - }, - "node_modules/autoprefixer/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/axe-core": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.3.5.tgz", - "integrity": "sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "dev": true, - "dependencies": { - "follow-redirects": "^1.14.8" - } - }, - "node_modules/axobject-query": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", - "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", - "dev": true - }, - "node_modules/babel-jest": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.0.tgz", - "integrity": "sha512-zNKk0yhDZ6QUwfxh9k07GII6siNGMJWVUU49gmFj5gfdqDKLqa2RArXOF2CODp4Dr7dLxN2cvAV+667dGJ4b4w==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/transform": "^28.1.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^28.0.2", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-loader": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.5.tgz", - "integrity": "sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==", - "dev": true, - "dependencies": { - "find-cache-dir": "^3.3.1", - "loader-utils": "^2.0.0", - "make-dir": "^3.1.0", - "schema-utils": "^2.6.5" - }, - "engines": { - "node": ">= 8.9" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "webpack": ">=2" - } - }, - "node_modules/babel-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/babel-loader/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/babel-loader/node_modules/schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/babel-plugin-dynamic-import-node": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", - "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", - "dev": true, - "dependencies": { - "object.assign": "^4.1.0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.0.2.tgz", - "integrity": "sha512-Kizhn/ZL+68ZQHxSnHyuvJv8IchXD62KQxV77TBDV/xoBFBOfgRAk97GNs6hXdTTCiVES9nB2I6+7MXXrk5llQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, - "node_modules/babel-plugin-named-asset-import": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz", - "integrity": "sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==", - "dev": true, - "peerDependencies": { - "@babel/core": "^7.1.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz", - "integrity": "sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.13.11", - "@babel/helper-define-polyfill-provider": "^0.3.1", - "semver": "^6.1.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz", - "integrity": "sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.1", - "core-js-compat": "^3.21.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz", - "integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-styled-components": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.13.2.tgz", - "integrity": "sha512-Vb1R3d4g+MUfPQPVDMCGjm3cDocJEUTR7Xq7QS95JWWeksN1wdFRYpD2kulDgI3Huuaf1CZd+NK4KQmqUFh5dA==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.0.0", - "@babel/helper-module-imports": "^7.0.0", - "babel-plugin-syntax-jsx": "^6.18.0", - "lodash": "^4.17.11" - }, - "peerDependencies": { - "styled-components": ">= 2" - } - }, - "node_modules/babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" - }, - "node_modules/babel-plugin-transform-react-remove-prop-types": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", - "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==", - "dev": true - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "dev": true, - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-jest": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-28.0.2.tgz", - "integrity": "sha512-sYzXIdgIXXroJTFeB3S6sNDWtlJ2dllCdTEsnZ65ACrMojj3hVNFRmnJ1HZtomGi+Be7aqpY/HJ92fr8OhKVkQ==", - "dev": true, - "peer": true, - "dependencies": { - "babel-plugin-jest-hoist": "^28.0.2", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-react-app": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.0.1.tgz", - "integrity": "sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.16.0", - "@babel/plugin-proposal-class-properties": "^7.16.0", - "@babel/plugin-proposal-decorators": "^7.16.4", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0", - "@babel/plugin-proposal-numeric-separator": "^7.16.0", - "@babel/plugin-proposal-optional-chaining": "^7.16.0", - "@babel/plugin-proposal-private-methods": "^7.16.0", - "@babel/plugin-transform-flow-strip-types": "^7.16.0", - "@babel/plugin-transform-react-display-name": "^7.16.0", - "@babel/plugin-transform-runtime": "^7.16.4", - "@babel/preset-env": "^7.16.4", - "@babel/preset-react": "^7.16.0", - "@babel/preset-typescript": "^7.16.0", - "@babel/runtime": "^7.16.3", - "babel-plugin-macros": "^3.1.0", - "babel-plugin-transform-react-remove-prop-types": "^0.4.24" - } - }, - "node_modules/babel-preset-react-app/node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/babel-preset-react-app/node_modules/@babel/compat-data": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz", - "integrity": "sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/babel-preset-react-app/node_modules/@babel/core": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.10.tgz", - "integrity": "sha512-liKoppandF3ZcBnIYFjfSDHZLKdLHGJRkoWtG8zQyGJBQfIYobpnVGI5+pLBNtS6psFLDzyq8+h5HiVljW9PNA==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-compilation-targets": "^7.17.10", - "@babel/helper-module-transforms": "^7.17.7", - "@babel/helpers": "^7.17.9", - "@babel/parser": "^7.17.10", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.10", - "@babel/types": "^7.17.10", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/babel-preset-react-app/node_modules/@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/babel-preset-react-app/node_modules/@babel/helper-compilation-targets": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz", - "integrity": "sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.17.10", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.20.2", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-react-app/node_modules/@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/babel-preset-react-app/node_modules/@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/babel-preset-react-app/node_modules/@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/babel-preset-react-app/node_modules/@babel/helper-module-transforms": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", - "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/babel-preset-react-app/node_modules/@babel/helper-simple-access": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", - "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/babel-preset-react-app/node_modules/@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/babel-preset-react-app/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/babel-preset-react-app/node_modules/@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/babel-preset-react-app/node_modules/@babel/helpers": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.9.tgz", - "integrity": "sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q==", - "dev": true, - "dependencies": { - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.9", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/babel-preset-react-app/node_modules/@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/babel-preset-react-app/node_modules/@babel/runtime": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz", - "integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==", - "dev": true, - "dependencies": { - "regenerator-runtime": "^0.13.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/babel-preset-react-app/node_modules/@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/babel-preset-react-app/node_modules/@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/babel-preset-react-app/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/babel-preset-react-app/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/babel-preset-react-app/node_modules/browserslist": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", - "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/babel-preset-react-app/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/babel-preset-react-app/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/babel-preset-react-app/node_modules/electron-to-chromium": { - "version": "1.4.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", - "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", - "dev": true - }, - "node_modules/babel-preset-react-app/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/babel-preset-react-app/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/babel-preset-react-app/node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/babel-preset-react-app/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/babel-preset-react-app/node_modules/node-releases": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", - "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", - "dev": true - }, - "node_modules/babel-preset-react-app/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/babel-preset-react-app/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", - "dev": true - }, - "node_modules/bfj": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.0.2.tgz", - "integrity": "sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw==", - "dev": true, - "dependencies": { - "bluebird": "^3.5.5", - "check-types": "^11.1.1", - "hoopy": "^0.1.4", - "tryer": "^1.0.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, - "node_modules/body-parser": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", - "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", - "dev": true, - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.10.3", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/bonjour-service": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.0.12.tgz", - "integrity": "sha512-pMmguXYCu63Ug37DluMKEHdxc+aaIf/ay4YbF8Gxtba+9d3u+rmEWy61VK3Z3hp8Rskok3BunHYnG0dUHAsblw==", - "dev": true, - "dependencies": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.4" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", - "dev": true - }, - "node_modules/browserslist": { - "version": "4.16.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", - "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", - "dependencies": { - "caniuse-lite": "^1.0.30001219", - "colorette": "^1.2.2", - "electron-to-chromium": "^1.3.723", - "escalade": "^3.1.1", - "node-releases": "^1.1.71" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } - }, - "node_modules/bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, - "node_modules/builtin-modules": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", - "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bulma": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.3.tgz", - "integrity": "sha512-0d7GNW1PY4ud8TWxdNcP6Cc8Bu7MxcntD/RRLGWuiw/s0a9P+XlH/6QoOIrmbj6o8WWJzJYhytiu9nFjTszk1g==" - }, - "node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/camel-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "dev": true, - "dependencies": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - } - }, - "node_modules/camel-case/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true - }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/camelize": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", - "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" - }, - "node_modules/caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "dev": true, - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001344", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001344.tgz", - "integrity": "sha512-0ZFjnlCaXNOAYcV7i+TtdKBp0L/3XEU2MF/x6Du1lrh+SRX4IfzIVL4HNJg5pB2PmFb8rszIGyOvsZnqqRoc2g==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - } - ] - }, - "node_modules/case-sensitive-paths-webpack-plugin": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", - "integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", - "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/charcodes": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/charcodes/-/charcodes-0.2.0.tgz", - "integrity": "sha512-Y4kiDb+AM4Ecy58YkuZrrSRJBDQdQ2L+NyS1vHHFtNtUjgutcZfx3yp1dAONI/oPaPmyGfCLx5CxL+zauIMyKQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, - "node_modules/check-types": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz", - "integrity": "sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ==", - "dev": true - }, - "node_modules/chokidar": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", - "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "dev": true, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/ci-info": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.1.tgz", - "integrity": "sha512-SXgeMX9VwDe7iFFaEWkA5AstuER9YKqy4EhHqr4DVqkwmD9rpVimkMKWHdjn30Ja45txyjhSn63lVX69eVCckg==", - "dev": true - }, - "node_modules/cjs-module-lexer": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", - "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", - "dev": true - }, - "node_modules/classnames": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" - }, - "node_modules/clean-css": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.0.tgz", - "integrity": "sha512-YYuuxv4H/iNb1Z/5IbMRoxgrzjWGhOEFfd+groZ5dMCVkpENiMZmwspdrzBo9286JjM1gZJPAyL7ZIdzuvu2AQ==", - "dev": true, - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 10.0" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-spinners": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", - "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", - "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", - "dev": true, - "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/cli-truncate/node_modules/ansi-styles": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.0.tgz", - "integrity": "sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cli-truncate/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/cli-truncate/node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.0.1.tgz", - "integrity": "sha512-5ohWO/M4//8lErlUUtrFy3b11GtNOuMOU0ysKCDXFcfXuuvUXu95akgj/i8ofmaGdN0hCqyl6uu9i8dS/mQp5g==", - "dev": true, - "dependencies": { - "emoji-regex": "^9.2.2", - "is-fullwidth-code-point": "^4.0.0", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/coa": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", - "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", - "dev": true, - "dependencies": { - "@types/q": "^1.5.1", - "chalk": "^2.4.1", - "q": "^1.1.2" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/coa/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/coa/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/coa/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/coa/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", - "dev": true - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "node_modules/colord": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz", - "integrity": "sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ==", - "dev": true - }, - "node_modules/colorette": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", - "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/common-path-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", - "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", - "dev": true - }, - "node_modules/common-tags": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", - "dev": true, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true - }, - "node_modules/compare-versions": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-4.1.3.tgz", - "integrity": "sha512-WQfnbDcrYnGr55UwbxKiQKASnTtNnaAWVi8jZyy8NTpVAXWACSne8lMD1iaIo9AiU6mnuLvSVshCzewVuWxHUg==", - "dev": true - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dev": true, - "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "node_modules/concurrently": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.5.1.tgz", - "integrity": "sha512-FlSwNpGjWQfRwPLXvJ/OgysbBxPkWpiVjy1042b0U7on7S7qwwMIILRj7WTN1mTgqa582bG6NFuScOoh6Zgdag==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "date-fns": "^2.16.1", - "lodash": "^4.17.21", - "rxjs": "^6.6.3", - "spawn-command": "^0.0.2-1", - "supports-color": "^8.1.0", - "tree-kill": "^1.2.2", - "yargs": "^16.2.0" - }, - "bin": { - "concurrently": "bin/concurrently.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/concurrently/node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, - "node_modules/concurrently/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/confusing-browser-globals": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", - "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", - "dev": true - }, - "node_modules/connect-history-api-fallback": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", - "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/consola": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", - "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", - "dev": true - }, - "node_modules/console.table": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/console.table/-/console.table-0.10.0.tgz", - "integrity": "sha1-CRcCVYiHW+/XDPLv9L7yxuLXXQQ=", - "dev": true, - "dependencies": { - "easy-table": "1.1.0" - }, - "engines": { - "node": "> 0.10" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dependencies": { - "safe-buffer": "~5.1.1" - } - }, - "node_modules/convert-source-map/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "dev": true - }, - "node_modules/core-js": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.14.0.tgz", - "integrity": "sha512-3s+ed8er9ahK+zJpp9ZtuVcDoFzHNiZsPbNAAE4KXgrRHbjSqqNN6xGSXq6bq7TZIbKj4NLrLb6bJ5i+vSVjHA==", - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-compat": { - "version": "3.22.5", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.22.5.tgz", - "integrity": "sha512-rEF75n3QtInrYICvJjrAgV03HwKiYvtKHdPtaba1KucG+cNZ4NJnH9isqt979e67KZlhpbCOTwnsvnIr+CVeOg==", - "dev": true, - "dependencies": { - "browserslist": "^4.20.3", - "semver": "7.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-compat/node_modules/browserslist": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", - "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/core-js-compat/node_modules/electron-to-chromium": { - "version": "1.4.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", - "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", - "dev": true - }, - "node_modules/core-js-compat/node_modules/node-releases": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", - "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", - "dev": true - }, - "node_modules/core-js-compat/node_modules/semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/core-js-pure": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.14.0.tgz", - "integrity": "sha512-YVh+LN2FgNU0odThzm61BsdkwrbrchumFq3oztnE9vTKC4KS2fvnPmcx8t6jnqAyOTCTF4ZSiuK8Qhh7SNcL4g==", - "dev": true, - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, - "node_modules/cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/css": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", - "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", - "dev": true, - "dependencies": { - "inherits": "^2.0.4", - "source-map": "^0.6.1", - "source-map-resolve": "^0.6.0" - } - }, - "node_modules/css-blank-pseudo": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", - "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.9" - }, - "bin": { - "css-blank-pseudo": "dist/cli.cjs" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-color-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", - "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=", - "engines": { - "node": ">=4" - } - }, - "node_modules/css-declaration-sorter": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.2.2.tgz", - "integrity": "sha512-Ufadglr88ZLsrvS11gjeu/40Lw74D9Am/Jpr3LlYm5Q4ZP5KdlUhG+6u2EjyXeZcxmZ2h1ebCKngDjolpeLHpg==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } - }, - "node_modules/css-has-pseudo": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz", - "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.9" - }, - "bin": { - "css-has-pseudo": "dist/cli.cjs" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-loader": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz", - "integrity": "sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==", - "dev": true, - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.7", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.3.5" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/css-loader/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/css-loader/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/css-minimizer-webpack-plugin": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz", - "integrity": "sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==", - "dev": true, - "dependencies": { - "cssnano": "^5.0.6", - "jest-worker": "^27.0.2", - "postcss": "^8.3.5", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@parcel/css": { - "optional": true - }, - "clean-css": { - "optional": true - }, - "csso": { - "optional": true - }, - "esbuild": { - "optional": true - } - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/css-prefers-color-scheme": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", - "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", - "dev": true, - "bin": { - "css-prefers-color-scheme": "dist/cli.cjs" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", - "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^3.2.1", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" - } - }, - "node_modules/css-select-base-adapter": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", - "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", - "dev": true - }, - "node_modules/css-to-react-native": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz", - "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==", - "dependencies": { - "camelize": "^1.0.0", - "css-color-keywords": "^1.0.0", - "postcss-value-parser": "^4.0.2" - } - }, - "node_modules/css-tree": { - "version": "1.0.0-alpha.37", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", - "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", - "dev": true, - "dependencies": { - "mdn-data": "2.0.4", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/css-what": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", - "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", - "dev": true, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", - "dev": true - }, - "node_modules/cssdb": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-6.6.1.tgz", - "integrity": "sha512-0/nZEYfp8SFEzJkMud8NxZJsGfD7RHDJti6GRBLZptIwAzco6RTx1KgwFl4mGWsYS0ZNbCrsY9QryhQ4ldF3Mg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssnano": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.7.tgz", - "integrity": "sha512-pVsUV6LcTXif7lvKKW9ZrmX+rGRzxkEdJuVJcp5ftUjWITgwam5LMZOgaTvUrWPkcORBey6he7JKb4XAJvrpKg==", - "dev": true, - "dependencies": { - "cssnano-preset-default": "^5.2.7", - "lilconfig": "^2.0.3", - "yaml": "^1.10.2" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/cssnano-preset-default": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.7.tgz", - "integrity": "sha512-JiKP38ymZQK+zVKevphPzNSGHSlTI+AOwlasoSRtSVMUU285O7/6uZyd5NbW92ZHp41m0sSHe6JoZosakj63uA==", - "dev": true, - "dependencies": { - "css-declaration-sorter": "^6.2.2", - "cssnano-utils": "^3.1.0", - "postcss-calc": "^8.2.3", - "postcss-colormin": "^5.3.0", - "postcss-convert-values": "^5.1.0", - "postcss-discard-comments": "^5.1.1", - "postcss-discard-duplicates": "^5.1.0", - "postcss-discard-empty": "^5.1.1", - "postcss-discard-overridden": "^5.1.0", - "postcss-merge-longhand": "^5.1.4", - "postcss-merge-rules": "^5.1.1", - "postcss-minify-font-values": "^5.1.0", - "postcss-minify-gradients": "^5.1.1", - "postcss-minify-params": "^5.1.2", - "postcss-minify-selectors": "^5.2.0", - "postcss-normalize-charset": "^5.1.0", - "postcss-normalize-display-values": "^5.1.0", - "postcss-normalize-positions": "^5.1.0", - "postcss-normalize-repeat-style": "^5.1.0", - "postcss-normalize-string": "^5.1.0", - "postcss-normalize-timing-functions": "^5.1.0", - "postcss-normalize-unicode": "^5.1.0", - "postcss-normalize-url": "^5.1.0", - "postcss-normalize-whitespace": "^5.1.1", - "postcss-ordered-values": "^5.1.1", - "postcss-reduce-initial": "^5.1.0", - "postcss-reduce-transforms": "^5.1.0", - "postcss-svgo": "^5.1.0", - "postcss-unique-selectors": "^5.1.1" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/cssnano-utils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", - "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/csso": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", - "dev": true, - "dependencies": { - "css-tree": "^1.1.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dev": true, - "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "dev": true - }, - "node_modules/cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", - "dev": true - }, - "node_modules/cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dev": true, - "dependencies": { - "cssom": "~0.3.6" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - }, - "node_modules/csstype": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz", - "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==" - }, - "node_modules/damerau-levenshtein": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz", - "integrity": "sha512-VvdQIPGdWP0SqFXghj79Wf/5LArmreyMsGLa6FG6iC4t3j7j5s71TrwWmT/4akbDQIqjfACkLZmjXhA7g2oUZw==", - "dev": true - }, - "node_modules/data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", - "dev": true, - "dependencies": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/data-urls/node_modules/tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", - "dev": true, - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/data-urls/node_modules/webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "dev": true, - "engines": { - "node": ">=10.4" - } - }, - "node_modules/data-urls/node_modules/whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", - "dev": true, - "dependencies": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/date-fns": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.23.0.tgz", - "integrity": "sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA==", - "dev": true, - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, - "node_modules/dayjs": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz", - "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decimal.js": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", - "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==", - "dev": true - }, - "node_modules/decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", - "dev": true - }, - "node_modules/deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" - }, - "node_modules/deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", - "dev": true, - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/defaults": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", - "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", - "dev": true, - "dependencies": { - "clone": "^1.0.2" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dependencies": { - "object-keys": "^1.0.12" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", - "dev": true - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true - }, - "node_modules/detect-port-alt": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", - "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", - "dev": true, - "dependencies": { - "address": "^1.0.1", - "debug": "^2.6.0" - }, - "bin": { - "detect": "bin/detect-port", - "detect-port": "bin/detect-port" - }, - "engines": { - "node": ">= 4.2.1" - } - }, - "node_modules/detective": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", - "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", - "dev": true, - "dependencies": { - "acorn-node": "^1.6.1", - "defined": "^1.0.0", - "minimist": "^1.1.1" - }, - "bin": { - "detective": "bin/detective.js" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/diff-match-patch": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", - "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" - }, - "node_modules/diff-sequences": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.0.2.tgz", - "integrity": "sha512-YtEoNynLDFCRznv/XDalsKGSZDoj0U5kLnXvY0JSq3nBboRrZXjD81+eSiwi+nzcZDwedMmcowcxNwwgFW23mQ==", - "dev": true, - "peer": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true - }, - "node_modules/dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", - "dev": true - }, - "node_modules/dns-packet": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.3.1.tgz", - "integrity": "sha512-spBwIj0TK0Ey3666GwIdWVfUpLyubpU53BTCu8iPn4r4oXd9O14Hjg3EHw3ts2oed77/SeckunUYCyRlSngqHw==", - "dev": true, - "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dom-accessibility-api": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.6.tgz", - "integrity": "sha512-DplGLZd8L1lN64jlT27N9TVSESFR5STaEJvX+thCby7fuCHonfPpAlodYc3vuUYbDuDec5w8AMP7oCM5TWFsqw==", - "dev": true - }, - "node_modules/dom-converter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", - "dev": true, - "dependencies": { - "utila": "~0.4" - } - }, - "node_modules/dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "dev": true, - "dependencies": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - } - }, - "node_modules/dom-serializer/node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true - }, - "node_modules/domexception": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", - "dev": true, - "dependencies": { - "webidl-conversions": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/domexception/node_modules/webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dev": true, - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domhandler/node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "dev": true, - "dependencies": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dev": true, - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/dot-case/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true - }, - "node_modules/dotenv": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz", - "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/dotenv-expand": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", - "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", - "dev": true - }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true - }, - "node_modules/easy-table": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.1.0.tgz", - "integrity": "sha1-hvmrTBAvA3G3KXuSplHVgkvIy3M=", - "dev": true, - "optionalDependencies": { - "wcwidth": ">=1.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", - "dev": true - }, - "node_modules/ejs": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.7.tgz", - "integrity": "sha512-BIar7R6abbUxDA3bfXrO4DSgwo8I+fB5/1zgujl3HLLjwd6+9iOnrT+t3grn2qbk9vOgBubXOFwX2m9axoFaGw==", - "dev": true, - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.3.752", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.752.tgz", - "integrity": "sha512-2Tg+7jSl3oPxgsBsWKh5H83QazTkmWG/cnNwJplmyZc7KcN61+I10oUgaXSVk/NwfvN3BdkKDR4FYuRBQQ2v0A==" - }, - "node_modules/emittery": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", - "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz", - "integrity": "sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "dependencies": { - "ansi-colors": "^4.1.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/error-stack-parser": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.7.tgz", - "integrity": "sha512-chLOW0ZGRf4s8raLrDxa5sdkvPec5YdvwbFnqJme4rk0rFajP8mPtrDL1+I+CwrQDCjswDA5sREX7jYQDQs9vA==", - "dev": true, - "dependencies": { - "stackframe": "^1.1.1" - } - }, - "node_modules/es-abstract": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", - "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "is-callable": "^1.2.3", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.10.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", - "dev": true - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", - "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", - "dependencies": { - "has": "^1.0.3" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/escodegen": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/escodegen/node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/eslint": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.15.0.tgz", - "integrity": "sha512-GG5USZ1jhCu8HJkzGgeK8/+RGnHaNYZGrGDzUtigK3BsGESW/rs2az23XqE0WVwDxy1VRvvjSSGu5nB0Bu+6SA==", - "dependencies": { - "@eslint/eslintrc": "^1.2.3", - "@humanwhocodes/config-array": "^0.9.2", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.2", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^6.0.1", - "globals": "^13.6.0", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "regexpp": "^3.2.0", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-airbnb": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", - "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", - "dev": true, - "dependencies": { - "eslint-config-airbnb-base": "^15.0.0", - "object.assign": "^4.1.2", - "object.entries": "^1.1.5" - }, - "engines": { - "node": "^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^7.32.0 || ^8.2.0", - "eslint-plugin-import": "^2.25.3", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-react": "^7.28.0", - "eslint-plugin-react-hooks": "^4.3.0" - } - }, - "node_modules/eslint-config-airbnb-base": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", - "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", - "dev": true, - "dependencies": { - "confusing-browser-globals": "^1.0.10", - "object.assign": "^4.1.2", - "object.entries": "^1.1.5", - "semver": "^6.3.0" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "peerDependencies": { - "eslint": "^7.32.0 || ^8.2.0", - "eslint-plugin-import": "^2.25.2" - } - }, - "node_modules/eslint-config-airbnb-base/node_modules/es-abstract": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", - "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", - "is-string": "^1.0.7", - "is-weakref": "^1.0.1", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-config-airbnb-base/node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-config-airbnb-base/node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-config-airbnb-base/node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-config-airbnb-base/node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-config-airbnb-base/node_modules/object.entries": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", - "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eslint-config-airbnb-base/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-config-airbnb-typescript": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-17.0.0.tgz", - "integrity": "sha512-elNiuzD0kPAPTXjFWg+lE24nMdHMtuxgYoD30OyMD6yrW1AhFZPAg27VX7d3tzOErw+dgJTNWfRSDqEcXb4V0g==", - "dev": true, - "dependencies": { - "eslint-config-airbnb-base": "^15.0.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^5.13.0", - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^7.32.0 || ^8.2.0", - "eslint-plugin-import": "^2.25.3" - } - }, - "node_modules/eslint-config-airbnb/node_modules/es-abstract": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", - "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", - "is-string": "^1.0.7", - "is-weakref": "^1.0.1", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-config-airbnb/node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-config-airbnb/node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-config-airbnb/node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-config-airbnb/node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-config-airbnb/node_modules/object.entries": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", - "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eslint-config-prettier": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", - "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", - "dev": true, - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-config-react-app": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", - "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.16.0", - "@babel/eslint-parser": "^7.16.3", - "@rushstack/eslint-patch": "^1.1.0", - "@typescript-eslint/eslint-plugin": "^5.5.0", - "@typescript-eslint/parser": "^5.5.0", - "babel-preset-react-app": "^10.0.1", - "confusing-browser-globals": "^1.0.11", - "eslint-plugin-flowtype": "^8.0.3", - "eslint-plugin-import": "^2.25.3", - "eslint-plugin-jest": "^25.3.0", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-react": "^7.27.1", - "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-testing-library": "^5.0.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "eslint": "^8.0.0" - } - }, - "node_modules/eslint-config-react-app/node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-config-react-app/node_modules/@babel/compat-data": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz", - "integrity": "sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-config-react-app/node_modules/@babel/core": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.10.tgz", - "integrity": "sha512-liKoppandF3ZcBnIYFjfSDHZLKdLHGJRkoWtG8zQyGJBQfIYobpnVGI5+pLBNtS6psFLDzyq8+h5HiVljW9PNA==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-compilation-targets": "^7.17.10", - "@babel/helper-module-transforms": "^7.17.7", - "@babel/helpers": "^7.17.9", - "@babel/parser": "^7.17.10", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.10", - "@babel/types": "^7.17.10", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/eslint-config-react-app/node_modules/@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-config-react-app/node_modules/@babel/helper-compilation-targets": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz", - "integrity": "sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.17.10", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.20.2", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/eslint-config-react-app/node_modules/@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-config-react-app/node_modules/@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-config-react-app/node_modules/@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-config-react-app/node_modules/@babel/helper-module-transforms": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", - "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-config-react-app/node_modules/@babel/helper-simple-access": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", - "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-config-react-app/node_modules/@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-config-react-app/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-config-react-app/node_modules/@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-config-react-app/node_modules/@babel/helpers": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.9.tgz", - "integrity": "sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q==", - "dev": true, - "dependencies": { - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.9", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-config-react-app/node_modules/@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-config-react-app/node_modules/@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-config-react-app/node_modules/@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-config-react-app/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-config-react-app/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-react-app/node_modules/browserslist": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", - "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/eslint-config-react-app/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-react-app/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/eslint-config-react-app/node_modules/electron-to-chromium": { - "version": "1.4.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", - "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", - "dev": true - }, - "node_modules/eslint-config-react-app/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-react-app/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-react-app/node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-config-react-app/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/eslint-config-react-app/node_modules/node-releases": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", - "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", - "dev": true - }, - "node_modules/eslint-config-react-app/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-config-react-app/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", - "dependencies": { - "debug": "^3.2.7", - "resolve": "^1.20.0" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/eslint-import-resolver-typescript": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz", - "integrity": "sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ==", - "dependencies": { - "debug": "^4.3.4", - "glob": "^7.2.0", - "is-glob": "^4.0.3", - "resolve": "^1.22.0", - "tsconfig-paths": "^3.14.1" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "*", - "eslint-plugin-import": "*" - } - }, - "node_modules/eslint-import-resolver-typescript/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/eslint-import-resolver-typescript/node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/eslint-import-resolver-typescript/node_modules/is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-import-resolver-typescript/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/eslint-import-resolver-typescript/node_modules/resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", - "dependencies": { - "is-core-module": "^2.8.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-import-resolver-typescript/node_modules/tsconfig-paths": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", - "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.1", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", - "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", - "dependencies": { - "debug": "^3.2.7", - "find-up": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/eslint-plugin-flowtype": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz", - "integrity": "sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==", - "dev": true, - "dependencies": { - "lodash": "^4.17.21", - "string-natural-compare": "^3.0.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@babel/plugin-syntax-flow": "^7.14.5", - "@babel/plugin-transform-react-jsx": "^7.14.9", - "eslint": "^8.1.0" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", - "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", - "dependencies": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.3", - "has": "^1.0.3", - "is-core-module": "^2.8.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.values": "^1.1.5", - "resolve": "^1.22.0", - "tsconfig-paths": "^3.14.1" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-import/node_modules/resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", - "dependencies": { - "is-core-module": "^2.8.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-jest": { - "version": "25.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz", - "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/experimental-utils": "^5.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^4.0.0 || ^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@typescript-eslint/eslint-plugin": { - "optional": true - }, - "jest": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-jest-dom": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-4.0.2.tgz", - "integrity": "sha512-Jo51Atwyo2TdcUncjmU+UQeSTKh3sc2LF/M5i/R3nTU0Djw9V65KGJisdm/RtuKhy2KH/r7eQ1n6kwYFPNdHlA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.16.3", - "@testing-library/dom": "^8.11.1", - "requireindex": "^1.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0", - "npm": ">=6", - "yarn": ">=1" - }, - "peerDependencies": { - "eslint": "^6.8.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/eslint-plugin-jest-dom/node_modules/@babel/runtime": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz", - "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==", - "dev": true, - "dependencies": { - "regenerator-runtime": "^0.13.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.5.1.tgz", - "integrity": "sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.16.3", - "aria-query": "^4.2.2", - "array-includes": "^3.1.4", - "ast-types-flow": "^0.0.7", - "axe-core": "^4.3.5", - "axobject-query": "^2.2.0", - "damerau-levenshtein": "^1.0.7", - "emoji-regex": "^9.2.2", - "has": "^1.0.3", - "jsx-ast-utils": "^3.2.1", - "language-tags": "^1.0.5", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/@babel/runtime": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz", - "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==", - "dev": true, - "dependencies": { - "regenerator-runtime": "^0.13.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/array-includes": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", - "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/es-abstract": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", - "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", - "is-string": "^1.0.7", - "is-weakref": "^1.0.1", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/jsx-ast-utils": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz", - "integrity": "sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.3", - "object.assign": "^4.1.2" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz", - "integrity": "sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ==", - "dev": true, - "dependencies": { - "prettier-linter-helpers": "^1.0.0" - }, - "engines": { - "node": ">=6.0.0" - }, - "peerDependencies": { - "eslint": ">=7.28.0", - "prettier": ">=2.0.0" - }, - "peerDependenciesMeta": { - "eslint-config-prettier": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.29.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.29.4.tgz", - "integrity": "sha512-CVCXajliVh509PcZYRFyu/BoUEz452+jtQJq2b3Bae4v3xBUWPLCmtmBM+ZinG4MzwmxJgJ2M5rMqhqLVn7MtQ==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.4", - "array.prototype.flatmap": "^1.2.5", - "doctrine": "^2.1.0", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.5", - "object.fromentries": "^2.0.5", - "object.hasown": "^1.1.0", - "object.values": "^1.1.5", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.3", - "semver": "^6.3.0", - "string.prototype.matchall": "^4.0.6" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.5.0.tgz", - "integrity": "sha512-8k1gRt7D7h03kd+SAAlzXkQwWK22BnK6GKZG+FJA6BAGy22CFvl8kCIXKpVux0cCxMWDQUPqSok0LKaZ0aOcCw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-react/node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.3", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", - "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", - "dev": true, - "dependencies": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-testing-library": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.5.0.tgz", - "integrity": "sha512-eWQ19l6uWL7LW8oeMyQVSGjVYFnBqk7DMHjadm0yOHBvX3Xi9OBrsNuxoAMdX4r7wlQ5WWpW46d+CB6FWFL/PQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/utils": "^5.13.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0", - "npm": ">=6" - }, - "peerDependencies": { - "eslint": "^7.5.0 || ^8.0.0" - } - }, - "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/scope-manager": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.23.0.tgz", - "integrity": "sha512-EhjaFELQHCRb5wTwlGsNMvzK9b8Oco4aYNleeDlNuL6qXWDF47ch4EhVNPh8Rdhf9tmqbN4sWDk/8g+Z/J8JVw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/visitor-keys": "5.23.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/types": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.23.0.tgz", - "integrity": "sha512-NfBsV/h4dir/8mJwdZz7JFibaKC3E/QdeMEDJhiAE3/eMkoniZ7MjbEMCGXw6MZnZDMN3G9S0mH/6WUIj91dmw==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.23.0.tgz", - "integrity": "sha512-xE9e0lrHhI647SlGMl+m+3E3CKPF1wzvvOEWnuE3CCjjT7UiRnDGJxmAcVKJIlFgK6DY9RB98eLr1OPigPEOGg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/visitor-keys": "5.23.0", - "debug": "^4.3.2", - "globby": "^11.0.4", - "is-glob": "^4.0.3", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/utils": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.23.0.tgz", - "integrity": "sha512-dbgaKN21drqpkbbedGMNPCtRPZo1IOUr5EI9Jrrh99r5UW5Q0dz46RKXeSBoPV+56R6dFKpbrdhgUNSJsDDRZA==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.23.0", - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/typescript-estree": "5.23.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.23.0.tgz", - "integrity": "sha512-Vd4mFNchU62sJB8pX19ZSPog05B0Y0CE2UxAZPT5k4iqhRYjPnqyY3woMxCd0++t9OTqkgjST+1ydLBi7e2Fvg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.23.0", - "eslint-visitor-keys": "^3.0.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-testing-library/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-testing-library/node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/eslint-plugin-testing-library/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/eslint-plugin-testing-library/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, - "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-webpack-plugin": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.1.1.tgz", - "integrity": "sha512-xSucskTN9tOkfW7so4EaiFIkulWLXwCB/15H917lR6pTv0Zot6/fetFucmENRb7J5whVSFKIvwnrnsa78SG2yg==", - "dev": true, - "dependencies": { - "@types/eslint": "^7.28.2", - "jest-worker": "^27.3.1", - "micromatch": "^4.0.4", - "normalize-path": "^3.0.0", - "schema-utils": "^3.1.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0", - "webpack": "^5.0.0" - } - }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/eslint/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/eslint/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/espree": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", - "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", - "dependencies": { - "acorn": "^8.7.1", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esquery/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", - "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", - "dev": true - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.0.tgz", - "integrity": "sha512-qFXKl8Pmxk8TBGfaFKRtcQjfXEnKAs+dmlxdwvukJZorwrAabT7M3h8oLOG01I2utEhkmUTi17CHaPBovZsKdw==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/expect-utils": "^28.1.0", - "jest-get-type": "^28.0.2", - "jest-matcher-utils": "^28.1.0", - "jest-message-util": "^28.1.0", - "jest-util": "^28.1.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/expect/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/expect/node_modules/jest-matcher-utils": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.0.tgz", - "integrity": "sha512-onnax0n2uTLRQFKAjC7TuaxibrPSvZgKTcSCnNUz/tOjJ9UhxNm7ZmPpoQavmTDUjXvUQ8KesWk2/VdrxIFzTQ==", - "dev": true, - "peer": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^28.1.0", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/expect/node_modules/pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/expect/node_modules/react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - }, - "node_modules/express": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", - "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", - "dev": true, - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.0", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.10.3", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "dev": true - }, - "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true - }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", - "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "dev": true, - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", - "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", - "dev": true, - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fetch-mock": { - "version": "9.11.0", - "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.11.0.tgz", - "integrity": "sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==", - "dependencies": { - "@babel/core": "^7.0.0", - "@babel/runtime": "^7.0.0", - "core-js": "^3.0.0", - "debug": "^4.1.1", - "glob-to-regexp": "^0.4.0", - "is-subset": "^0.1.1", - "lodash.isequal": "^4.5.0", - "path-to-regexp": "^2.2.1", - "querystring": "^0.2.0", - "whatwg-url": "^6.5.0" - }, - "engines": { - "node": ">=4.0.0" - }, - "funding": { - "type": "charity", - "url": "https://www.justgiving.com/refugee-support-europe" - }, - "peerDependencies": { - "node-fetch": "*" - }, - "peerDependenciesMeta": { - "node-fetch": { - "optional": true - } - } - }, - "node_modules/fetch-mock-jest": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/fetch-mock-jest/-/fetch-mock-jest-1.5.1.tgz", - "integrity": "sha512-+utwzP8C+Pax1GSka3nFXILWMY3Er2L+s090FOgqVNrNCPp0fDqgXnAHAJf12PLHi0z4PhcTaZNTz8e7K3fjqQ==", - "dev": true, - "dependencies": { - "fetch-mock": "^9.11.0" - }, - "engines": { - "node": ">=8.0.0" - }, - "funding": { - "type": "charity", - "url": "https://www.justgiving.com/refugee-support-europe" - }, - "peerDependencies": { - "node-fetch": "*" - }, - "peerDependenciesMeta": { - "node-fetch": { - "optional": true - } - } - }, - "node_modules/fetch-mock/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/fetch-mock/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/fetch-mock/node_modules/path-to-regexp": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz", - "integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==" - }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", - "dev": true, - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/filelist": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.3.tgz", - "integrity": "sha512-LwjCsruLWQULGYKy7TX0OPtrL9kLpojOFKc5VCTxdFTV7w5zbsgqVKfnkKG7Qgjtq50gKfO56hJv88OfcGb70Q==", - "dev": true, - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/filesize": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", - "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dependencies": { - "locate-path": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", - "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==" - }, - "node_modules/follow-redirects": { - "version": "1.14.8", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", - "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/fork-ts-checker-webpack-plugin": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.2.tgz", - "integrity": "sha512-m5cUmF30xkZ7h4tWUgTAcEaKmUW7tfyUyTqNNOz7OxWJ0v1VWKTcOvH8FWHUwSjlW/356Ijc9vi3XfcPstpQKA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "chokidar": "^3.4.2", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "glob": "^7.1.6", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" - }, - "engines": { - "node": ">=10", - "yarn": ">=1.0.0" - }, - "peerDependencies": { - "eslint": ">= 6", - "typescript": ">= 2.7", - "vue-template-compiler": "*", - "webpack": ">= 4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - } - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "dev": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/format-util": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/format-util/-/format-util-1.0.5.tgz", - "integrity": "sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==" - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fraction.js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", - "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", - "dev": true, - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://www.patreon.com/infusion" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-extra": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", - "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/fs-monkey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", - "dev": true - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name/node_modules/es-abstract": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.0.tgz", - "integrity": "sha512-URbD8tgRthKD3YcC39vbvSDrX23upXnPcnGAjQfgxXF5ID75YcENawc9ZX/9iTP9ptUyfCLIxTTuMYoRfiOVKA==", - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.1", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name/node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name/node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name/node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name/node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name/node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name/node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name/node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name/node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name/node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name/node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name/node_modules/string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name/node_modules/string.prototype.trimend/node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name/node_modules/string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name/node_modules/string.prototype.trimstart/node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name/node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-own-enumerable-property-symbols": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", - "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", - "dev": true - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" - }, - "node_modules/global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "dev": true, - "dependencies": { - "global-prefix": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "dev": true, - "dependencies": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/globals": { - "version": "13.14.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.14.0.tgz", - "integrity": "sha512-ERO68sOYwm5UuLvSJTY7w7NP2c8S4UcXs3X1GBX8cwOr+ShOcDBbCY5mH4zxz0jsYCdJ8ve8Mv9n2YGJMB1aeg==", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby/node_modules/ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true - }, - "node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "dev": true, - "dependencies": { - "duplexer": "^0.1.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true - }, - "node_modules/harmony-reflect": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", - "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", - "dev": true - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "dependencies": { - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "bin": { - "he": "bin/he" - } - }, - "node_modules/history": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", - "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", - "dependencies": { - "@babel/runtime": "^7.7.6" - } - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoopy": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", - "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", - "dev": true, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "node_modules/hpack.js/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/hpack.js/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/hpack.js/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", - "dev": true, - "dependencies": { - "whatwg-encoding": "^1.0.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/html-entities": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", - "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", - "dev": true - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "node_modules/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", - "dev": true, - "dependencies": { - "camel-case": "^4.1.2", - "clean-css": "^5.2.2", - "commander": "^8.3.0", - "he": "^1.2.0", - "param-case": "^3.0.4", - "relateurl": "^0.2.7", - "terser": "^5.10.0" - }, - "bin": { - "html-minifier-terser": "cli.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/html-webpack-plugin": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", - "integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==", - "dev": true, - "dependencies": { - "@types/html-minifier-terser": "^6.0.0", - "html-minifier-terser": "^6.0.2", - "lodash": "^4.17.21", - "pretty-error": "^4.0.0", - "tapable": "^2.0.0" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/html-webpack-plugin" - }, - "peerDependencies": { - "webpack": "^5.20.0" - } - }, - "node_modules/htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - }, - "node_modules/htmlparser2/node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/htmlparser2/node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/htmlparser2/node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", - "dev": true - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-parser-js": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.6.tgz", - "integrity": "sha512-vDlkRPDJn93swjcjqMSaGSPABbIarsr1TLAui/gLDXzV5VsJNdXNzMYDyNBLQkjWQCJ1uizu8T2oDMhmGt0PRA==", - "dev": true - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dev": true, - "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http-proxy-agent/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/http-proxy-agent/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", - "dev": true, - "dependencies": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/husky": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/husky/-/husky-7.0.1.tgz", - "integrity": "sha512-gceRaITVZ+cJH9sNHqx5tFwbzlLCVxtVZcusME8JYQ8Edy5mpGDOqD8QBCdMhpyo9a+JXddnujQ4rpY2Ff9SJA==", - "dev": true, - "bin": { - "husky": "lib/bin.js" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/idb": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/idb/-/idb-6.1.5.tgz", - "integrity": "sha512-IJtugpKkiVXQn5Y+LteyBCNk1N8xpGV3wWZk9EVtZWH8DYkjBn0bX1XnGP9RkyZF0sAcywa6unHqSWKe7q4LGw==", - "dev": true - }, - "node_modules/identity-obj-proxy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", - "integrity": "sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ=", - "dev": true, - "dependencies": { - "harmony-reflect": "^1.4.6" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/immer": { - "version": "9.0.12", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz", - "integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/immutable": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz", - "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==" - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dev": true, - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "node_modules/inquirer": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.2.tgz", - "integrity": "sha512-pG7I/si6K/0X7p1qU+rfWnpTE1UIkTONN1wxtzh0d+dHXtT/JG6qBgLxoyHVsQa8cFABxAPh0pD6uUUHiAoaow==", - "dev": true, - "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.1", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.21", - "mute-stream": "0.0.8", - "ora": "^5.4.1", - "run-async": "^2.4.0", - "rxjs": "^7.5.5", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/inquirer/node_modules/rxjs": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz", - "integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/inquirer/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true - }, - "node_modules/internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ipaddr.js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", - "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "node_modules/is-bigint": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", - "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", - "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", - "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", - "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", - "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", - "dev": true - }, - "node_modules/is-negative-zero": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", - "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", - "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true - }, - "node_modules/is-regex": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", - "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-root": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", - "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", - "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", - "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-subset": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", - "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=" - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-weakref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.1.tgz", - "integrity": "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz", - "integrity": "sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A==", - "dev": true, - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/istanbul-reports": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", - "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", - "dev": true, - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/iterare": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", - "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/jake": { - "version": "10.8.5", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", - "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", - "dev": true, - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.1", - "minimatch": "^3.0.4" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-28.1.0.tgz", - "integrity": "sha512-TZR+tHxopPhzw3c3560IJXZWLNHgpcz1Zh0w5A65vynLGNcg/5pZ+VildAd7+XGOu6jd58XMY/HNn0IkZIXVXg==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/core": "^28.1.0", - "import-local": "^3.0.2", - "jest-cli": "^28.1.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-28.0.2.tgz", - "integrity": "sha512-QX9u+5I2s54ZnGoMEjiM2WeBvJR2J7w/8ZUmH2um/WLAuGAYFQcsVXY9+1YL6k0H/AGUdH8pXUAv6erDqEsvIA==", - "dev": true, - "peer": true, - "dependencies": { - "execa": "^5.0.0", - "throat": "^6.0.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-circus": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-28.1.0.tgz", - "integrity": "sha512-rNYfqfLC0L0zQKRKsg4n4J+W1A2fbyGH7Ss/kDIocp9KXD9iaL111glsLu7+Z7FHuZxwzInMDXq+N1ZIBkI/TQ==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/environment": "^28.1.0", - "@jest/expect": "^28.1.0", - "@jest/test-result": "^28.1.0", - "@jest/types": "^28.1.0", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^28.1.0", - "jest-matcher-utils": "^28.1.0", - "jest-message-util": "^28.1.0", - "jest-runtime": "^28.1.0", - "jest-snapshot": "^28.1.0", - "jest-util": "^28.1.0", - "pretty-format": "^28.1.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3", - "throat": "^6.0.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-circus/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/jest-matcher-utils": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.0.tgz", - "integrity": "sha512-onnax0n2uTLRQFKAjC7TuaxibrPSvZgKTcSCnNUz/tOjJ9UhxNm7ZmPpoQavmTDUjXvUQ8KesWk2/VdrxIFzTQ==", - "dev": true, - "peer": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^28.1.0", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-circus/node_modules/pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-circus/node_modules/react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - }, - "node_modules/jest-cli": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-28.1.0.tgz", - "integrity": "sha512-fDJRt6WPRriHrBsvvgb93OxgajHHsJbk4jZxiPqmZbMDRcHskfJBBfTyjFko0jjfprP544hOktdSi9HVgl4VUQ==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/core": "^28.1.0", - "@jest/test-result": "^28.1.0", - "@jest/types": "^28.1.0", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "import-local": "^3.0.2", - "jest-config": "^28.1.0", - "jest-util": "^28.1.0", - "jest-validate": "^28.1.0", - "prompts": "^2.0.1", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-cli/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/jest-validate": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.0.tgz", - "integrity": "sha512-Lly7CJYih3vQBfjLeANGgBSBJ7pEa18cxpQfQEq2go2xyEzehnHfQTjoUia8xUv4x4J80XKFIDwJJThXtRFQXQ==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.0", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^28.0.2", - "leven": "^3.1.0", - "pretty-format": "^28.1.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-cli/node_modules/pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-cli/node_modules/react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - }, - "node_modules/jest-cli/node_modules/yargs": { - "version": "17.5.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", - "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", - "dev": true, - "peer": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/jest-cli/node_modules/yargs-parser": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", - "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/jest-config": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-28.1.0.tgz", - "integrity": "sha512-aOV80E9LeWrmflp7hfZNn/zGA4QKv/xsn2w8QCBP0t0+YqObuCWTSgNbHJ0j9YsTuCO08ZR/wsvlxqqHX20iUA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^28.1.0", - "@jest/types": "^28.1.0", - "babel-jest": "^28.1.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^28.1.0", - "jest-environment-node": "^28.1.0", - "jest-get-type": "^28.0.2", - "jest-regex-util": "^28.0.2", - "jest-resolve": "^28.1.0", - "jest-runner": "^28.1.0", - "jest-util": "^28.1.0", - "jest-validate": "^28.1.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^28.1.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-config/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/jest-haste-map": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.0.tgz", - "integrity": "sha512-xyZ9sXV8PtKi6NCrJlmq53PyNVHzxmcfXNVvIRHpHmh1j/HChC4pwKgyjj7Z9us19JMw8PpQTJsFWOsIfT93Dw==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.0", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.0", - "jest-worker": "^28.1.0", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-config/node_modules/jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "dev": true, - "peer": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-config/node_modules/jest-resolve": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-28.1.0.tgz", - "integrity": "sha512-vvfN7+tPNnnhDvISuzD1P+CRVP8cK0FHXRwPAcdDaQv4zgvwvag2n55/h5VjYcM5UJG7L4TwE5tZlzcI0X2Lhw==", - "dev": true, - "peer": true, - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^28.1.0", - "jest-validate": "^28.1.0", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-config/node_modules/jest-validate": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.0.tgz", - "integrity": "sha512-Lly7CJYih3vQBfjLeANGgBSBJ7pEa18cxpQfQEq2go2xyEzehnHfQTjoUia8xUv4x4J80XKFIDwJJThXtRFQXQ==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.0", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^28.0.2", - "leven": "^3.1.0", - "pretty-format": "^28.1.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-config/node_modules/jest-worker": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.0.tgz", - "integrity": "sha512-ZHwM6mNwaWBR52Snff8ZvsCTqQsvhCxP/bT1I6T6DAnb6ygkshsyLQIMxFwHpYxht0HOoqt23JlC01viI7T03A==", - "dev": true, - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-config/node_modules/pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-config/node_modules/react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - }, - "node_modules/jest-config/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jest-diff": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.0.tgz", - "integrity": "sha512-8eFd3U3OkIKRtlasXfiAQfbovgFgRDb0Ngcs2E+FMeBZ4rUezqIaGjuyggJBp+llosQXNEWofk/Sz4Hr5gMUhA==", - "dev": true, - "peer": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^28.0.2", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-diff/node_modules/react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - }, - "node_modules/jest-docblock": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-28.0.2.tgz", - "integrity": "sha512-FH10WWw5NxLoeSdQlJwu+MTiv60aXV/t8KEwIRGEv74WARE1cXIqh1vGdy2CraHuWOOrnzTWj/azQKqW4fO7xg==", - "dev": true, - "peer": true, - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-each": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-28.1.0.tgz", - "integrity": "sha512-a/XX02xF5NTspceMpHujmOexvJ4GftpYXqr6HhhmKmExtMXsyIN/fvanQlt/BcgFoRKN4OCXxLQKth9/n6OPFg==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.0", - "chalk": "^4.0.0", - "jest-get-type": "^28.0.2", - "jest-util": "^28.1.0", - "pretty-format": "^28.1.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-each/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-each/node_modules/react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - }, - "node_modules/jest-environment-jsdom": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", - "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", - "dev": true, - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1", - "jsdom": "^16.6.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-jsdom/node_modules/@jest/environment": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", - "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", - "dev": true, - "dependencies": { - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-jsdom/node_modules/@jest/fake-timers": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", - "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@sinonjs/fake-timers": "^8.0.1", - "@types/node": "*", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-jsdom/node_modules/@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-jsdom/node_modules/@sinonjs/fake-timers": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", - "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/jest-environment-jsdom/node_modules/@types/yargs": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", - "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-environment-jsdom/node_modules/jest-message-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", - "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^27.5.1", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-jsdom/node_modules/jest-mock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", - "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-jsdom/node_modules/jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-28.1.0.tgz", - "integrity": "sha512-gBLZNiyrPw9CSMlTXF1yJhaBgWDPVvH0Pq6bOEwGMXaYNzhzhw2kA/OijNF8egbCgDS0/veRv97249x2CX+udQ==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/environment": "^28.1.0", - "@jest/fake-timers": "^28.1.0", - "@jest/types": "^28.1.0", - "@types/node": "*", - "jest-mock": "^28.1.0", - "jest-util": "^28.1.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", - "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", - "dev": true, - "peer": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", - "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@types/graceful-fs": "^4.1.2", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^27.5.1", - "jest-serializer": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-haste-map/node_modules/@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-haste-map/node_modules/@types/yargs": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", - "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-haste-map/node_modules/jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz", - "integrity": "sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==", - "dev": true, - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "expect": "^27.5.1", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/@jest/console": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", - "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/@jest/environment": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", - "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", - "dev": true, - "dependencies": { - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/@jest/fake-timers": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", - "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@sinonjs/fake-timers": "^8.0.1", - "@types/node": "*", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/@jest/globals": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz", - "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==", - "dev": true, - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/types": "^27.5.1", - "expect": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/@jest/source-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", - "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9", - "source-map": "^0.6.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/@jest/test-result": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz", - "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==", - "dev": true, - "dependencies": { - "@jest/console": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/@jest/transform": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", - "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.1.0", - "@jest/types": "^27.5.1", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-util": "^27.5.1", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/@sinonjs/fake-timers": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", - "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/jest-jasmine2/node_modules/@types/yargs": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", - "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-jasmine2/node_modules/diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", - "dev": true, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/expect": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", - "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/jest-diff": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", - "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/jest-each": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz", - "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "dev": true, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/jest-message-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", - "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^27.5.1", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/jest-mock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", - "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/jest-runtime": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", - "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==", - "dev": true, - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/globals": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "execa": "^5.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/jest-snapshot": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", - "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.7.2", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.0.0", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__traverse": "^7.0.4", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "natural-compare": "^1.4.0", - "pretty-format": "^27.5.1", - "semver": "^7.3.2" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-jasmine2/node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-jasmine2/node_modules/write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "node_modules/jest-leak-detector": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-28.1.0.tgz", - "integrity": "sha512-uIJDQbxwEL2AMMs2xjhZl2hw8s77c3wrPaQ9v6tXJLGaaQ+4QrNJH5vuw7hA7w/uGT/iJ42a83opAqxGHeyRIA==", - "dev": true, - "peer": true, - "dependencies": { - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-leak-detector/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-leak-detector/node_modules/pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-leak-detector/node_modules/react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - }, - "node_modules/jest-matcher-utils": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", - "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", - "dev": true, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/jest-diff": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", - "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "dev": true, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.0.tgz", - "integrity": "sha512-RpA8mpaJ/B2HphDMiDlrAZdDytkmwFqgjDZovM21F35lHGeUeCvYmm6W+sbQ0ydaLpg5bFAUuWG1cjqOl8vqrw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^28.1.0", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^28.1.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-message-util/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "dependencies": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-message-util/node_modules/react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true - }, - "node_modules/jest-mock": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-28.1.0.tgz", - "integrity": "sha512-H7BrhggNn77WhdL7O1apG0Q/iwl0Bdd5E1ydhCJzL3oBLh/UYxAwR3EJLsBZ9XA3ZU4PA3UNw4tQjduBTCTmLw==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.0", - "@types/node": "*" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", - "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true, - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", - "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", - "dev": true, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz", - "integrity": "sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.0.tgz", - "integrity": "sha512-Ue1VYoSZquPwEvng7Uefw8RmZR+me/1kr30H2jMINjGeHgeO/JgrR6wxj2ofkJ7KSAA11W3cOrhNCbj5Dqqd9g==", - "dev": true, - "peer": true, - "dependencies": { - "jest-regex-util": "^28.0.2", - "jest-snapshot": "^28.1.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-resolve-dependencies/node_modules/jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "dev": true, - "peer": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-resolve/node_modules/@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve/node_modules/@types/yargs": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", - "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-resolve/node_modules/jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runner": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-28.1.0.tgz", - "integrity": "sha512-FBpmuh1HB2dsLklAlRdOxNTTHKFR6G1Qmd80pVDvwbZXTriqjWqjei5DKFC1UlM732KjYcE6yuCdiF0WUCOS2w==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/console": "^28.1.0", - "@jest/environment": "^28.1.0", - "@jest/test-result": "^28.1.0", - "@jest/transform": "^28.1.0", - "@jest/types": "^28.1.0", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.10.2", - "graceful-fs": "^4.2.9", - "jest-docblock": "^28.0.2", - "jest-environment-node": "^28.1.0", - "jest-haste-map": "^28.1.0", - "jest-leak-detector": "^28.1.0", - "jest-message-util": "^28.1.0", - "jest-resolve": "^28.1.0", - "jest-runtime": "^28.1.0", - "jest-util": "^28.1.0", - "jest-watcher": "^28.1.0", - "jest-worker": "^28.1.0", - "source-map-support": "0.5.13", - "throat": "^6.0.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runner/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-runner/node_modules/jest-haste-map": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.0.tgz", - "integrity": "sha512-xyZ9sXV8PtKi6NCrJlmq53PyNVHzxmcfXNVvIRHpHmh1j/HChC4pwKgyjj7Z9us19JMw8PpQTJsFWOsIfT93Dw==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.0", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.0", - "jest-worker": "^28.1.0", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-runner/node_modules/jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "dev": true, - "peer": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runner/node_modules/jest-resolve": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-28.1.0.tgz", - "integrity": "sha512-vvfN7+tPNnnhDvISuzD1P+CRVP8cK0FHXRwPAcdDaQv4zgvwvag2n55/h5VjYcM5UJG7L4TwE5tZlzcI0X2Lhw==", - "dev": true, - "peer": true, - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^28.1.0", - "jest-validate": "^28.1.0", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runner/node_modules/jest-validate": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.0.tgz", - "integrity": "sha512-Lly7CJYih3vQBfjLeANGgBSBJ7pEa18cxpQfQEq2go2xyEzehnHfQTjoUia8xUv4x4J80XKFIDwJJThXtRFQXQ==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.0", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^28.0.2", - "leven": "^3.1.0", - "pretty-format": "^28.1.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runner/node_modules/jest-worker": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.0.tgz", - "integrity": "sha512-ZHwM6mNwaWBR52Snff8ZvsCTqQsvhCxP/bT1I6T6DAnb6ygkshsyLQIMxFwHpYxht0HOoqt23JlC01viI7T03A==", - "dev": true, - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runner/node_modules/pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runner/node_modules/react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - }, - "node_modules/jest-runner/node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/jest-runner/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jest-runtime": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-28.1.0.tgz", - "integrity": "sha512-wNYDiwhdH/TV3agaIyVF0lsJ33MhyujOe+lNTUiolqKt8pchy1Hq4+tDMGbtD5P/oNLA3zYrpx73T9dMTOCAcg==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/environment": "^28.1.0", - "@jest/fake-timers": "^28.1.0", - "@jest/globals": "^28.1.0", - "@jest/source-map": "^28.0.2", - "@jest/test-result": "^28.1.0", - "@jest/transform": "^28.1.0", - "@jest/types": "^28.1.0", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "execa": "^5.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.0", - "jest-message-util": "^28.1.0", - "jest-mock": "^28.1.0", - "jest-regex-util": "^28.0.2", - "jest-resolve": "^28.1.0", - "jest-snapshot": "^28.1.0", - "jest-util": "^28.1.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runtime/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-runtime/node_modules/jest-haste-map": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.0.tgz", - "integrity": "sha512-xyZ9sXV8PtKi6NCrJlmq53PyNVHzxmcfXNVvIRHpHmh1j/HChC4pwKgyjj7Z9us19JMw8PpQTJsFWOsIfT93Dw==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.0", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.0", - "jest-worker": "^28.1.0", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-runtime/node_modules/jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "dev": true, - "peer": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runtime/node_modules/jest-resolve": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-28.1.0.tgz", - "integrity": "sha512-vvfN7+tPNnnhDvISuzD1P+CRVP8cK0FHXRwPAcdDaQv4zgvwvag2n55/h5VjYcM5UJG7L4TwE5tZlzcI0X2Lhw==", - "dev": true, - "peer": true, - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^28.1.0", - "jest-validate": "^28.1.0", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runtime/node_modules/jest-validate": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.0.tgz", - "integrity": "sha512-Lly7CJYih3vQBfjLeANGgBSBJ7pEa18cxpQfQEq2go2xyEzehnHfQTjoUia8xUv4x4J80XKFIDwJJThXtRFQXQ==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.0", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^28.0.2", - "leven": "^3.1.0", - "pretty-format": "^28.1.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runtime/node_modules/jest-worker": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.0.tgz", - "integrity": "sha512-ZHwM6mNwaWBR52Snff8ZvsCTqQsvhCxP/bT1I6T6DAnb6ygkshsyLQIMxFwHpYxht0HOoqt23JlC01viI7T03A==", - "dev": true, - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runtime/node_modules/pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runtime/node_modules/react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - }, - "node_modules/jest-runtime/node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runtime/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jest-serializer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", - "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", - "dev": true, - "dependencies": { - "@types/node": "*", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-28.1.0.tgz", - "integrity": "sha512-ex49M2ZrZsUyQLpLGxQtDbahvgBjlLPgklkqGM0hq/F7W/f8DyqZxVHjdy19QKBm4O93eDp+H5S23EiTbbUmHw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^28.1.0", - "@jest/transform": "^28.1.0", - "@jest/types": "^28.1.0", - "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^28.1.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^28.1.0", - "jest-get-type": "^28.0.2", - "jest-haste-map": "^28.1.0", - "jest-matcher-utils": "^28.1.0", - "jest-message-util": "^28.1.0", - "jest-util": "^28.1.0", - "natural-compare": "^1.4.0", - "pretty-format": "^28.1.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/jest-haste-map": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.0.tgz", - "integrity": "sha512-xyZ9sXV8PtKi6NCrJlmq53PyNVHzxmcfXNVvIRHpHmh1j/HChC4pwKgyjj7Z9us19JMw8PpQTJsFWOsIfT93Dw==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.0", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.0", - "jest-worker": "^28.1.0", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-snapshot/node_modules/jest-matcher-utils": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.0.tgz", - "integrity": "sha512-onnax0n2uTLRQFKAjC7TuaxibrPSvZgKTcSCnNUz/tOjJ9UhxNm7ZmPpoQavmTDUjXvUQ8KesWk2/VdrxIFzTQ==", - "dev": true, - "peer": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^28.1.0", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "dev": true, - "peer": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/jest-worker": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.0.tgz", - "integrity": "sha512-ZHwM6mNwaWBR52Snff8ZvsCTqQsvhCxP/bT1I6T6DAnb6ygkshsyLQIMxFwHpYxht0HOoqt23JlC01viI7T03A==", - "dev": true, - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "peer": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-snapshot/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jest-sonar-reporter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jest-sonar-reporter/-/jest-sonar-reporter-2.0.0.tgz", - "integrity": "sha512-ZervDCgEX5gdUbdtWsjdipLN3bKJwpxbvhkYNXTAYvAckCihobSLr9OT/IuyNIRT1EZMDDwR6DroWtrq+IL64w==", - "dev": true, - "dependencies": { - "xml": "^1.0.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/jest-styled-components": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/jest-styled-components/-/jest-styled-components-7.0.8.tgz", - "integrity": "sha512-0KE54d0yIzKcvtOv8eikyjG3rFRtKYUyQovaoha3nondtZzXYGB3bhsvYgEegU08Iry0ndWx2+g9f5ZzD4I+0Q==", - "dev": true, - "dependencies": { - "css": "^3.0.0" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "styled-components": ">= 5" - } - }, - "node_modules/jest-util": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.0.tgz", - "integrity": "sha512-qYdCKD77k4Hwkose2YBEqQk7PzUf/NSE+rutzceduFveQREeH6b+89Dc9+wjX9dAwHcgdx4yedGA3FQlU/qCTA==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.0", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-validate": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz", - "integrity": "sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", - "leven": "^3.1.0", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-validate/node_modules/@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-validate/node_modules/@types/yargs": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", - "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-validate/node_modules/jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "dev": true, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-watch-typeahead": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-1.1.0.tgz", - "integrity": "sha512-Va5nLSJTN7YFtC2jd+7wsoe1pNe5K4ShLux/E5iHEwlB9AxaxmggY7to9KUqKojhaJw3aXqt5WAb4jGPOolpEw==", - "dev": true, - "dependencies": { - "ansi-escapes": "^4.3.1", - "chalk": "^4.0.0", - "jest-regex-util": "^28.0.0", - "jest-watcher": "^28.0.0", - "slash": "^4.0.0", - "string-length": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "jest": "^27.0.0 || ^28.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "dev": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watch-typeahead/node_modules/string-length": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz", - "integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==", - "dev": true, - "dependencies": { - "char-regex": "^2.0.0", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watch-typeahead/node_modules/string-length/node_modules/char-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.1.tgz", - "integrity": "sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==", - "dev": true, - "engines": { - "node": ">=12.20" - } - }, - "node_modules/jest-watch-typeahead/node_modules/strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/jest-watch-typeahead/node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/jest-watcher": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.0.tgz", - "integrity": "sha512-tNHMtfLE8Njcr2IRS+5rXYA4BhU90gAOwI9frTGOqd+jX0P/Au/JfRSNqsf5nUTcWdbVYuLxS1KjnzILSoR5hA==", - "dev": true, - "dependencies": { - "@jest/test-result": "^28.1.0", - "@jest/types": "^28.1.0", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.10.2", - "jest-util": "^28.1.0", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsdom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", - "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", - "dev": true, - "dependencies": { - "abab": "^2.0.5", - "acorn": "^8.2.4", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", - "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", - "domexception": "^2.0.1", - "escodegen": "^2.0.0", - "form-data": "^3.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", - "xml-name-validator": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", - "dev": true, - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jsdom/node_modules/webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "dev": true, - "engines": { - "node": ">=10.4" - } - }, - "node_modules/jsdom/node_modules/whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", - "dev": true, - "dependencies": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "dev": true - }, - "node_modules/json-schema-faker": { - "version": "0.5.0-rcv.40", - "resolved": "https://registry.npmjs.org/json-schema-faker/-/json-schema-faker-0.5.0-rcv.40.tgz", - "integrity": "sha512-BczZvu03jKrGh3ovCWrHusiX6MwiaKK2WZeyomKBNA8Nm/n7aBYz0mub1CnONB6cgxOZTNxx4afNmLblbUmZbA==", - "dependencies": { - "json-schema-ref-parser": "^6.1.0", - "jsonpath-plus": "^5.1.0" - }, - "bin": { - "jsf": "bin/gen.js" - } - }, - "node_modules/json-schema-ref-parser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-6.1.0.tgz", - "integrity": "sha512-pXe9H1m6IgIpXmE5JSb8epilNTGsmTb2iPohAXpOdhqGFbQjNeHHsZxU+C8w6T81GZxSPFLeUoqDJmzxx5IGuw==", - "dependencies": { - "call-me-maybe": "^1.0.1", - "js-yaml": "^3.12.1", - "ono": "^4.0.11" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" - }, - "node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonpath-plus": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-5.1.0.tgz", - "integrity": "sha512-890w2Pjtj0iswAxalRlt2kHthi6HKrXEfZcn+ZNZptv7F3rUGIeDuZo+C+h4vXBHLEsVjJrHeCm35nYeZLzSBQ==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/jsonpointer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.0.tgz", - "integrity": "sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.0.tgz", - "integrity": "sha512-XzO9luP6L0xkxwhIJMTJQpZo/eeN60K08jHdexfD569AGxeNug6UketeHXEhROoM8aR7EcUoOQmIhcJQjcuq8Q==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.4", - "object.assign": "^4.1.2" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/klona": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", - "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/language-subtag-registry": { - "version": "0.3.21", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", - "integrity": "sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==", - "dev": true - }, - "node_modules/language-tags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", - "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", - "dev": true, - "dependencies": { - "language-subtag-registry": "~0.3.2" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz", - "integrity": "sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "node_modules/lint-staged": { - "version": "12.1.2", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.1.2.tgz", - "integrity": "sha512-bSMcQVqMW98HLLLR2c2tZ+vnDCnx4fd+0QJBQgN/4XkdspGRPc8DGp7UuOEBe1ApCfJ+wXXumYnJmU+wDo7j9A==", - "dev": true, - "dependencies": { - "cli-truncate": "^3.1.0", - "colorette": "^2.0.16", - "commander": "^8.3.0", - "debug": "^4.3.2", - "enquirer": "^2.3.6", - "execa": "^5.1.1", - "lilconfig": "2.0.4", - "listr2": "^3.13.3", - "micromatch": "^4.0.4", - "normalize-path": "^3.0.0", - "object-inspect": "^1.11.0", - "string-argv": "^0.3.1", - "supports-color": "^9.0.2", - "yaml": "^1.10.2" - }, - "bin": { - "lint-staged": "bin/lint-staged.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/lint-staged" - } - }, - "node_modules/lint-staged/node_modules/colorette": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", - "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", - "dev": true - }, - "node_modules/lint-staged/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/lint-staged/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/lint-staged/node_modules/object-inspect": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.1.tgz", - "integrity": "sha512-If7BjFlpkzzBeV1cqgT3OSWT3azyoxDGajR+iGnFBfVV2EWyDyWaZZW2ERDjUaY2QM8i5jI3Sj7mhsM4DDAqWA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/lint-staged/node_modules/supports-color": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.2.1.tgz", - "integrity": "sha512-Obv7ycoCTG51N7y175StI9BlAXrmgZrFhZOb0/PyjHBher/NmsdBgbbQ1Inhq+gIhz6+7Gb+jWF2Vqi7Mf1xnQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/listr2": { - "version": "3.13.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.13.5.tgz", - "integrity": "sha512-3n8heFQDSk+NcwBn3CgxEibZGaRzx+pC64n3YjpMD1qguV4nWus3Al+Oo3KooqFKTQEJ1v7MmnbnyyNspgx3NA==", - "dev": true, - "dependencies": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.16", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rfdc": "^1.3.0", - "rxjs": "^7.4.0", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "enquirer": ">= 2.3.0 < 3" - }, - "peerDependenciesMeta": { - "enquirer": { - "optional": true - } - } - }, - "node_modules/listr2/node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/colorette": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", - "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", - "dev": true - }, - "node_modules/listr2/node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "dev": true, - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", - "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/loader-utils/node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", - "dev": true - }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", - "dev": true - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", - "dev": true - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - }, - "node_modules/lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", - "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", - "dev": true, - "dependencies": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/lower-case/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/lz-string": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", - "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", - "dev": true, - "dependencies": { - "sourcemap-codec": "^1.4.8" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, - "node_modules/makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", - "dev": true, - "dependencies": { - "tmpl": "1.0.x" - } - }, - "node_modules/mdn-data": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", - "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", - "dev": true - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memfs": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz", - "integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==", - "dev": true, - "dependencies": { - "fs-monkey": "1.0.3" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/mini-css-extract-plugin": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.0.tgz", - "integrity": "sha512-ndG8nxCEnAemsg4FSgS+yNyHKgkTB4nPKqCOgh65j3/30qqC5RaSQQXMm++Y6sb6E1zRSxPkztj9fqxhS1Eo6w==", - "dev": true, - "dependencies": { - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "node_modules/multicast-dns": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.4.tgz", - "integrity": "sha512-XkCYOU+rr2Ft3LI6w4ye51M3VK31qJXFIxu0XLw169PtKG0Zx47OrXeVW/GCYOfpC9s1yyyf1S+L8/4LY0J9Zw==", - "dev": true, - "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, - "node_modules/nanoclone": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", - "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==" - }, - "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/no-case/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true - }, - "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "dev": true, - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true - }, - "node_modules/node-releases": { - "version": "1.1.73", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz", - "integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dev": true, - "dependencies": { - "boolbase": "~1.0.0" - } - }, - "node_modules/nwsapi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", - "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", - "dev": true - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", - "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", - "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.entries/node_modules/es-abstract": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.0.tgz", - "integrity": "sha512-URbD8tgRthKD3YcC39vbvSDrX23upXnPcnGAjQfgxXF5ID75YcENawc9ZX/9iTP9ptUyfCLIxTTuMYoRfiOVKA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.1", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries/node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries/node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries/node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries/node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries/node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries/node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries/node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries/node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries/node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries/node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries/node_modules/string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries/node_modules/string.prototype.trimend/node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries/node_modules/string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries/node_modules/string.prototype.trimstart/node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries/node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", - "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries/node_modules/es-abstract": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.0.tgz", - "integrity": "sha512-URbD8tgRthKD3YcC39vbvSDrX23upXnPcnGAjQfgxXF5ID75YcENawc9ZX/9iTP9ptUyfCLIxTTuMYoRfiOVKA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.1", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries/node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries/node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries/node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries/node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries/node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries/node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries/node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries/node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries/node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries/node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries/node_modules/string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries/node_modules/string.prototype.trimend/node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries/node_modules/string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries/node_modules/string.prototype.trimstart/node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries/node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz", - "integrity": "sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors/node_modules/es-abstract": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.0.tgz", - "integrity": "sha512-URbD8tgRthKD3YcC39vbvSDrX23upXnPcnGAjQfgxXF5ID75YcENawc9ZX/9iTP9ptUyfCLIxTTuMYoRfiOVKA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.1", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors/node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors/node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors/node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors/node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors/node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors/node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors/node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors/node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors/node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors/node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors/node_modules/string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors/node_modules/string.prototype.trimend/node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors/node_modules/string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors/node_modules/string.prototype.trimstart/node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors/node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.1.tgz", - "integrity": "sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown/node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown/node_modules/es-abstract": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.0.tgz", - "integrity": "sha512-URbD8tgRthKD3YcC39vbvSDrX23upXnPcnGAjQfgxXF5ID75YcENawc9ZX/9iTP9ptUyfCLIxTTuMYoRfiOVKA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.1", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown/node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown/node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown/node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown/node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown/node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown/node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown/node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown/node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown/node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown/node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown/node_modules/string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown/node_modules/string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown/node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", - "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values/node_modules/es-abstract": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.0.tgz", - "integrity": "sha512-URbD8tgRthKD3YcC39vbvSDrX23upXnPcnGAjQfgxXF5ID75YcENawc9ZX/9iTP9ptUyfCLIxTTuMYoRfiOVKA==", - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.1", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values/node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values/node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values/node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values/node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values/node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values/node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values/node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values/node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values/node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values/node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values/node_modules/string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values/node_modules/string.prototype.trimend/node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values/node_modules/string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values/node_modules/string.prototype.trimstart/node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values/node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ono": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/ono/-/ono-4.0.11.tgz", - "integrity": "sha512-jQ31cORBFE6td25deYeD80wxKBMj+zBmHTrVxnc6CKhx8gho6ipmWM5zj/oeoqioZ99yqBls9Z/9Nss7J26G2g==", - "dependencies": { - "format-util": "^1.0.3" - } - }, - "node_modules/open": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", - "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", - "dev": true, - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dependencies": { - "p-try": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dependencies": { - "p-limit": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "dev": true, - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "engines": { - "node": ">=4" - } - }, - "node_modules/param-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "dev": true, - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/param-case/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", - "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/pascal-case": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dev": true, - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/pascal-case/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true - }, - "node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "engines": { - "node": ">=4" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", - "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-dir/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", - "dev": true, - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-up/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-up/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/postcss": { - "version": "8.4.13", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.13.tgz", - "integrity": "sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - } - ], - "dependencies": { - "nanoid": "^3.3.3", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-attribute-case-insensitive": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.0.tgz", - "integrity": "sha512-b4g9eagFGq9T5SWX4+USfVyjIb3liPnjhHHRMP7FMB2kFVpYyfEscV0wP3eaXhKlcHKUut8lt5BGoeylWA/dBQ==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.2" - }, - "peerDependencies": { - "postcss": "^8.0.2" - } - }, - "node_modules/postcss-browser-comments": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz", - "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "browserslist": ">=4", - "postcss": ">=8" - } - }, - "node_modules/postcss-calc": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", - "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.9", - "postcss-value-parser": "^4.2.0" - }, - "peerDependencies": { - "postcss": "^8.2.2" - } - }, - "node_modules/postcss-calc/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-clamp": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", - "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=7.6.0" - }, - "peerDependencies": { - "postcss": "^8.4.6" - } - }, - "node_modules/postcss-clamp/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-color-functional-notation": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.2.tgz", - "integrity": "sha512-DXVtwUhIk4f49KK5EGuEdgx4Gnyj6+t2jBSEmxvpIK9QI40tWrpS2Pua8Q7iIZWBrki2QOaeUdEaLPPa91K0RQ==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-color-functional-notation/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-color-hex-alpha": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.3.tgz", - "integrity": "sha512-fESawWJCrBV035DcbKRPAVmy21LpoyiXdPTuHUfWJ14ZRjY7Y7PA6P4g8z6LQGYhU1WAxkTxjIjurXzoe68Glw==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-color-hex-alpha/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-color-rebeccapurple": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.0.2.tgz", - "integrity": "sha512-SFc3MaocHaQ6k3oZaFwH8io6MdypkUtEy/eXzXEB1vEQlO3S3oDc/FSZA8AsS04Z25RirQhlDlHLh3dn7XewWw==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/postcss-color-rebeccapurple/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-colormin": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.0.tgz", - "integrity": "sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg==", - "dev": true, - "dependencies": { - "browserslist": "^4.16.6", - "caniuse-api": "^3.0.0", - "colord": "^2.9.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-colormin/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-convert-values": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.0.tgz", - "integrity": "sha512-GkyPbZEYJiWtQB0KZ0X6qusqFHUepguBCNFi9t5JJc7I2OTXG7C0twbTLvCfaKOLl3rSXmpAwV7W5txd91V84g==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-convert-values/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-custom-media": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.0.tgz", - "integrity": "sha512-FvO2GzMUaTN0t1fBULDeIvxr5IvbDXcIatt6pnJghc736nqNgsGao5NT+5+WVLAQiTt6Cb3YUms0jiPaXhL//g==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-custom-properties": { - "version": "12.1.7", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.7.tgz", - "integrity": "sha512-N/hYP5gSoFhaqxi2DPCmvto/ZcRDVjE3T1LiAMzc/bg53hvhcHOLpXOHb526LzBBp5ZlAUhkuot/bfpmpgStJg==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-custom-properties/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-custom-selectors": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.0.tgz", - "integrity": "sha512-/1iyBhz/W8jUepjGyu7V1OPcGbc636snN1yXEQCinb6Bwt7KxsiU7/bLQlp8GwAXzCh7cobBU5odNn/2zQWR8Q==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "postcss": "^8.1.2" - } - }, - "node_modules/postcss-dir-pseudo-class": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.4.tgz", - "integrity": "sha512-I8epwGy5ftdzNWEYok9VjW9whC4xnelAtbajGv4adql4FIF09rnrxnA9Y8xSHN47y7gqFIv10C5+ImsLeJpKBw==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.9" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-discard-comments": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.1.tgz", - "integrity": "sha512-5JscyFmvkUxz/5/+TB3QTTT9Gi9jHkcn8dcmmuN68JQcv3aQg4y88yEHHhwFB52l/NkaJ43O0dbksGMAo49nfQ==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-duplicates": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", - "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-empty": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", - "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-overridden": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", - "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-double-position-gradients": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.1.tgz", - "integrity": "sha512-jM+CGkTs4FcG53sMPjrrGE0rIvLDdCrqMzgDC5fLI7JHDO7o6QG8C5TQBtExb13hdBdoH9C2QVbG4jo2y9lErQ==", - "dev": true, - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-double-position-gradients/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-env-function": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz", - "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-env-function/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-flexbugs-fixes": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", - "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==", - "dev": true, - "peerDependencies": { - "postcss": "^8.1.4" - } - }, - "node_modules/postcss-focus-visible": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", - "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.9" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-focus-within": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", - "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.9" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-font-variant": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", - "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", - "dev": true, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-gap-properties": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.3.tgz", - "integrity": "sha512-rPPZRLPmEKgLk/KlXMqRaNkYTUpE7YC+bOIQFN5xcu1Vp11Y4faIXv6/Jpft6FMnl6YRxZqDZG0qQOW80stzxQ==", - "dev": true, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-image-set-function": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.6.tgz", - "integrity": "sha512-KfdC6vg53GC+vPd2+HYzsZ6obmPqOk6HY09kttU19+Gj1nC3S3XBVEXDHxkhxTohgZqzbUb94bKXvKDnYWBm/A==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-image-set-function/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-initial": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", - "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", - "dev": true, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", - "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", - "dev": true, - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.3.3" - } - }, - "node_modules/postcss-lab-function": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.0.tgz", - "integrity": "sha512-Zb1EO9DGYfa3CP8LhINHCcTTCTLI+R3t7AX2mKsDzdgVQ/GkCpHOTgOr6HBHslP7XDdVbqgHW5vvRPMdVANQ8w==", - "dev": true, - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-lab-function/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", - "dev": true, - "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz", - "integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/postcss-loader": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", - "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", - "dev": true, - "dependencies": { - "cosmiconfig": "^7.0.0", - "klona": "^2.0.5", - "semver": "^7.3.5" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - } - }, - "node_modules/postcss-loader/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/postcss-logical": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", - "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", - "dev": true, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-media-minmax": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", - "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-merge-longhand": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.4.tgz", - "integrity": "sha512-hbqRRqYfmXoGpzYKeW0/NCZhvNyQIlQeWVSao5iKWdyx7skLvCfQFGIUsP9NUs3dSbPac2IC4Go85/zG+7MlmA==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^5.1.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-merge-longhand/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-merge-rules": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.1.tgz", - "integrity": "sha512-8wv8q2cXjEuCcgpIB1Xx1pIy8/rhMPIQqYKNzEdyx37m6gpq83mQQdCxgIkFgliyEnKvdwJf/C61vN4tQDq4Ww==", - "dev": true, - "dependencies": { - "browserslist": "^4.16.6", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^3.1.0", - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-font-values": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", - "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-font-values/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-minify-gradients": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", - "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", - "dev": true, - "dependencies": { - "colord": "^2.9.1", - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-gradients/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-minify-params": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.2.tgz", - "integrity": "sha512-aEP+p71S/urY48HWaRHasyx4WHQJyOYaKpQ6eXl8k0kxg66Wt/30VR6/woh8THgcpRbonJD5IeD+CzNhPi1L8g==", - "dev": true, - "dependencies": { - "browserslist": "^4.16.6", - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-params/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-minify-selectors": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.0.tgz", - "integrity": "sha512-vYxvHkW+iULstA+ctVNx0VoRAR4THQQRkG77o0oa4/mBS0OzGvvzLIvHDv/nNEM0crzN2WIyFU5X7wZhaUK3RA==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", - "dev": true, - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-nested": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", - "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.6" - }, - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-nesting": { - "version": "10.1.5", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.1.5.tgz", - "integrity": "sha512-+NyBBE/wUcJ+NJgVd2FyKIZ414lul6ExqkOt1qXXw7oRzpQ0iT68cVpx+QfHh42QUMHXNoVLlN9InFY9XXK8ng==", - "dev": true, - "dependencies": { - "@csstools/selector-specificity": "1.0.0", - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-normalize": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-10.0.1.tgz", - "integrity": "sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA==", - "dev": true, - "dependencies": { - "@csstools/normalize.css": "*", - "postcss-browser-comments": "^4", - "sanitize.css": "*" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "browserslist": ">= 4", - "postcss": ">= 8" - } - }, - "node_modules/postcss-normalize-charset": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", - "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-display-values": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", - "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-display-values/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-normalize-positions": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.0.tgz", - "integrity": "sha512-8gmItgA4H5xiUxgN/3TVvXRoJxkAWLW6f/KKhdsH03atg0cB8ilXnrB5PpSshwVu/dD2ZsRFQcR1OEmSBDAgcQ==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-positions/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-normalize-repeat-style": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.0.tgz", - "integrity": "sha512-IR3uBjc+7mcWGL6CtniKNQ4Rr5fTxwkaDHwMBDGGs1x9IVRkYIT/M4NelZWkAOBdV6v3Z9S46zqaKGlyzHSchw==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-repeat-style/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-normalize-string": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", - "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-string/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-normalize-timing-functions": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", - "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-timing-functions/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-normalize-unicode": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.0.tgz", - "integrity": "sha512-J6M3MizAAZ2dOdSjy2caayJLQT8E8K9XjLce8AUQMwOrCvjCHv24aLC/Lps1R1ylOfol5VIDMaM/Lo9NGlk1SQ==", - "dev": true, - "dependencies": { - "browserslist": "^4.16.6", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-unicode/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-normalize-url": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", - "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", - "dev": true, - "dependencies": { - "normalize-url": "^6.0.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-url/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-normalize-whitespace": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", - "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-whitespace/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-opacity-percentage": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.2.tgz", - "integrity": "sha512-lyUfF7miG+yewZ8EAk9XUBIlrHyUE6fijnesuz+Mj5zrIHIEw6KcIZSOk/elVMqzLvREmXB83Zi/5QpNRYd47w==", - "dev": true, - "funding": [ - { - "type": "kofi", - "url": "https://ko-fi.com/mrcgrtz" - }, - { - "type": "liberapay", - "url": "https://liberapay.com/mrcgrtz" - } - ], - "engines": { - "node": "^12 || ^14 || >=16" - } - }, - "node_modules/postcss-ordered-values": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.1.tgz", - "integrity": "sha512-7lxgXF0NaoMIgyihL/2boNAEZKiW0+HkMhdKMTD93CjW8TdCy2hSdj8lsAo+uwm7EDG16Da2Jdmtqpedl0cMfw==", - "dev": true, - "dependencies": { - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-ordered-values/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-overflow-shorthand": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.3.tgz", - "integrity": "sha512-CxZwoWup9KXzQeeIxtgOciQ00tDtnylYIlJBBODqkgS/PU2jISuWOL/mYLHmZb9ZhZiCaNKsCRiLp22dZUtNsg==", - "dev": true, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-page-break": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", - "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", - "dev": true, - "peerDependencies": { - "postcss": "^8" - } - }, - "node_modules/postcss-place": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.4.tgz", - "integrity": "sha512-MrgKeiiu5OC/TETQO45kV3npRjOFxEHthsqGtkh3I1rPbZSbXGD/lZVi9j13cYh+NA8PIAPyk6sGjT9QbRyvSg==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-place/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-preset-env": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.5.0.tgz", - "integrity": "sha512-0BJzWEfCdTtK2R3EiKKSdkE51/DI/BwnhlnicSW482Ym6/DGHud8K0wGLcdjip1epVX0HKo4c8zzTeV/SkiejQ==", - "dev": true, - "dependencies": { - "@csstools/postcss-color-function": "^1.1.0", - "@csstools/postcss-font-format-keywords": "^1.0.0", - "@csstools/postcss-hwb-function": "^1.0.0", - "@csstools/postcss-ic-unit": "^1.0.0", - "@csstools/postcss-is-pseudo-class": "^2.0.2", - "@csstools/postcss-normalize-display-values": "^1.0.0", - "@csstools/postcss-oklab-function": "^1.1.0", - "@csstools/postcss-progressive-custom-properties": "^1.3.0", - "@csstools/postcss-stepped-value-functions": "^1.0.0", - "@csstools/postcss-unset-value": "^1.0.0", - "autoprefixer": "^10.4.6", - "browserslist": "^4.20.3", - "css-blank-pseudo": "^3.0.3", - "css-has-pseudo": "^3.0.4", - "css-prefers-color-scheme": "^6.0.3", - "cssdb": "^6.6.1", - "postcss-attribute-case-insensitive": "^5.0.0", - "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^4.2.2", - "postcss-color-hex-alpha": "^8.0.3", - "postcss-color-rebeccapurple": "^7.0.2", - "postcss-custom-media": "^8.0.0", - "postcss-custom-properties": "^12.1.7", - "postcss-custom-selectors": "^6.0.0", - "postcss-dir-pseudo-class": "^6.0.4", - "postcss-double-position-gradients": "^3.1.1", - "postcss-env-function": "^4.0.6", - "postcss-focus-visible": "^6.0.4", - "postcss-focus-within": "^5.0.4", - "postcss-font-variant": "^5.0.0", - "postcss-gap-properties": "^3.0.3", - "postcss-image-set-function": "^4.0.6", - "postcss-initial": "^4.0.1", - "postcss-lab-function": "^4.2.0", - "postcss-logical": "^5.0.4", - "postcss-media-minmax": "^5.0.0", - "postcss-nesting": "^10.1.4", - "postcss-opacity-percentage": "^1.1.2", - "postcss-overflow-shorthand": "^3.0.3", - "postcss-page-break": "^3.0.4", - "postcss-place": "^7.0.4", - "postcss-pseudo-class-any-link": "^7.1.2", - "postcss-replace-overflow-wrap": "^4.0.0", - "postcss-selector-not": "^5.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-preset-env/node_modules/browserslist": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", - "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/postcss-preset-env/node_modules/electron-to-chromium": { - "version": "1.4.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", - "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", - "dev": true - }, - "node_modules/postcss-preset-env/node_modules/node-releases": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", - "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", - "dev": true - }, - "node_modules/postcss-preset-env/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-pseudo-class-any-link": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.3.tgz", - "integrity": "sha512-I9Yp1VV2r8xFwg/JrnAlPCcKmutv6f6Ig6/CHFPqGJiDgYXM9C+0kgLfK4KOXbKNw+63QYl4agRUB0Wi9ftUIg==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-reduce-initial": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.0.tgz", - "integrity": "sha512-5OgTUviz0aeH6MtBjHfbr57tml13PuedK/Ecg8szzd4XRMbYxH4572JFG067z+FqBIf6Zp/d+0581glkvvWMFw==", - "dev": true, - "dependencies": { - "browserslist": "^4.16.6", - "caniuse-api": "^3.0.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-reduce-transforms": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", - "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-reduce-transforms/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-replace-overflow-wrap": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", - "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", - "dev": true, - "peerDependencies": { - "postcss": "^8.0.3" - } - }, - "node_modules/postcss-selector-not": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-5.0.0.tgz", - "integrity": "sha512-/2K3A4TCP9orP4TNS7u3tGdRFVKqz/E6pX3aGnriPG0jU78of8wsUcqE4QAhWEU0d+WnMSF93Ah3F//vUtK+iQ==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-svgo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", - "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0", - "svgo": "^2.7.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-svgo/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/postcss-svgo/node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/postcss-svgo/node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dev": true, - "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/postcss-svgo/node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/postcss-svgo/node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/postcss-svgo/node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/postcss-svgo/node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/postcss-svgo/node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "dev": true - }, - "node_modules/postcss-svgo/node_modules/nth-check": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", - "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/postcss-svgo/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/postcss-svgo/node_modules/svgo": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", - "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", - "dev": true, - "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^4.1.3", - "css-tree": "^1.1.3", - "csso": "^4.2.0", - "picocolors": "^1.0.0", - "stable": "^0.1.8" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/postcss-unique-selectors": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", - "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", - "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/pretty-bytes": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", - "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pretty-error": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", - "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", - "dev": true, - "dependencies": { - "lodash": "^4.17.20", - "renderkid": "^3.0.0" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/pretty-format/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" - }, - "node_modules/pretty-ms": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", - "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", - "dependencies": { - "parse-ms": "^2.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "node_modules/promise": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz", - "integrity": "sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==", - "dev": true, - "dependencies": { - "asap": "~2.0.6" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, - "node_modules/property-expr": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz", - "integrity": "sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg==" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", - "dev": true - }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "engines": { - "node": ">=6" - } - }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", - "dev": true, - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" - } - }, - "node_modules/qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", - "dev": true, - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/querystring": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", - "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/raf": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", - "dev": true, - "dependencies": { - "performance-now": "^2.1.0" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dev": true, - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/react": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz", - "integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-ace": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-9.5.0.tgz", - "integrity": "sha512-4l5FgwGh6K7A0yWVMQlPIXDItM4Q9zzXRqOae8KkCl6MkOob7sC1CzHxZdOGvV+QioKWbX2p5HcdOVUv6cAdSg==", - "dependencies": { - "ace-builds": "^1.4.13", - "diff-match-patch": "^1.0.5", - "lodash.get": "^4.4.2", - "lodash.isequal": "^4.5.0", - "prop-types": "^15.7.2" - }, - "peerDependencies": { - "react": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0", - "react-dom": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/react-ace/node_modules/ace-builds": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.13.tgz", - "integrity": "sha512-SOLzdaQkY6ecPKYRDDg+MY1WoGgXA34cIvYJNNoBMGGUswHmlauU2Hy0UL96vW0Fs/LgFbMUjD+6vqzWTldIYQ==" - }, - "node_modules/react-app-polyfill": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz", - "integrity": "sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w==", - "dev": true, - "dependencies": { - "core-js": "^3.19.2", - "object-assign": "^4.1.1", - "promise": "^8.1.0", - "raf": "^3.4.1", - "regenerator-runtime": "^0.13.9", - "whatwg-fetch": "^3.6.2" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/react-app-polyfill/node_modules/core-js": { - "version": "3.22.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.22.5.tgz", - "integrity": "sha512-VP/xYuvJ0MJWRAobcmQ8F2H6Bsn+s7zqAAjFaHGBMc5AQm7zaelhD1LGduFn2EehEcQcU+br6t+fwbpQ5d1ZWA==", - "dev": true, - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/react-app-polyfill/node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true - }, - "node_modules/react-datepicker": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.5.0.tgz", - "integrity": "sha512-mFP/SbtFSXx21Wx3Nfv+RREwd/x0q14x7QL79ZCi/PVkHSFLwLWhXyOtj3OIzi1AcVYb/fMMcvi8e5b12n8/sg==", - "dependencies": { - "@popperjs/core": "^2.9.2", - "classnames": "^2.2.6", - "date-fns": "^2.24.0", - "prop-types": "^15.7.2", - "react-onclickoutside": "^6.12.0", - "react-popper": "^2.2.5" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17", - "react-dom": "^16.9.0 || ^17" - } - }, - "node_modules/react-datepicker/node_modules/date-fns": { - "version": "2.27.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.27.0.tgz", - "integrity": "sha512-sj+J0Mo2p2X1e306MHq282WS4/A8Pz/95GIFcsPNMPMZVI3EUrAdSv90al1k+p74WGLCruMXk23bfEDZa71X9Q==", - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, - "node_modules/react-dev-utils": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", - "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.0", - "address": "^1.1.2", - "browserslist": "^4.18.1", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "detect-port-alt": "^1.1.6", - "escape-string-regexp": "^4.0.0", - "filesize": "^8.0.6", - "find-up": "^5.0.0", - "fork-ts-checker-webpack-plugin": "^6.5.0", - "global-modules": "^2.0.0", - "globby": "^11.0.4", - "gzip-size": "^6.0.0", - "immer": "^9.0.7", - "is-root": "^2.1.0", - "loader-utils": "^3.2.0", - "open": "^8.4.0", - "pkg-up": "^3.1.0", - "prompts": "^2.4.2", - "react-error-overlay": "^6.0.11", - "recursive-readdir": "^2.2.2", - "shell-quote": "^1.7.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/react-dev-utils/node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/react-dev-utils/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/react-dev-utils/node_modules/@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/react-dev-utils/node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/react-dev-utils/node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/react-dev-utils/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/react-dev-utils/node_modules/browserslist": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", - "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/react-dev-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-dev-utils/node_modules/chalk/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-dev-utils/node_modules/chalk/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-dev-utils/node_modules/chalk/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-dev-utils/node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-dev-utils/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/react-dev-utils/node_modules/electron-to-chromium": { - "version": "1.4.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", - "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", - "dev": true - }, - "node_modules/react-dev-utils/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/react-dev-utils/node_modules/loader-utils": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.0.tgz", - "integrity": "sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ==", - "dev": true, - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/react-dev-utils/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/node-releases": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", - "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", - "dev": true - }, - "node_modules/react-dev-utils/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-dev-utils/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/react-dom": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", - "integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.22.0" - }, - "peerDependencies": { - "react": "^18.1.0" - } - }, - "node_modules/react-error-overlay": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", - "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", - "dev": true - }, - "node_modules/react-fast-compare": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", - "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" - }, - "node_modules/react-hook-form": { - "version": "7.6.9", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.6.9.tgz", - "integrity": "sha512-nz+btC4WFIm3zPBjw22K3t9nnJtlMMwj8slcbPYoTKlkSVA5l+q3Ai+VF0YzeRi7vbyyeGQvpyibov1xd/TV7A==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-hook-form" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/react-multi-select-component": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/react-multi-select-component/-/react-multi-select-component-4.0.6.tgz", - "integrity": "sha512-cNpDv8vh1kWkJiMsa097tTUqWLVTQn+La4aXlgoGOQVpOSH9u1fbj1+MsvnLQjTBySuDx+pzm/DpbIoma/i1Fw==", - "peerDependencies": { - "react": ">=17" - } - }, - "node_modules/react-onclickoutside": { - "version": "6.12.1", - "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.1.tgz", - "integrity": "sha512-a5Q7CkWznBRUWPmocCvE8b6lEYw1s6+opp/60dCunhO+G6E4tDTO2Sd2jKE+leEnnrLAE2Wj5DlDHNqj5wPv1Q==", - "funding": { - "type": "individual", - "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md" - }, - "peerDependencies": { - "react": "^15.5.x || ^16.x || ^17.x", - "react-dom": "^15.5.x || ^16.x || ^17.x" - } - }, - "node_modules/react-popper": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.2.5.tgz", - "integrity": "sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw==", - "dependencies": { - "react-fast-compare": "^3.0.1", - "warning": "^4.0.2" - }, - "peerDependencies": { - "@popperjs/core": "^2.0.0", - "react": "^16.8.0 || ^17" - } - }, - "node_modules/react-redux": { - "version": "7.2.6", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz", - "integrity": "sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==", - "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" - }, - "peerDependencies": { - "react": "^16.8.3 || ^17" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/react-redux/node_modules/@babel/runtime": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz", - "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==", - "dependencies": { - "regenerator-runtime": "^0.13.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/react-redux/node_modules/@types/react-redux": { - "version": "7.1.22", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.22.tgz", - "integrity": "sha512-GxIA1kM7ClU73I6wg9IRTVwSO9GS+SAKZKe0Enj+82HMU6aoESFU2HNAdNi3+J53IaOHPiUfT3kSG4L828joDQ==", - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, - "node_modules/react-redux/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" - }, - "node_modules/react-refresh": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", - "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router-dom": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz", - "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==", - "dependencies": { - "history": "^5.2.0", - "react-router": "6.3.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/react-router-dom/node_modules/react-router": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", - "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==", - "dependencies": { - "history": "^5.2.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-scripts": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", - "integrity": "sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==", - "dev": true, - "dependencies": { - "@babel/core": "^7.16.0", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", - "@svgr/webpack": "^5.5.0", - "babel-jest": "^27.4.2", - "babel-loader": "^8.2.3", - "babel-plugin-named-asset-import": "^0.3.8", - "babel-preset-react-app": "^10.0.1", - "bfj": "^7.0.2", - "browserslist": "^4.18.1", - "camelcase": "^6.2.1", - "case-sensitive-paths-webpack-plugin": "^2.4.0", - "css-loader": "^6.5.1", - "css-minimizer-webpack-plugin": "^3.2.0", - "dotenv": "^10.0.0", - "dotenv-expand": "^5.1.0", - "eslint": "^8.3.0", - "eslint-config-react-app": "^7.0.1", - "eslint-webpack-plugin": "^3.1.1", - "file-loader": "^6.2.0", - "fs-extra": "^10.0.0", - "html-webpack-plugin": "^5.5.0", - "identity-obj-proxy": "^3.0.0", - "jest": "^27.4.3", - "jest-resolve": "^27.4.2", - "jest-watch-typeahead": "^1.0.0", - "mini-css-extract-plugin": "^2.4.5", - "postcss": "^8.4.4", - "postcss-flexbugs-fixes": "^5.0.2", - "postcss-loader": "^6.2.1", - "postcss-normalize": "^10.0.1", - "postcss-preset-env": "^7.0.1", - "prompts": "^2.4.2", - "react-app-polyfill": "^3.0.0", - "react-dev-utils": "^12.0.1", - "react-refresh": "^0.11.0", - "resolve": "^1.20.0", - "resolve-url-loader": "^4.0.0", - "sass-loader": "^12.3.0", - "semver": "^7.3.5", - "source-map-loader": "^3.0.0", - "style-loader": "^3.3.1", - "tailwindcss": "^3.0.2", - "terser-webpack-plugin": "^5.2.5", - "webpack": "^5.64.4", - "webpack-dev-server": "^4.6.0", - "webpack-manifest-plugin": "^4.0.2", - "workbox-webpack-plugin": "^6.4.1" - }, - "bin": { - "react-scripts": "bin/react-scripts.js" - }, - "engines": { - "node": ">=14.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - }, - "peerDependencies": { - "react": ">= 16", - "typescript": "^3.2.1 || ^4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/react-scripts/node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/react-scripts/node_modules/@babel/compat-data": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz", - "integrity": "sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/react-scripts/node_modules/@babel/core": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.10.tgz", - "integrity": "sha512-liKoppandF3ZcBnIYFjfSDHZLKdLHGJRkoWtG8zQyGJBQfIYobpnVGI5+pLBNtS6psFLDzyq8+h5HiVljW9PNA==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-compilation-targets": "^7.17.10", - "@babel/helper-module-transforms": "^7.17.7", - "@babel/helpers": "^7.17.9", - "@babel/parser": "^7.17.10", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.10", - "@babel/types": "^7.17.10", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/react-scripts/node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/react-scripts/node_modules/@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/react-scripts/node_modules/@babel/helper-compilation-targets": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz", - "integrity": "sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.17.10", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.20.2", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/react-scripts/node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/react-scripts/node_modules/@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/react-scripts/node_modules/@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/react-scripts/node_modules/@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/react-scripts/node_modules/@babel/helper-module-transforms": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", - "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/react-scripts/node_modules/@babel/helper-simple-access": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", - "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/react-scripts/node_modules/@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/react-scripts/node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/react-scripts/node_modules/@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/react-scripts/node_modules/@babel/helpers": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.9.tgz", - "integrity": "sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q==", - "dev": true, - "dependencies": { - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.9", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/react-scripts/node_modules/@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/react-scripts/node_modules/@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/react-scripts/node_modules/@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/react-scripts/node_modules/@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/react-scripts/node_modules/@jest/console": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", - "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/@jest/console/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/@jest/console/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/@jest/console/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-scripts/node_modules/@jest/console/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/@jest/console/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/@jest/core": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz", - "integrity": "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==", - "dev": true, - "dependencies": { - "@jest/console": "^27.5.1", - "@jest/reporters": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.8.1", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^27.5.1", - "jest-config": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-resolve-dependencies": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "jest-watcher": "^27.5.1", - "micromatch": "^4.0.4", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/react-scripts/node_modules/@jest/core/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/@jest/core/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/@jest/core/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-scripts/node_modules/@jest/core/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/@jest/core/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/@jest/environment": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", - "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", - "dev": true, - "dependencies": { - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/@jest/fake-timers": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", - "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@sinonjs/fake-timers": "^8.0.1", - "@types/node": "*", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/@jest/globals": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz", - "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==", - "dev": true, - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/types": "^27.5.1", - "expect": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/@jest/reporters": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz", - "integrity": "sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==", - "dev": true, - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-haste-map": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "slash": "^3.0.0", - "source-map": "^0.6.0", - "string-length": "^4.0.1", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^8.1.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/react-scripts/node_modules/@jest/reporters/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/@jest/reporters/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/@jest/reporters/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-scripts/node_modules/@jest/reporters/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/@jest/reporters/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/@jest/source-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", - "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9", - "source-map": "^0.6.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/@jest/test-result": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz", - "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==", - "dev": true, - "dependencies": { - "@jest/console": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/@jest/test-sequencer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz", - "integrity": "sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==", - "dev": true, - "dependencies": { - "@jest/test-result": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-runtime": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/@jest/transform": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", - "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.1.0", - "@jest/types": "^27.5.1", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-util": "^27.5.1", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/@jest/transform/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/@jest/transform/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/@jest/transform/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-scripts/node_modules/@jest/transform/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/@jest/transform/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/@jest/types/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/@jest/types/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/@jest/types/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-scripts/node_modules/@jest/types/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/@jest/types/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/@sinonjs/fake-timers": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", - "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/react-scripts/node_modules/@types/yargs": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", - "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/react-scripts/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/react-scripts/node_modules/babel-jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", - "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==", - "dev": true, - "dependencies": { - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^27.5.1", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/react-scripts/node_modules/babel-jest/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/babel-jest/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/babel-jest/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-scripts/node_modules/babel-jest/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/babel-jest/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/babel-plugin-jest-hoist": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz", - "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==", - "dev": true, - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.0.0", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/babel-preset-jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", - "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==", - "dev": true, - "dependencies": { - "babel-plugin-jest-hoist": "^27.5.1", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/react-scripts/node_modules/browserslist": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", - "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/react-scripts/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/react-scripts/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/react-scripts/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/react-scripts/node_modules/diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", - "dev": true, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/react-scripts/node_modules/electron-to-chromium": { - "version": "1.4.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", - "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", - "dev": true - }, - "node_modules/react-scripts/node_modules/emittery": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", - "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/expect": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", - "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/react-scripts/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/react-scripts/node_modules/jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", - "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", - "dev": true, - "dependencies": { - "@jest/core": "^27.5.1", - "import-local": "^3.0.2", - "jest-cli": "^27.5.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/react-scripts/node_modules/jest-changed-files": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz", - "integrity": "sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "execa": "^5.0.0", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-circus": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz", - "integrity": "sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==", - "dev": true, - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "expect": "^27.5.1", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-circus/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-circus/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-circus/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-circus/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-circus/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-cli": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", - "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==", - "dev": true, - "dependencies": { - "@jest/core": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "import-local": "^3.0.2", - "jest-config": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "prompts": "^2.0.1", - "yargs": "^16.2.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/react-scripts/node_modules/jest-cli/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-cli/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-cli/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-cli/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-cli/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-config": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", - "integrity": "sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.8.0", - "@jest/test-sequencer": "^27.5.1", - "@jest/types": "^27.5.1", - "babel-jest": "^27.5.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.1", - "graceful-fs": "^4.2.9", - "jest-circus": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-jasmine2": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "ts-node": { - "optional": true - } - } - }, - "node_modules/react-scripts/node_modules/jest-config/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-config/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-config/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-config/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-config/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-diff": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", - "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-diff/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-diff/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-diff/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-diff/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-diff/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-docblock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz", - "integrity": "sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==", - "dev": true, - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-each": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz", - "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-each/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-each/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-each/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-each/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-each/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-environment-node": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz", - "integrity": "sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==", - "dev": true, - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "dev": true, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-leak-detector": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", - "integrity": "sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==", - "dev": true, - "dependencies": { - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-message-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", - "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^27.5.1", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-message-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-message-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-message-util/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-message-util/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-message-util/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-mock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", - "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-resolve-dependencies": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz", - "integrity": "sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-snapshot": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-runner": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz", - "integrity": "sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==", - "dev": true, - "dependencies": { - "@jest/console": "^27.5.1", - "@jest/environment": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.8.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-leak-detector": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "source-map-support": "^0.5.6", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-runner/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-runner/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-runner/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-runner/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-runner/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-runtime": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", - "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==", - "dev": true, - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/globals": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "execa": "^5.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-runtime/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-runtime/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-runtime/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-runtime/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-runtime/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-snapshot": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", - "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.7.2", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.0.0", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__traverse": "^7.0.4", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "natural-compare": "^1.4.0", - "pretty-format": "^27.5.1", - "semver": "^7.3.2" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-snapshot/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-snapshot/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-snapshot/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-snapshot/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-snapshot/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-util/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-util/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-util/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-watcher": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz", - "integrity": "sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==", - "dev": true, - "dependencies": { - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "jest-util": "^27.5.1", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-watcher/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-watcher/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-scripts/node_modules/jest-watcher/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-scripts/node_modules/jest-watcher/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/jest-watcher/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/react-scripts/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/react-scripts/node_modules/node-releases": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", - "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", - "dev": true - }, - "node_modules/react-scripts/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/react-scripts/node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-scripts/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/react-scripts/node_modules/v8-to-istanbul": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", - "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0", - "source-map": "^0.7.3" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/react-scripts/node_modules/v8-to-istanbul/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/react-scripts/node_modules/write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/recursive-readdir": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", - "integrity": "sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==", - "dev": true, - "dependencies": { - "minimatch": "3.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/redux": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz", - "integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==", - "dependencies": { - "@babel/runtime": "^7.9.2" - } - }, - "node_modules/redux-mock-store": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.4.tgz", - "integrity": "sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==", - "dev": true, - "dependencies": { - "lodash.isplainobject": "^4.0.6" - } - }, - "node_modules/redux-thunk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", - "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==", - "peerDependencies": { - "redux": "^4" - } - }, - "node_modules/reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", - "dev": true - }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz", - "integrity": "sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw==", - "dev": true, - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" - }, - "node_modules/regenerator-transform": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.0.tgz", - "integrity": "sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, - "node_modules/regex-parser": { - "version": "2.2.11", - "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", - "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", - "dev": true - }, - "node_modules/regexp.prototype.flags": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", - "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/regexpu-core": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.0.1.tgz", - "integrity": "sha512-CriEZlrKK9VJw/xQGJpQM5rY88BtuL8DM+AEwvcThHilbxiTAy8vq4iJnd2tqq8wLmjbGZzP7ZcKFjbGkmEFrw==", - "dev": true, - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.0.1", - "regjsgen": "^0.6.0", - "regjsparser": "^0.8.2", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsgen": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.6.0.tgz", - "integrity": "sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA==", - "dev": true - }, - "node_modules/regjsparser": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.8.4.tgz", - "integrity": "sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA==", - "dev": true, - "dependencies": { - "jsesc": "~0.5.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - } - }, - "node_modules/relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/renderkid": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", - "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", - "dev": true, - "dependencies": { - "css-select": "^4.1.3", - "dom-converter": "^0.2.0", - "htmlparser2": "^6.1.0", - "lodash": "^4.17.21", - "strip-ansi": "^6.0.1" - } - }, - "node_modules/renderkid/node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/renderkid/node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/renderkid/node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/renderkid/node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/renderkid/node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/renderkid/node_modules/nth-check": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", - "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requireindex": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", - "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", - "dev": true, - "engines": { - "node": ">=0.10.5" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true - }, - "node_modules/reselect": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.5.tgz", - "integrity": "sha512-uVdlz8J7OO+ASpBYoz1Zypgx0KasCY20H+N8JD13oUMtPvSHQuscrHop4KbXrbsBcdB9Ds7lVK7eRkBIfO43vQ==" - }, - "node_modules/resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dependencies": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-url-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz", - "integrity": "sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA==", - "dev": true, - "dependencies": { - "adjust-sourcemap-loader": "^4.0.0", - "convert-source-map": "^1.7.0", - "loader-utils": "^2.0.0", - "postcss": "^7.0.35", - "source-map": "0.6.1" - }, - "engines": { - "node": ">=8.9" - }, - "peerDependencies": { - "rework": "1.0.1", - "rework-visit": "1.0.0" - }, - "peerDependenciesMeta": { - "rework": { - "optional": true - }, - "rework-visit": { - "optional": true - } - } - }, - "node_modules/resolve-url-loader/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/resolve-url-loader/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/resolve.exports": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz", - "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", - "dev": true - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "2.72.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.72.1.tgz", - "integrity": "sha512-NTc5UGy/NWFGpSqF1lFY8z9Adri6uhyMLI6LvPAXdBKoPRFhIIiBUpt+Qg2awixqO3xvzSijjhnb4+QEZwJmxA==", - "dev": true, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=10.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/rollup-plugin-terser": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", - "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "jest-worker": "^26.2.1", - "serialize-javascript": "^4.0.0", - "terser": "^5.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0" - } - }, - "node_modules/rollup-plugin-terser/node_modules/jest-worker": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", - "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", - "dev": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/rollup-plugin-terser/node_modules/serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/rxjs": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz", - "integrity": "sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w==", - "dev": true, - "dependencies": { - "tslib": "~2.1.0" - } - }, - "node_modules/rxjs/node_modules/tslib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", - "dev": true - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "node_modules/sanitize.css": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", - "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==", - "dev": true - }, - "node_modules/sass": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.44.0.tgz", - "integrity": "sha512-0hLREbHFXGQqls/K8X+koeP+ogFRPF4ZqetVB19b7Cst9Er8cOR0rc6RU7MaI4W1JmUShd1BPgPoeqmmgMMYFw==", - "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/sass-loader": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", - "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", - "dev": true, - "dependencies": { - "klona": "^2.0.4", - "neo-async": "^2.6.2" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - } - } - }, - "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "node_modules/saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "dev": true, - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/scheduler": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", - "integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", - "dev": true - }, - "node_modules/selfsigned": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.1.tgz", - "integrity": "sha512-LmME957M1zOsUhG+67rAjKfiWFox3SBxE/yymatMZsAx+oMrJ0YQ8AToOnyCm7xbeg2ep37IHLxdu0o2MavQOQ==", - "dev": true, - "dependencies": { - "node-forge": "^1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", - "dev": true, - "dependencies": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-index/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "dev": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true - }, - "node_modules/serve-index/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dev": true, - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true - }, - "node_modules/shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", - "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", - "dev": true - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/sockjs": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", - "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", - "dev": true, - "dependencies": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" - } - }, - "node_modules/source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "dev": true - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-loader": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.1.tgz", - "integrity": "sha512-Vp1UsfyPvgujKQzi4pyDiTOnE3E4H+yHvkVRN3c/9PJmQS4CQJExvcDvaX/D+RV+xQben9HJ56jMJS3CgUeWyA==", - "dev": true, - "dependencies": { - "abab": "^2.0.5", - "iconv-lite": "^0.6.3", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/source-map-loader/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-resolve": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", - "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", - "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", - "dev": true, - "dependencies": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "dev": true - }, - "node_modules/spawn-command": { - "version": "0.0.2-1", - "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", - "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=", - "dev": true - }, - "node_modules/spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "dev": true, - "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, - "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - } - }, - "node_modules/spdy-transport/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/spdy-transport/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/spdy/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/spdy/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "node_modules/stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "dev": true - }, - "node_modules/stack-utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", - "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/stackframe": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.1.tgz", - "integrity": "sha512-h88QkzREN/hy8eRdyNhhsO7RSJ5oyTqxxmmn0dzBIMUclZsjpfmrsg81vp8mjjAs2vAZ72nyWxRUwSwmh0e4xg==", - "dev": true - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-argv": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", - "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", - "dev": true, - "engines": { - "node": ">=0.6.19" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-natural-compare": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", - "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==", - "dev": true - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz", - "integrity": "sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.3.1", - "side-channel": "^1.0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.matchall/node_modules/es-abstract": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", - "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", - "is-string": "^1.0.7", - "is-weakref": "^1.0.1", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.matchall/node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.matchall/node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.matchall/node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.matchall/node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/stringify-object": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", - "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", - "dev": true, - "dependencies": { - "get-own-enumerable-property-symbols": "^3.0.0", - "is-obj": "^1.0.1", - "is-regexp": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", - "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/style-loader": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", - "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", - "dev": true, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/styled-components": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.1.tgz", - "integrity": "sha512-JThv2JRzyH0NOIURrk9iskdxMSAAtCfj/b2Sf1WJaCUsloQkblepy1jaCLX/bYE+mhYo3unmwVSI9I5d9ncSiQ==", - "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/traverse": "^7.4.5", - "@emotion/is-prop-valid": "^0.8.8", - "@emotion/stylis": "^0.8.4", - "@emotion/unitless": "^0.7.4", - "babel-plugin-styled-components": ">= 1.12.0", - "css-to-react-native": "^3.0.0", - "hoist-non-react-statics": "^3.0.0", - "shallowequal": "^1.1.0", - "supports-color": "^5.5.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/styled-components" - }, - "peerDependencies": { - "react": ">= 16.8.0", - "react-dom": ">= 16.8.0", - "react-is": ">= 16.8.0" - } - }, - "node_modules/styled-components/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "engines": { - "node": ">=4" - } - }, - "node_modules/styled-components/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/stylehacks": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.0.tgz", - "integrity": "sha512-SzLmvHQTrIWfSgljkQCw2++C9+Ne91d/6Sp92I8c5uHTcy/PgeHamwITIbBW9wnFTY/3ZfSXR9HIL6Ikqmcu6Q==", - "dev": true, - "dependencies": { - "browserslist": "^4.16.6", - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-hyperlinks": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", - "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svg-parser": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", - "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", - "dev": true - }, - "node_modules/svgo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", - "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", - "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", - "dev": true, - "dependencies": { - "chalk": "^2.4.1", - "coa": "^2.0.2", - "css-select": "^2.0.0", - "css-select-base-adapter": "^0.1.1", - "css-tree": "1.0.0-alpha.37", - "csso": "^4.0.2", - "js-yaml": "^3.13.1", - "mkdirp": "~0.5.1", - "object.values": "^1.1.0", - "sax": "~1.2.4", - "stable": "^0.1.8", - "unquote": "~1.1.1", - "util.promisify": "~1.0.0" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/svgo/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/svgo/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/svgo/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/svgo/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true - }, - "node_modules/tailwindcss": { - "version": "3.0.24", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.0.24.tgz", - "integrity": "sha512-H3uMmZNWzG6aqmg9q07ZIRNIawoiEcNFKDfL+YzOPuPsXuDXxJxB9icqzLgdzKNwjG3SAro2h9SYav8ewXNgig==", - "dev": true, - "dependencies": { - "arg": "^5.0.1", - "chokidar": "^3.5.3", - "color-name": "^1.1.4", - "detective": "^5.2.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.2.11", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "lilconfig": "^2.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.12", - "postcss-js": "^4.0.0", - "postcss-load-config": "^3.1.4", - "postcss-nested": "5.0.6", - "postcss-selector-parser": "^6.0.10", - "postcss-value-parser": "^4.2.0", - "quick-lru": "^5.1.1", - "resolve": "^1.22.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=12.13.0" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } - }, - "node_modules/tailwindcss/node_modules/arg": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.1.tgz", - "integrity": "sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA==", - "dev": true - }, - "node_modules/tailwindcss/node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/tailwindcss/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/tailwindcss/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/tailwindcss/node_modules/is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tailwindcss/node_modules/lilconfig": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz", - "integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/tailwindcss/node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/tailwindcss/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/tailwindcss/node_modules/resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.8.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/temp-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", - "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/tempy": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", - "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", - "dev": true, - "dependencies": { - "is-stream": "^2.0.0", - "temp-dir": "^2.0.0", - "type-fest": "^0.16.0", - "unique-string": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/type-fest": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", - "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "dependencies": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terser": { - "version": "5.13.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.13.1.tgz", - "integrity": "sha512-hn4WKOfwnwbYfe48NgrQjqNOH9jzLqRcIfbYytOXCOv46LBfWr9bDS17MQqOi+BWGD0sJK3Sj5NC/gJjiojaoA==", - "dev": true, - "dependencies": { - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map": "~0.8.0-beta.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz", - "integrity": "sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g==", - "dev": true, - "dependencies": { - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1", - "terser": "^5.7.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "node_modules/terser/node_modules/source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "dev": true, - "dependencies": { - "whatwg-url": "^7.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/terser/node_modules/whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" - }, - "node_modules/throat": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", - "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", - "dev": true - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "node_modules/thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true - }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/toposort": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", - "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=" - }, - "node_modules/tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", - "dev": true, - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.1.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/tryer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", - "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", - "dev": true - }, - "node_modules/ts-jest": { - "version": "28.0.3", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.3.tgz", - "integrity": "sha512-HzgbEDQ2KgVtDmpXToqAcKTyGHdHsG23i/iUjfxji92G5eT09S1m9UHZd7csF0Bfgh9txM4JzwHnv7r1waFPlw==", - "dev": true, - "dependencies": { - "bs-logger": "0.x", - "fast-json-stable-stringify": "2.x", - "jest-util": "^28.0.0", - "json5": "^2.2.1", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "7.x", - "yargs-parser": "^20.x" - }, - "bin": { - "ts-jest": "cli.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@types/jest": "^27.0.0", - "babel-jest": "^28.0.0", - "jest": "^28.0.0", - "typescript": ">=4.3" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@types/jest": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - } - } - }, - "node_modules/ts-jest/node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/ts-jest/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-node": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.0.tgz", - "integrity": "sha512-/fNd5Qh+zTt8Vt1KbYZjRHCE9sI5i7nqfD/dzBBRDeVXZXS6kToW6R7tTU6Nd4XavFs0mAVCg29Q//ML7WsZYA==", - "dev": true, - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/ts-node/node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/tsconfig-paths": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", - "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.1", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tslib": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", - "dev": true - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, - "node_modules/typescript": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", - "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz", - "integrity": "sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz", - "integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "dev": true, - "dependencies": { - "crypto-random-string": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/unquote": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", - "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", - "dev": true - }, - "node_modules/upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true, - "engines": { - "node": ">=4", - "yarn": "*" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-debounce": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-8.0.1.tgz", - "integrity": "sha512-6tGAFJKJ0qCalecaV7/gm/M6n238nmitNppvR89ff1yfwSFjwFKR7IQZzIZf1KZRQhqNireBzytzU6jgb29oVg==", - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "node_modules/util.promisify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", - "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.2", - "has-symbols": "^1.0.1", - "object.getownpropertydescriptors": "^2.1.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", - "dev": true - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==" - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true - }, - "node_modules/v8-to-istanbul": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.0.tgz", - "integrity": "sha512-HcvgY/xaRm7isYmyx+lFKA4uQmfUbN0J4M0nNItvzTvH/iQ9kW5j/t4YSR+Ge323/lrgDAWJoF46tzGQHwBHFw==", - "dev": true, - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.7", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "dev": true, - "dependencies": { - "browser-process-hrtime": "^1.0.0" - } - }, - "node_modules/w3c-xmlserializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", - "dev": true, - "dependencies": { - "xml-name-validator": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", - "dev": true, - "dependencies": { - "makeerror": "1.0.x" - } - }, - "node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, - "node_modules/watchpack": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz", - "integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==", - "dev": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "dependencies": { - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", - "dev": true, - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - }, - "node_modules/webpack": { - "version": "5.72.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.72.1.tgz", - "integrity": "sha512-dXG5zXCLspQR4krZVR6QgajnZOjW2K/djHvdcRaDQvsjV9z9vaW6+ja5dZOYbqBBjF6kGXka/2ZyxNdc+8Jung==", - "dev": true, - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.4.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.9.3", - "es-module-lexer": "^0.9.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.3.1", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-middleware": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.1.tgz", - "integrity": "sha512-81EujCKkyles2wphtdrnPg/QqegC/AtqNH//mQkBYSMqwFVCQrxM6ktB2O/SPlZy7LqeEfTbV3cZARGQz6umhg==", - "dev": true, - "dependencies": { - "colorette": "^2.0.10", - "memfs": "^3.4.1", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/webpack-dev-middleware/node_modules/colorette": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", - "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", - "dev": true - }, - "node_modules/webpack-dev-middleware/node_modules/schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/webpack-dev-server": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.0.tgz", - "integrity": "sha512-+Nlb39iQSOSsFv0lWUuUTim3jDQO8nhK3E68f//J2r5rIcp4lULHXz2oZ0UVdEeWXEh5lSzYUlzarZhDAeAVQw==", - "dev": true, - "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.1", - "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^1.6.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", - "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.0.1", - "serve-index": "^1.9.1", - "sockjs": "^0.3.21", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", - "ws": "^8.4.2" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" - }, - "engines": { - "node": ">= 12.13.0" - }, - "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-server/node_modules/@types/ws": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", - "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/webpack-dev-server/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/webpack-dev-server/node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/webpack-dev-server/node_modules/colorette": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", - "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", - "dev": true - }, - "node_modules/webpack-dev-server/node_modules/schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.6.0.tgz", - "integrity": "sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/webpack-manifest-plugin": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz", - "integrity": "sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow==", - "dev": true, - "dependencies": { - "tapable": "^2.0.0", - "webpack-sources": "^2.2.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "peerDependencies": { - "webpack": "^4.44.2 || ^5.47.0" - } - }, - "node_modules/webpack-manifest-plugin/node_modules/webpack-sources": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", - "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", - "dev": true, - "dependencies": { - "source-list-map": "^2.0.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "dev": true, - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dev": true, - "dependencies": { - "iconv-lite": "0.4.24" - } - }, - "node_modules/whatwg-fetch": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", - "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", - "dev": true - }, - "node_modules/whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true - }, - "node_modules/whatwg-url": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", - "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/workbox-background-sync": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.5.3.tgz", - "integrity": "sha512-0DD/V05FAcek6tWv9XYj2w5T/plxhDSpclIcAGjA/b7t/6PdaRkQ7ZgtAX6Q/L7kV7wZ8uYRJUoH11VjNipMZw==", - "dev": true, - "dependencies": { - "idb": "^6.1.4", - "workbox-core": "6.5.3" - } - }, - "node_modules/workbox-broadcast-update": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.5.3.tgz", - "integrity": "sha512-4AwCIA5DiDrYhlN+Miv/fp5T3/whNmSL+KqhTwRBTZIL6pvTgE4lVuRzAt1JltmqyMcQ3SEfCdfxczuI4kwFQg==", - "dev": true, - "dependencies": { - "workbox-core": "6.5.3" - } - }, - "node_modules/workbox-build": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.5.3.tgz", - "integrity": "sha512-8JNHHS7u13nhwIYCDea9MNXBNPHXCs5KDZPKI/ZNTr3f4sMGoD7hgFGecbyjX1gw4z6e9bMpMsOEJNyH5htA/w==", - "dev": true, - "dependencies": { - "@apideck/better-ajv-errors": "^0.3.1", - "@babel/core": "^7.11.1", - "@babel/preset-env": "^7.11.0", - "@babel/runtime": "^7.11.2", - "@rollup/plugin-babel": "^5.2.0", - "@rollup/plugin-node-resolve": "^11.2.1", - "@rollup/plugin-replace": "^2.4.1", - "@surma/rollup-plugin-off-main-thread": "^2.2.3", - "ajv": "^8.6.0", - "common-tags": "^1.8.0", - "fast-json-stable-stringify": "^2.1.0", - "fs-extra": "^9.0.1", - "glob": "^7.1.6", - "lodash": "^4.17.20", - "pretty-bytes": "^5.3.0", - "rollup": "^2.43.1", - "rollup-plugin-terser": "^7.0.0", - "source-map": "^0.8.0-beta.0", - "stringify-object": "^3.3.0", - "strip-comments": "^2.0.1", - "tempy": "^0.6.0", - "upath": "^1.2.0", - "workbox-background-sync": "6.5.3", - "workbox-broadcast-update": "6.5.3", - "workbox-cacheable-response": "6.5.3", - "workbox-core": "6.5.3", - "workbox-expiration": "6.5.3", - "workbox-google-analytics": "6.5.3", - "workbox-navigation-preload": "6.5.3", - "workbox-precaching": "6.5.3", - "workbox-range-requests": "6.5.3", - "workbox-recipes": "6.5.3", - "workbox-routing": "6.5.3", - "workbox-strategies": "6.5.3", - "workbox-streams": "6.5.3", - "workbox-sw": "6.5.3", - "workbox-window": "6.5.3" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/workbox-build/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/workbox-build/node_modules/source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "dev": true, - "dependencies": { - "whatwg-url": "^7.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/workbox-build/node_modules/whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "node_modules/workbox-cacheable-response": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.5.3.tgz", - "integrity": "sha512-6JE/Zm05hNasHzzAGKDkqqgYtZZL2H06ic2GxuRLStA4S/rHUfm2mnLFFXuHAaGR1XuuYyVCEey1M6H3PdZ7SQ==", - "dev": true, - "dependencies": { - "workbox-core": "6.5.3" - } - }, - "node_modules/workbox-core": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.5.3.tgz", - "integrity": "sha512-Bb9ey5n/M9x+l3fBTlLpHt9ASTzgSGj6vxni7pY72ilB/Pb3XtN+cZ9yueboVhD5+9cNQrC9n/E1fSrqWsUz7Q==", - "dev": true - }, - "node_modules/workbox-expiration": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.5.3.tgz", - "integrity": "sha512-jzYopYR1zD04ZMdlbn/R2Ik6ixiXbi15c9iX5H8CTi6RPDz7uhvMLZPKEndZTpfgmUk8mdmT9Vx/AhbuCl5Sqw==", - "dev": true, - "dependencies": { - "idb": "^6.1.4", - "workbox-core": "6.5.3" - } - }, - "node_modules/workbox-google-analytics": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.5.3.tgz", - "integrity": "sha512-3GLCHotz5umoRSb4aNQeTbILETcrTVEozSfLhHSBaegHs1PnqCmN0zbIy2TjTpph2AGXiNwDrWGF0AN+UgDNTw==", - "dev": true, - "dependencies": { - "workbox-background-sync": "6.5.3", - "workbox-core": "6.5.3", - "workbox-routing": "6.5.3", - "workbox-strategies": "6.5.3" - } - }, - "node_modules/workbox-navigation-preload": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.5.3.tgz", - "integrity": "sha512-bK1gDFTc5iu6lH3UQ07QVo+0ovErhRNGvJJO/1ngknT0UQ702nmOUhoN9qE5mhuQSrnK+cqu7O7xeaJ+Rd9Tmg==", - "dev": true, - "dependencies": { - "workbox-core": "6.5.3" - } - }, - "node_modules/workbox-precaching": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.5.3.tgz", - "integrity": "sha512-sjNfgNLSsRX5zcc63H/ar/hCf+T19fRtTqvWh795gdpghWb5xsfEkecXEvZ8biEi1QD7X/ljtHphdaPvXDygMQ==", - "dev": true, - "dependencies": { - "workbox-core": "6.5.3", - "workbox-routing": "6.5.3", - "workbox-strategies": "6.5.3" - } - }, - "node_modules/workbox-range-requests": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.5.3.tgz", - "integrity": "sha512-pGCP80Bpn/0Q0MQsfETSfmtXsQcu3M2QCJwSFuJ6cDp8s2XmbUXkzbuQhCUzKR86ZH2Vex/VUjb2UaZBGamijA==", - "dev": true, - "dependencies": { - "workbox-core": "6.5.3" - } - }, - "node_modules/workbox-recipes": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.5.3.tgz", - "integrity": "sha512-IcgiKYmbGiDvvf3PMSEtmwqxwfQ5zwI7OZPio3GWu4PfehA8jI8JHI3KZj+PCfRiUPZhjQHJ3v1HbNs+SiSkig==", - "dev": true, - "dependencies": { - "workbox-cacheable-response": "6.5.3", - "workbox-core": "6.5.3", - "workbox-expiration": "6.5.3", - "workbox-precaching": "6.5.3", - "workbox-routing": "6.5.3", - "workbox-strategies": "6.5.3" - } - }, - "node_modules/workbox-routing": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.5.3.tgz", - "integrity": "sha512-DFjxcuRAJjjt4T34RbMm3MCn+xnd36UT/2RfPRfa8VWJGItGJIn7tG+GwVTdHmvE54i/QmVTJepyAGWtoLPTmg==", - "dev": true, - "dependencies": { - "workbox-core": "6.5.3" - } - }, - "node_modules/workbox-strategies": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.5.3.tgz", - "integrity": "sha512-MgmGRrDVXs7rtSCcetZgkSZyMpRGw8HqL2aguszOc3nUmzGZsT238z/NN9ZouCxSzDu3PQ3ZSKmovAacaIhu1w==", - "dev": true, - "dependencies": { - "workbox-core": "6.5.3" - } - }, - "node_modules/workbox-streams": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.5.3.tgz", - "integrity": "sha512-vN4Qi8o+b7zj1FDVNZ+PlmAcy1sBoV7SC956uhqYvZ9Sg1fViSbOpydULOssVJ4tOyKRifH/eoi6h99d+sJ33w==", - "dev": true, - "dependencies": { - "workbox-core": "6.5.3", - "workbox-routing": "6.5.3" - } - }, - "node_modules/workbox-sw": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.5.3.tgz", - "integrity": "sha512-BQBzm092w+NqdIEF2yhl32dERt9j9MDGUTa2Eaa+o3YKL4Qqw55W9yQC6f44FdAHdAJrJvp0t+HVrfh8AiGj8A==", - "dev": true - }, - "node_modules/workbox-webpack-plugin": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.5.3.tgz", - "integrity": "sha512-Es8Xr02Gi6Kc3zaUwR691ZLy61hz3vhhs5GztcklQ7kl5k2qAusPh0s6LF3wEtlpfs9ZDErnmy5SErwoll7jBA==", - "dev": true, - "dependencies": { - "fast-json-stable-stringify": "^2.1.0", - "pretty-bytes": "^5.4.1", - "upath": "^1.2.0", - "webpack-sources": "^1.4.3", - "workbox-build": "6.5.3" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "webpack": "^4.4.0 || ^5.9.0" - } - }, - "node_modules/workbox-webpack-plugin/node_modules/webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "dependencies": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - }, - "node_modules/workbox-window": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.5.3.tgz", - "integrity": "sha512-GnJbx1kcKXDtoJBVZs/P7ddP0Yt52NNy4nocjBpYPiRhMqTpJCNrSL+fGHZ/i/oP6p/vhE8II0sA6AZGKGnssw==", - "dev": true, - "dependencies": { - "@types/trusted-types": "^2.0.2", - "workbox-core": "6.5.3" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "node_modules/write-file-atomic": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.1.tgz", - "integrity": "sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==", - "dev": true, - "peer": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16" - } - }, - "node_modules/ws": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", - "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", - "dev": true, - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", - "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", - "dev": true - }, - "node_modules/xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.7", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", - "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yup": { - "version": "0.32.11", - "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz", - "integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==", - "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/lodash": "^4.14.175", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "nanoclone": "^0.2.1", - "property-expr": "^2.0.4", - "toposort": "^2.0.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yup/node_modules/@babel/runtime": { - "version": "7.16.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz", - "integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==", - "dependencies": { - "regenerator-runtime": "^0.13.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/yup/node_modules/@types/lodash": { - "version": "4.14.177", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.177.tgz", - "integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==" - } - }, - "dependencies": { - "@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", - "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@apideck/better-ajv-errors": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.3.tgz", - "integrity": "sha512-9o+HO2MbJhJHjDYZaDxJmSDckvDpiuItEsrIShV0DXeCshXWRHhqYyU/PKHMkuClOmFnZhRd6wzv4vpDu/dRKg==", - "dev": true, - "requires": { - "json-schema": "^0.4.0", - "jsonpointer": "^5.0.0", - "leven": "^3.1.0" - } - }, - "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", - "requires": { - "@babel/highlight": "^7.14.5" - } - }, - "@babel/compat-data": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.5.tgz", - "integrity": "sha512-kixrYn4JwfAVPa0f2yfzc2AWti6WRRyO3XjWW5PJAvtE11qhSayrrcrEnee05KAtNaPC+EwehE8Qt1UedEVB8w==" - }, - "@babel/core": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.6.tgz", - "integrity": "sha512-gJnOEWSqTk96qG5BoIrl5bVtc23DCycmIePPYnamY9RboYdI4nFy5vAQMSl81O5K/W0sLDWfGysnOECC+KUUCA==", - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.14.5", - "@babel/helper-compilation-targets": "^7.14.5", - "@babel/helper-module-transforms": "^7.14.5", - "@babel/helpers": "^7.14.6", - "@babel/parser": "^7.14.6", - "@babel/template": "^7.14.5", - "@babel/traverse": "^7.14.5", - "@babel/types": "^7.14.5", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.1.2", - "semver": "^6.3.0", - "source-map": "^0.5.0" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } - } - }, - "@babel/eslint-parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.17.0.tgz", - "integrity": "sha512-PUEJ7ZBXbRkbq3qqM/jZ2nIuakUBqCYc7Qf52Lj7dlZ6zERnqisdHioL0l4wwQZnmskMeasqUNzLBFKs3nylXA==", - "dev": true, - "requires": { - "eslint-scope": "^5.1.1", - "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz", - "integrity": "sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA==", - "requires": { - "@babel/types": "^7.14.5", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz", - "integrity": "sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA==", - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.7.tgz", - "integrity": "sha512-C6FdbRaxYjwVu/geKW4ZeQ0Q31AftgRcdSnZ5/jsH6BzCJbtvXvhpfkbkThYSuutZA7nCXpPR6AD9zd1dprMkA==", - "dev": true, - "requires": { - "@babel/helper-explode-assignable-expression": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-compilation-targets": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.5.tgz", - "integrity": "sha512-v+QtZqXEiOnpO6EYvlImB6zCD2Lel06RzOPzmkz/D/XgQiUu3C/Jb1LOqSt/AIA34TYi/Q+KlT8vTQrgdxkbLw==", - "requires": { - "@babel/compat-data": "^7.14.5", - "@babel/helper-validator-option": "^7.14.5", - "browserslist": "^4.16.6", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "@babel/helper-create-class-features-plugin": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.9.tgz", - "integrity": "sha512-kUjip3gruz6AJKOq5i3nC6CoCEEF/oHH3cp6tOZhB+IyyyPyW0g1Gfsxn3mkk6S08pIA2y8GQh609v9G/5sHVQ==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-member-expression-to-functions": "^7.17.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/helper-replace-supers": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "requires": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", - "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz", - "integrity": "sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz", - "integrity": "sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-replace-supers": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz", - "integrity": "sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-member-expression-to-functions": "^7.16.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/helper-create-regexp-features-plugin": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz", - "integrity": "sha512-awO2So99wG6KnlE+TPs6rn83gCz5WlEePJDTnLEqbchMVrBeAujURVphRdigsk094VhvZehFoNOihSlcBjwsXA==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "regexpu-core": "^5.0.1" - }, - "dependencies": { - "@babel/helper-annotate-as-pure": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", - "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-define-polyfill-provider": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz", - "integrity": "sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA==", - "dev": true, - "requires": { - "@babel/helper-compilation-targets": "^7.13.0", - "@babel/helper-module-imports": "^7.12.13", - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/traverse": "^7.13.0", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/helper-environment-visitor": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", - "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-explode-assignable-expression": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.7.tgz", - "integrity": "sha512-KyUenhWMC8VrxzkGP0Jizjo4/Zx+1nNZhgocs+gLzyZyB8SHidhoq9KK/8Ato4anhwsivfkBLftky7gvzbZMtQ==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-function-name": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz", - "integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==", - "requires": { - "@babel/helper-get-function-arity": "^7.14.5", - "@babel/template": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz", - "integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==", - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz", - "integrity": "sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==", - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.5.tgz", - "integrity": "sha512-UxUeEYPrqH1Q/k0yRku1JE7dyfyehNwT6SVkMHvYvPDv4+uu627VXBckVj891BO8ruKBkiDoGnZf4qPDD8abDQ==", - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-module-imports": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz", - "integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==", - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-module-transforms": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.5.tgz", - "integrity": "sha512-iXpX4KW8LVODuAieD7MzhNjmM6dzYY5tfRqT+R9HDXWl0jPn/djKmA+G9s/2C2T9zggw5tK1QNqZ70USfedOwA==", - "requires": { - "@babel/helper-module-imports": "^7.14.5", - "@babel/helper-replace-supers": "^7.14.5", - "@babel/helper-simple-access": "^7.14.5", - "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/helper-validator-identifier": "^7.14.5", - "@babel/template": "^7.14.5", - "@babel/traverse": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz", - "integrity": "sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==", - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", - "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", - "dev": true - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz", - "integrity": "sha512-fm0gH7Flb8H51LqJHy3HJ3wnE1+qtYR2A99K06ahwrawLdOFsCEWjZOrYricXJHoPSudNKxrMBUPEIPxiIIvBw==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-wrap-function": "^7.16.8", - "@babel/types": "^7.16.8" - }, - "dependencies": { - "@babel/helper-annotate-as-pure": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", - "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-replace-supers": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.5.tgz", - "integrity": "sha512-3i1Qe9/8x/hCHINujn+iuHy+mMRLoc77b2nI9TB0zjH1hvn9qGlXjWlggdwUcju36PkPCy/lpM7LLUdcTyH4Ow==", - "requires": { - "@babel/helper-member-expression-to-functions": "^7.14.5", - "@babel/helper-optimise-call-expression": "^7.14.5", - "@babel/traverse": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-simple-access": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.14.5.tgz", - "integrity": "sha512-nfBN9xvmCt6nrMZjfhkl7i0oTV3yxR4/FztsbOASyTvVcoYd0TRHh7eMLdlEcCqobydC0LAF3LtC92Iwxo0wyw==", - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz", - "integrity": "sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.0" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz", - "integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==", - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", - "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==" - }, - "@babel/helper-validator-option": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", - "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==" - }, - "@babel/helper-wrap-function": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.16.8.tgz", - "integrity": "sha512-8RpyRVIAW1RcDDGTA+GpPAwV22wXCfKOoM9bet6TLkGIFTkRQSkH1nMQ5Yet4MpoXe1ZwHPVtNasc2w0uZMqnw==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.8", - "@babel/types": "^7.16.8" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "requires": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - } - }, - "@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/helpers": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.6.tgz", - "integrity": "sha512-yesp1ENQBiLI+iYHSJdoZKUtRpfTlL1grDIX9NRlAVppljLw/4tTyYupIB7uIYmC3stW/imAv8EqaKaS/ibmeA==", - "requires": { - "@babel/template": "^7.14.5", - "@babel/traverse": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/parser": { - "version": "7.18.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz", - "integrity": "sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==" - }, - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz", - "integrity": "sha512-anv/DObl7waiGEnC24O9zqL0pSuI9hljihqiDuFHC8d7/bjr/4RLGPWuc8rYOff/QPzbEPSkzG8wGG9aDuhHRg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.16.7.tgz", - "integrity": "sha512-di8vUHRdf+4aJ7ltXhaDbPoszdkh59AQtJM5soLsuHpQJdFQZOA4uGj0V2u/CZ8bJ/u8ULDL5yq6FO/bCXnKHw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0", - "@babel/plugin-proposal-optional-chaining": "^7.16.7" - } - }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.8.tgz", - "integrity": "sha512-71YHIvMuiuqWJQkebWJtdhQTfd4Q4mF76q2IX37uZPkG9+olBxsX+rH1vkhFto4UeJZ9dPY2s+mDvhDm1u2BGQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-remap-async-to-generator": "^7.16.8", - "@babel/plugin-syntax-async-generators": "^7.8.4" - } - }, - "@babel/plugin-proposal-class-properties": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.7.tgz", - "integrity": "sha512-IobU0Xme31ewjYOShSIqd/ZGM/r/cuOz2z0MDbNrhF5FW+ZVgi0f2lyeoj9KFPDOAqsYxmLWZte1WOwlvY9aww==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-proposal-class-static-block": { - "version": "7.17.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.17.6.tgz", - "integrity": "sha512-X/tididvL2zbs7jZCeeRJ8167U/+Ac135AM6jCAx6gYXDUviZV5Ku9UDvWS2NCuWlFjIRXklYhwo6HhAC7ETnA==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.17.6", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - } - }, - "@babel/plugin-proposal-decorators": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.17.9.tgz", - "integrity": "sha512-EfH2LZ/vPa2wuPwJ26j+kYRkaubf89UlwxKXtxqEm57HrgSEYDB8t4swFP+p8LcI9yiP9ZRJJjo/58hS6BnaDA==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.17.9", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-replace-supers": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/plugin-syntax-decorators": "^7.17.0", - "charcodes": "^0.2.0" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "requires": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - } - }, - "@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz", - "integrity": "sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz", - "integrity": "sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-replace-supers": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz", - "integrity": "sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-member-expression-to-functions": "^7.16.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/plugin-proposal-dynamic-import": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz", - "integrity": "sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - } - }, - "@babel/plugin-proposal-export-namespace-from": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.7.tgz", - "integrity": "sha512-ZxdtqDXLRGBL64ocZcs7ovt71L3jhC1RGSyR996svrCi3PYqHNkb3SwPJCs8RIzD86s+WPpt2S73+EHCGO+NUA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - } - }, - "@babel/plugin-proposal-json-strings": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.16.7.tgz", - "integrity": "sha512-lNZ3EEggsGY78JavgbHsK9u5P3pQaW7k4axlgFLYkMd7UBsiNahCITShLjNQschPyjtO6dADrL24757IdhBrsQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-json-strings": "^7.8.3" - } - }, - "@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.7.tgz", - "integrity": "sha512-K3XzyZJGQCr00+EtYtrDjmwX7o7PLK6U9bi1nCwkQioRFVUv6dJoxbQjtWVtP+bCPy82bONBKG8NPyQ4+i6yjg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - } - }, - "@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.7.tgz", - "integrity": "sha512-aUOrYU3EVtjf62jQrCj63pYZ7k6vns2h/DQvHPWGmsJRYzWXZ6/AsfgpiRy6XiuIDADhJzP2Q9MwSMKauBQ+UQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - } - }, - "@babel/plugin-proposal-numeric-separator": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.7.tgz", - "integrity": "sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - } - }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.17.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.17.3.tgz", - "integrity": "sha512-yuL5iQA/TbZn+RGAfxQXfi7CNLmKi1f8zInn4IgobuCWcAb7i+zj4TYzQ9l8cEzVyJ89PDGuqxK1xZpUDISesw==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.17.0", - "@babel/helper-compilation-targets": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.16.7" - }, - "dependencies": { - "@babel/compat-data": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz", - "integrity": "sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw==", - "dev": true - }, - "@babel/helper-compilation-targets": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz", - "integrity": "sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.17.10", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.20.2", - "semver": "^6.3.0" - } - }, - "@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true - }, - "browserslist": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", - "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" - } - }, - "electron-to-chromium": { - "version": "1.4.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", - "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", - "dev": true - }, - "node-releases": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", - "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.16.7.tgz", - "integrity": "sha512-eMOH/L4OvWSZAE1VkHbr1vckLG1WUcHGJSLqqQwl2GaUqG6QjddvrOaTUMNYiv77H5IKPMZ9U9P7EaHwvAShfA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - } - }, - "@babel/plugin-proposal-optional-chaining": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.7.tgz", - "integrity": "sha512-eC3xy+ZrUcBtP7x+sq62Q/HYd674pPTb/77XZMb5wbDPGWIdUbSr4Agr052+zaUPSb+gGRnjxXfKFvx5iMJ+DA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - } - }, - "@babel/plugin-proposal-private-methods": { - "version": "7.16.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.11.tgz", - "integrity": "sha512-F/2uAkPlXDr8+BHpZvo19w3hLFKge+k75XUprE6jaqKxjGkSYcK+4c+bup5PdW/7W/Rpjwql7FTVEDW+fRAQsw==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.16.10", - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-proposal-private-property-in-object": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.7.tgz", - "integrity": "sha512-rMQkjcOFbm+ufe3bTZLyOfsOUOxyvLXZJCTARhJr+8UMSoZmqTe1K1BgkFcrW37rAchWg57yI69ORxiWvUINuQ==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-create-class-features-plugin": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, - "dependencies": { - "@babel/helper-annotate-as-pure": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", - "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.16.7.tgz", - "integrity": "sha512-QRK0YI/40VLhNVGIjRNAAQkEHws0cswSdFFjpFyt943YmJIU1da9uW63Iu6NFV6CxTZW5eTDCrwZUstBWgp/Rg==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-decorators": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.17.0.tgz", - "integrity": "sha512-qWe85yCXsvDEluNP0OyeQjH63DlhAR3W7K9BxxU1MvbDb48tgBG+Ao6IJJ6smPDrrVzSQZrbF6donpkFBMcs3A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-syntax-flow": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.16.7.tgz", - "integrity": "sha512-UDo3YGQO0jH6ytzVwgSLv9i/CzMcUjbKenL67dTrAZPPv6GFAtDhe6jqnvmoKzC/7htNTohhos+onPtDMqJwaQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-jsx": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz", - "integrity": "sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-typescript": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.17.10.tgz", - "integrity": "sha512-xJefea1DWXW09pW4Tm9bjwVlPDyYA2it3fWlmEjpYz6alPvTUjL0EOzNzI/FEOyI3r4/J7uVH5UqKgl1TQ5hqQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz", - "integrity": "sha512-9ffkFFMbvzTvv+7dTp/66xvZAWASuPD5Tl9LK3Z9vhOmANo6j94rik+5YMBt4CwHVMWLWpMsriIc2zsa3WW3xQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.8.tgz", - "integrity": "sha512-MtmUmTJQHCnyJVrScNzNlofQJ3dLFuobYn3mwOTKHnSCMtbNsqvF71GQmJfFjdrXSsAA7iysFmYWw4bXZ20hOg==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-remap-async-to-generator": "^7.16.8" - }, - "dependencies": { - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.7.tgz", - "integrity": "sha512-JUuzlzmF40Z9cXyytcbZEZKckgrQzChbQJw/5PuEHYeqzCsvebDx0K0jWnIIVcmmDOAVctCgnYs0pMcrYj2zJg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.7.tgz", - "integrity": "sha512-ObZev2nxVAYA4bhyusELdo9hb3H+A56bxH3FZMbEImZFiEDYVHXQSJ1hQKFlDnlt8G9bBrCZ5ZpURZUrV4G5qQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz", - "integrity": "sha512-WY7og38SFAGYRe64BrjKf8OrE6ulEHtr5jEYaZMwox9KebgqPi67Zqz8K53EKk1fFEJgm96r32rkKZ3qA2nCWQ==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-replace-supers": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "globals": "^11.1.0" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "requires": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", - "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz", - "integrity": "sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz", - "integrity": "sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-replace-supers": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz", - "integrity": "sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-member-expression-to-functions": "^7.16.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.7.tgz", - "integrity": "sha512-gN72G9bcmenVILj//sv1zLNaPyYcOzUho2lIJBMh/iakJ9ygCo/hEF9cpGb61SCMEDxbbyBoVQxrt+bWKu5KGw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.17.7.tgz", - "integrity": "sha512-XVh0r5yq9sLR4vZ6eVZe8FKfIcSgaTBxVBRSYokRj2qksf6QerYnTxz9/GTuKTH/n/HwLP7t6gtlybHetJ/6hQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.16.7.tgz", - "integrity": "sha512-Lyttaao2SjZF6Pf4vk1dVKv8YypMpomAbygW+mU5cYP3S5cWTfCJjG8xV6CFdzGFlfWK81IjL9viiTvpb6G7gQ==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.16.7.tgz", - "integrity": "sha512-03DvpbRfvWIXyK0/6QiR1KMTWeT6OcQ7tbhjrXyFS02kjuX/mu5Bvnh5SDSWHxyawit2g5aWhKwI86EE7GUnTw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.7.tgz", - "integrity": "sha512-8UYLSlyLgRixQvlYH3J2ekXFHDFLQutdy7FfFAMm3CPZ6q9wHCwnUyiXpQCe3gVVnQlHc5nsuiEVziteRNTXEA==", - "dev": true, - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-flow-strip-types": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.16.7.tgz", - "integrity": "sha512-mzmCq3cNsDpZZu9FADYYyfZJIOrSONmHcop2XEKPdBNMa4PDC4eEvcOvzZaCNcjKu72v0XQlA5y1g58aLRXdYg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-flow": "^7.16.7" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.7.tgz", - "integrity": "sha512-/QZm9W92Ptpw7sjI9Nx1mbcsWz33+l8kuMIQnDwgQBG5s3fAfQvkRjQ7NqXhtNcKOnPkdICmUHyCaWW06HCsqg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.7.tgz", - "integrity": "sha512-SU/C68YVwTRxqWj5kgsbKINakGag0KTgq9f2iZEXdStoAbOzLHEBRYzImmA6yFo8YZhJVflvXmIHUO7GWHmxxA==", - "dev": true, - "requires": { - "@babel/helper-compilation-targets": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/compat-data": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz", - "integrity": "sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw==", - "dev": true - }, - "@babel/helper-compilation-targets": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz", - "integrity": "sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.17.10", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.20.2", - "semver": "^6.3.0" - } - }, - "@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true - }, - "@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "browserslist": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", - "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "electron-to-chromium": { - "version": "1.4.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", - "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "node-releases": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", - "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/plugin-transform-literals": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.16.7.tgz", - "integrity": "sha512-6tH8RTpTWI0s2sV6uq3e/C9wPo4PTqqZps4uF0kzQ9/xPLFQtipynvmT1g/dOfEJ+0EQsHhkQ/zyRId8J2b8zQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-member-expression-literals": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.7.tgz", - "integrity": "sha512-mBruRMbktKQwbxaJof32LT9KLy2f3gH+27a5XSuXo6h7R3vqltl0PgZ80C8ZMKw98Bf8bqt6BEVi3svOh2PzMw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.16.7.tgz", - "integrity": "sha512-KaaEtgBL7FKYwjJ/teH63oAmE3lP34N3kshz8mm4VMAw7U3PxjVwwUmxEFksbgsNUaO3wId9R2AVQYSEGRa2+g==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "requires": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - } - }, - "@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-transforms": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", - "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-simple-access": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", - "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.17.9.tgz", - "integrity": "sha512-2TBFd/r2I6VlYn0YRTz2JdazS+FoUuQ2rIFHoAxtyP/0G3D82SBLaRq9rnUkpqlLg03Byfl/+M32mpxjO6KaPw==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "requires": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - } - }, - "@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-transforms": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", - "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-simple-access": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", - "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.17.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.17.8.tgz", - "integrity": "sha512-39reIkMTUVagzgA5x88zDYXPCMT6lcaRKs1+S9K6NKBPErbgO/w/kP8GlNQTC87b412ZTlmNgr3k2JrWgHH+Bw==", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-module-transforms": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "requires": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - } - }, - "@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-transforms": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", - "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-simple-access": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", - "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.16.7.tgz", - "integrity": "sha512-EMh7uolsC8O4xhudF2F6wedbSHm1HHZ0C6aJ7K67zcDNidMzVcxWdGr+htW9n21klm+bOn+Rx4CBsAntZd3rEQ==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "requires": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - } - }, - "@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-transforms": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", - "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-simple-access": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", - "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.17.10.tgz", - "integrity": "sha512-v54O6yLaJySCs6mGzaVOUw9T967GnH38T6CQSAtnzdNPwu84l2qAjssKzo/WSO8Yi7NF+7ekm5cVbF/5qiIgNA==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.17.0" - } - }, - "@babel/plugin-transform-new-target": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.16.7.tgz", - "integrity": "sha512-xiLDzWNMfKoGOpc6t3U+etCE2yRnn3SM09BXqWPIZOBpL2gvVrBWUKnsJx0K/ADi5F5YC5f8APFfWrz25TdlGg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.7.tgz", - "integrity": "sha512-14J1feiQVWaGvRxj2WjyMuXS2jsBkgB3MdSN5HuC2G5nRspa5RK9COcs82Pwy5BuGcjb+fYaUj94mYcOj7rCvw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-replace-supers": "^7.16.7" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "requires": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - } - }, - "@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz", - "integrity": "sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz", - "integrity": "sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-replace-supers": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz", - "integrity": "sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-member-expression-to-functions": "^7.16.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.7.tgz", - "integrity": "sha512-AT3MufQ7zZEhU2hwOA11axBnExW0Lszu4RL/tAlUJBuNoRak+wehQW8h6KcXOcgjY42fHtDxswuMhMjFEuv/aw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-property-literals": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.7.tgz", - "integrity": "sha512-z4FGr9NMGdoIl1RqavCqGG+ZuYjfZ/hkCIeuH6Do7tXmSm0ls11nYVSJqFEUOSJbDab5wC6lRE/w6YjVcr6Hqw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-react-constant-elements": { - "version": "7.17.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.17.6.tgz", - "integrity": "sha512-OBv9VkyyKtsHZiHLoSfCn+h6yU7YKX8nrs32xUmOa1SRSk+t03FosB6fBZ0Yz4BpD1WV7l73Nsad+2Tz7APpqw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-react-display-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.16.7.tgz", - "integrity": "sha512-qgIg8BcZgd0G/Cz916D5+9kqX0c7nPZyXaP8R2tLNN5tkyIZdG5fEwBrxwplzSnjC1jvQmyMNVwUCZPcbGY7Pg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-react-jsx": { - "version": "7.17.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.17.3.tgz", - "integrity": "sha512-9tjBm4O07f7mzKSIlEmPdiE6ub7kfIe6Cd+w+oQebpATfTQMAgW+YOuWxogbKVTulA+MEO7byMeIUtQ1z+z+ZQ==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-jsx": "^7.16.7", - "@babel/types": "^7.17.0" - }, - "dependencies": { - "@babel/helper-annotate-as-pure": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", - "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/plugin-transform-react-jsx-development": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.16.7.tgz", - "integrity": "sha512-RMvQWvpla+xy6MlBpPlrKZCMRs2AGiHOGHY3xRwl0pEeim348dDyxeH4xBsMPbIMhujeq7ihE702eM2Ew0Wo+A==", - "dev": true, - "requires": { - "@babel/plugin-transform-react-jsx": "^7.16.7" - } - }, - "@babel/plugin-transform-react-pure-annotations": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.16.7.tgz", - "integrity": "sha512-hs71ToC97k3QWxswh2ElzMFABXHvGiJ01IB1TbYQDGeWRKWz/MPUTh5jGExdHvosYKpnJW5Pm3S4+TA3FyX+GA==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - }, - "dependencies": { - "@babel/helper-annotate-as-pure": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", - "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.17.9.tgz", - "integrity": "sha512-Lc2TfbxR1HOyn/c6b4Y/b6NHoTb67n/IoWLxTu4kC7h4KQnWlhCq2S8Tx0t2SVvv5Uu87Hs+6JEJ5kt2tYGylQ==", - "dev": true, - "requires": { - "regenerator-transform": "^0.15.0" - } - }, - "@babel/plugin-transform-reserved-words": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.16.7.tgz", - "integrity": "sha512-KQzzDnZ9hWQBjwi5lpY5v9shmm6IVG0U9pB18zvMu2i4H90xpT4gmqwPYsn8rObiadYe2M0gmgsiOIF5A/2rtg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-runtime": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.17.10.tgz", - "integrity": "sha512-6jrMilUAJhktTr56kACL8LnWC5hx3Lf27BS0R0DSyW/OoJfb/iTHeE96V3b1dgKG3FSFdd/0culnYWMkjcKCig==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "babel-plugin-polyfill-corejs2": "^0.3.0", - "babel-plugin-polyfill-corejs3": "^0.5.0", - "babel-plugin-polyfill-regenerator": "^0.3.0", - "semver": "^6.3.0" - }, - "dependencies": { - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz", - "integrity": "sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.7.tgz", - "integrity": "sha512-+pjJpgAngb53L0iaA5gU/1MLXJIfXcYepLgXB3esVRf4fqmj8f2cxM3/FKaHsZms08hFQJkFccEWuIpm429TXg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.7.tgz", - "integrity": "sha512-NJa0Bd/87QV5NZZzTuZG5BPJjLYadeSZ9fO6oOUoL4iQx+9EEuw/eEM92SrsT19Yc2jgB1u1hsjqDtH02c3Drw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.7.tgz", - "integrity": "sha512-VwbkDDUeenlIjmfNeDX/V0aWrQH2QiVyJtwymVQSzItFDTpxfyJh3EVaQiS0rIN/CqbLGr0VcGmuwyTdZtdIsA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.16.7.tgz", - "integrity": "sha512-p2rOixCKRJzpg9JB4gjnG4gjWkWa89ZoYUnl9snJ1cWIcTH/hvxZqfO+WjG6T8DRBpctEol5jw1O5rA8gkCokQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-typescript": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.16.8.tgz", - "integrity": "sha512-bHdQ9k7YpBDO2d0NVfkj51DpQcvwIzIusJ7mEUaMlbZq3Kt/U47j24inXZHQ5MDiYpCs+oZiwnXyKedE8+q7AQ==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-typescript": "^7.16.7" - } - }, - "@babel/plugin-transform-unicode-escapes": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.7.tgz", - "integrity": "sha512-TAV5IGahIz3yZ9/Hfv35TV2xEm+kaBDaZQCn2S/hG9/CZ0DktxJv9eKfPc7yYCvOYR4JGx1h8C+jcSOvgaaI/Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.7.tgz", - "integrity": "sha512-oC5tYYKw56HO75KZVLQ+R/Nl3Hro9kf8iG0hXoaHP7tjAyCpvqBiSNe6vGrZni1Z6MggmUOC6A7VP7AVmw225Q==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/preset-env": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.17.10.tgz", - "integrity": "sha512-YNgyBHZQpeoBSRBg0xixsZzfT58Ze1iZrajvv0lJc70qDDGuGfonEnMGfWeSY0mQ3JTuCWFbMkzFRVafOyJx4g==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.17.10", - "@babel/helper-compilation-targets": "^7.17.10", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-validator-option": "^7.16.7", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.16.7", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.16.7", - "@babel/plugin-proposal-async-generator-functions": "^7.16.8", - "@babel/plugin-proposal-class-properties": "^7.16.7", - "@babel/plugin-proposal-class-static-block": "^7.17.6", - "@babel/plugin-proposal-dynamic-import": "^7.16.7", - "@babel/plugin-proposal-export-namespace-from": "^7.16.7", - "@babel/plugin-proposal-json-strings": "^7.16.7", - "@babel/plugin-proposal-logical-assignment-operators": "^7.16.7", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7", - "@babel/plugin-proposal-numeric-separator": "^7.16.7", - "@babel/plugin-proposal-object-rest-spread": "^7.17.3", - "@babel/plugin-proposal-optional-catch-binding": "^7.16.7", - "@babel/plugin-proposal-optional-chaining": "^7.16.7", - "@babel/plugin-proposal-private-methods": "^7.16.11", - "@babel/plugin-proposal-private-property-in-object": "^7.16.7", - "@babel/plugin-proposal-unicode-property-regex": "^7.16.7", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.16.7", - "@babel/plugin-transform-async-to-generator": "^7.16.8", - "@babel/plugin-transform-block-scoped-functions": "^7.16.7", - "@babel/plugin-transform-block-scoping": "^7.16.7", - "@babel/plugin-transform-classes": "^7.16.7", - "@babel/plugin-transform-computed-properties": "^7.16.7", - "@babel/plugin-transform-destructuring": "^7.17.7", - "@babel/plugin-transform-dotall-regex": "^7.16.7", - "@babel/plugin-transform-duplicate-keys": "^7.16.7", - "@babel/plugin-transform-exponentiation-operator": "^7.16.7", - "@babel/plugin-transform-for-of": "^7.16.7", - "@babel/plugin-transform-function-name": "^7.16.7", - "@babel/plugin-transform-literals": "^7.16.7", - "@babel/plugin-transform-member-expression-literals": "^7.16.7", - "@babel/plugin-transform-modules-amd": "^7.16.7", - "@babel/plugin-transform-modules-commonjs": "^7.17.9", - "@babel/plugin-transform-modules-systemjs": "^7.17.8", - "@babel/plugin-transform-modules-umd": "^7.16.7", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.17.10", - "@babel/plugin-transform-new-target": "^7.16.7", - "@babel/plugin-transform-object-super": "^7.16.7", - "@babel/plugin-transform-parameters": "^7.16.7", - "@babel/plugin-transform-property-literals": "^7.16.7", - "@babel/plugin-transform-regenerator": "^7.17.9", - "@babel/plugin-transform-reserved-words": "^7.16.7", - "@babel/plugin-transform-shorthand-properties": "^7.16.7", - "@babel/plugin-transform-spread": "^7.16.7", - "@babel/plugin-transform-sticky-regex": "^7.16.7", - "@babel/plugin-transform-template-literals": "^7.16.7", - "@babel/plugin-transform-typeof-symbol": "^7.16.7", - "@babel/plugin-transform-unicode-escapes": "^7.16.7", - "@babel/plugin-transform-unicode-regex": "^7.16.7", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.17.10", - "babel-plugin-polyfill-corejs2": "^0.3.0", - "babel-plugin-polyfill-corejs3": "^0.5.0", - "babel-plugin-polyfill-regenerator": "^0.3.0", - "core-js-compat": "^3.22.1", - "semver": "^6.3.0" - }, - "dependencies": { - "@babel/compat-data": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz", - "integrity": "sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw==", - "dev": true - }, - "@babel/helper-compilation-targets": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz", - "integrity": "sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.17.10", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.20.2", - "semver": "^6.3.0" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "browserslist": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", - "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" - } - }, - "electron-to-chromium": { - "version": "1.4.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", - "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", - "dev": true - }, - "node-releases": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", - "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - } - }, - "@babel/preset-react": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.16.7.tgz", - "integrity": "sha512-fWpyI8UM/HE6DfPBzD8LnhQ/OcH8AgTaqcqP2nGOXEUV+VKBR5JRN9hCk9ai+zQQ57vtm9oWeXguBCPNUjytgA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-validator-option": "^7.16.7", - "@babel/plugin-transform-react-display-name": "^7.16.7", - "@babel/plugin-transform-react-jsx": "^7.16.7", - "@babel/plugin-transform-react-jsx-development": "^7.16.7", - "@babel/plugin-transform-react-pure-annotations": "^7.16.7" - }, - "dependencies": { - "@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true - } - } - }, - "@babel/preset-typescript": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.16.7.tgz", - "integrity": "sha512-WbVEmgXdIyvzB77AQjGBEyYPZx+8tTsO50XtfozQrkW8QB2rLJpH2lgx0TRw5EJrBxOZQ+wCcyPVQvS8tjEHpQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-validator-option": "^7.16.7", - "@babel/plugin-transform-typescript": "^7.16.7" - }, - "dependencies": { - "@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true - } - } - }, - "@babel/runtime": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz", - "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "@babel/runtime-corejs3": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.14.6.tgz", - "integrity": "sha512-Xl8SPYtdjcMoCsIM4teyVRg7jIcgl8F2kRtoCcXuHzXswt9UxZCS6BzRo8fcnCuP6u2XtPgvyonmEPF57Kxo9Q==", - "dev": true, - "requires": { - "core-js-pure": "^3.14.0", - "regenerator-runtime": "^0.13.4" - } - }, - "@babel/template": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", - "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/parser": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/traverse": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.5.tgz", - "integrity": "sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg==", - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.14.5", - "@babel/helper-function-name": "^7.14.5", - "@babel/helper-hoist-variables": "^7.14.5", - "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.14.5", - "@babel/types": "^7.14.5", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "@babel/types": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", - "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "to-fast-properties": "^2.0.0" - } - }, - "@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "dependencies": { - "@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - } - } - }, - "@csstools/normalize.css": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.0.0.tgz", - "integrity": "sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg==", - "dev": true - }, - "@csstools/postcss-color-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.0.tgz", - "integrity": "sha512-5D5ND/mZWcQoSfYnSPsXtuiFxhzmhxt6pcjrFLJyldj+p0ZN2vvRpYNX+lahFTtMhAYOa2WmkdGINr0yP0CvGA==", - "dev": true, - "requires": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "@csstools/postcss-font-format-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.0.tgz", - "integrity": "sha512-oO0cZt8do8FdVBX8INftvIA4lUrKUSCcWUf9IwH9IPWOgKT22oAZFXeHLoDK7nhB2SmkNycp5brxfNMRLIhd6Q==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "@csstools/postcss-hwb-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.0.tgz", - "integrity": "sha512-VSTd7hGjmde4rTj1rR30sokY3ONJph1reCBTUXqeW1fKwETPy1x4t/XIeaaqbMbC5Xg4SM/lyXZ2S8NELT2TaA==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "@csstools/postcss-ic-unit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.0.tgz", - "integrity": "sha512-i4yps1mBp2ijrx7E96RXrQXQQHm6F4ym1TOD0D69/sjDjZvQ22tqiEvaNw7pFZTUO5b9vWRHzbHzP9+UKuw+bA==", - "dev": true, - "requires": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "@csstools/postcss-is-pseudo-class": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.3.tgz", - "integrity": "sha512-wMQ3GMWrJyRQfvBJsD38ndF/nwHT32xevSn8w2X+iCoWqmhhoj0K7HgdGW8XQhah6sdENBa8yS9gRosdezaQZw==", - "dev": true, - "requires": { - "@csstools/selector-specificity": "^1.0.0", - "postcss-selector-parser": "^6.0.10" - } - }, - "@csstools/postcss-normalize-display-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.0.tgz", - "integrity": "sha512-bX+nx5V8XTJEmGtpWTO6kywdS725t71YSLlxWt78XoHUbELWgoCXeOFymRJmL3SU1TLlKSIi7v52EWqe60vJTQ==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "@csstools/postcss-oklab-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.0.tgz", - "integrity": "sha512-e/Q5HopQzmnQgqimG9v3w2IG4VRABsBq3itOcn4bnm+j4enTgQZ0nWsaH/m9GV2otWGQ0nwccYL5vmLKyvP1ww==", - "dev": true, - "requires": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "@csstools/postcss-progressive-custom-properties": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", - "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "@csstools/postcss-stepped-value-functions": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.0.tgz", - "integrity": "sha512-q8c4bs1GumAiRenmFjASBcWSLKrbzHzWl6C2HcaAxAXIiL2rUlUWbqQZUjwVG5tied0rld19j/Mm90K3qI26vw==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "@csstools/postcss-unset-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.1.tgz", - "integrity": "sha512-f1G1WGDXEU/RN1TWAxBPQgQudtLnLQPyiWdtypkPC+mVYNKFKH/HYXSxH4MVNqwF8M0eDsoiU7HumJHCg/L/jg==", - "dev": true, - "requires": {} - }, - "@csstools/selector-specificity": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-1.0.0.tgz", - "integrity": "sha512-RkYG5KiGNX0fJ5YoI0f4Wfq2Yo74D25Hru4fxTOioYdQvHBxcrrtTTyT5Ozzh2ejcNrhFy7IEts2WyEY7yi5yw==", - "dev": true, - "requires": {} - }, - "@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "requires": { - "@emotion/memoize": "0.7.4" - } - }, - "@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" - }, - "@emotion/stylis": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" - }, - "@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" - }, - "@eslint/eslintrc": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.3.tgz", - "integrity": "sha512-uGo44hIwoLGNyduRpjdEpovcbMdd+Nv7amtmJxnKmI8xj6yd5LncmSwDa5NgX/41lIFJtkjD6YdVfgEzPfJ5UA==", - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.3.2", - "globals": "^13.9.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==" - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "@fortawesome/fontawesome-free": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.1.1.tgz", - "integrity": "sha512-J/3yg2AIXc9wznaVqpHVX3Wa5jwKovVF0AMYSnbmcXTiL3PpRPfF58pzWucCwEiCJBp+hCNRLWClTomD8SseKg==" - }, - "@hookform/error-message": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@hookform/error-message/-/error-message-2.0.0.tgz", - "integrity": "sha512-Y90nHzjgL2MP7GFy75kscdvxrCTjtyxGmOLLxX14nd08OXRIh9lMH/y9Kpdo0p1IPowJBiZMHyueg7p+yrqynQ==", - "requires": {} - }, - "@hookform/resolvers": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.8.9.tgz", - "integrity": "sha512-IXwGpjewxScF4N2kuyYDip6ABqH4lCg9n1f1mp0vbmKik+u+nestpbtdEs6U1WQZxwaoK/2APv1+MEr4czX7XA==", - "requires": {} - }, - "@humanwhocodes/config-array": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", - "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.4" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" - }, - "@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true - }, - "@jest/console": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.0.tgz", - "integrity": "sha512-tscn3dlJFGay47kb4qVruQg/XWlmvU0xp3EJOjzzY+sBaI+YgwKcvAmTcyYU7xEiLLIY5HCdWRooAL8dqkFlDA==", - "dev": true, - "requires": { - "@jest/types": "^28.1.0", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^28.1.0", - "jest-util": "^28.1.0", - "slash": "^3.0.0" - } - }, - "@jest/core": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-28.1.0.tgz", - "integrity": "sha512-/2PTt0ywhjZ4NwNO4bUqD9IVJfmFVhVKGlhvSpmEfUCuxYf/3NHcKmRFI+I71lYzbTT3wMuYpETDCTHo81gC/g==", - "dev": true, - "peer": true, - "requires": { - "@jest/console": "^28.1.0", - "@jest/reporters": "^28.1.0", - "@jest/test-result": "^28.1.0", - "@jest/transform": "^28.1.0", - "@jest/types": "^28.1.0", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^28.0.2", - "jest-config": "^28.1.0", - "jest-haste-map": "^28.1.0", - "jest-message-util": "^28.1.0", - "jest-regex-util": "^28.0.2", - "jest-resolve": "^28.1.0", - "jest-resolve-dependencies": "^28.1.0", - "jest-runner": "^28.1.0", - "jest-runtime": "^28.1.0", - "jest-snapshot": "^28.1.0", - "jest-util": "^28.1.0", - "jest-validate": "^28.1.0", - "jest-watcher": "^28.1.0", - "micromatch": "^4.0.4", - "pretty-format": "^28.1.0", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true - }, - "jest-haste-map": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.0.tgz", - "integrity": "sha512-xyZ9sXV8PtKi6NCrJlmq53PyNVHzxmcfXNVvIRHpHmh1j/HChC4pwKgyjj7Z9us19JMw8PpQTJsFWOsIfT93Dw==", - "dev": true, - "peer": true, - "requires": { - "@jest/types": "^28.1.0", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.3.2", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.0", - "jest-worker": "^28.1.0", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - } - }, - "jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "dev": true, - "peer": true - }, - "jest-resolve": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-28.1.0.tgz", - "integrity": "sha512-vvfN7+tPNnnhDvISuzD1P+CRVP8cK0FHXRwPAcdDaQv4zgvwvag2n55/h5VjYcM5UJG7L4TwE5tZlzcI0X2Lhw==", - "dev": true, - "peer": true, - "requires": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^28.1.0", - "jest-validate": "^28.1.0", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - } - }, - "jest-validate": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.0.tgz", - "integrity": "sha512-Lly7CJYih3vQBfjLeANGgBSBJ7pEa18cxpQfQEq2go2xyEzehnHfQTjoUia8xUv4x4J80XKFIDwJJThXtRFQXQ==", - "dev": true, - "peer": true, - "requires": { - "@jest/types": "^28.1.0", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^28.0.2", - "leven": "^3.1.0", - "pretty-format": "^28.1.0" - } - }, - "jest-worker": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.0.tgz", - "integrity": "sha512-ZHwM6mNwaWBR52Snff8ZvsCTqQsvhCxP/bT1I6T6DAnb6ygkshsyLQIMxFwHpYxht0HOoqt23JlC01viI7T03A==", - "dev": true, - "peer": true, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - } - }, - "pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "requires": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - } - }, - "react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/environment": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-28.1.0.tgz", - "integrity": "sha512-S44WGSxkRngzHslhV6RoAExekfF7Qhwa6R5+IYFa81mpcj0YgdBnRSmvHe3SNwOt64yXaE5GG8Y2xM28ii5ssA==", - "dev": true, - "peer": true, - "requires": { - "@jest/fake-timers": "^28.1.0", - "@jest/types": "^28.1.0", - "@types/node": "*", - "jest-mock": "^28.1.0" - } - }, - "@jest/expect": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-28.1.0.tgz", - "integrity": "sha512-be9ETznPLaHOmeJqzYNIXv1ADEzENuQonIoobzThOYPuK/6GhrWNIJDVTgBLCrz3Am73PyEU2urQClZp0hLTtA==", - "dev": true, - "peer": true, - "requires": { - "expect": "^28.1.0", - "jest-snapshot": "^28.1.0" - } - }, - "@jest/expect-utils": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.0.tgz", - "integrity": "sha512-5BrG48dpC0sB80wpeIX5FU6kolDJI4K0n5BM9a5V38MGx0pyRvUBSS0u2aNTdDzmOrCjhOg8pGs6a20ivYkdmw==", - "dev": true, - "peer": true, - "requires": { - "jest-get-type": "^28.0.2" - } - }, - "@jest/fake-timers": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-28.1.0.tgz", - "integrity": "sha512-Xqsf/6VLeAAq78+GNPzI7FZQRf5cCHj1qgQxCjws9n8rKw8r1UYoeaALwBvyuzOkpU3c1I6emeMySPa96rxtIg==", - "dev": true, - "peer": true, - "requires": { - "@jest/types": "^28.1.0", - "@sinonjs/fake-timers": "^9.1.1", - "@types/node": "*", - "jest-message-util": "^28.1.0", - "jest-mock": "^28.1.0", - "jest-util": "^28.1.0" - } - }, - "@jest/globals": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-28.1.0.tgz", - "integrity": "sha512-3m7sTg52OTQR6dPhsEQSxAvU+LOBbMivZBwOvKEZ+Rb+GyxVnXi9HKgOTYkx/S99T8yvh17U4tNNJPIEQmtwYw==", - "dev": true, - "peer": true, - "requires": { - "@jest/environment": "^28.1.0", - "@jest/expect": "^28.1.0", - "@jest/types": "^28.1.0" - } - }, - "@jest/reporters": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-28.1.0.tgz", - "integrity": "sha512-qxbFfqap/5QlSpIizH9c/bFCDKsQlM4uAKSOvZrP+nIdrjqre3FmKzpTtYyhsaVcOSNK7TTt2kjm+4BJIjysFA==", - "dev": true, - "peer": true, - "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^28.1.0", - "@jest/test-result": "^28.1.0", - "@jest/transform": "^28.1.0", - "@jest/types": "^28.1.0", - "@jridgewell/trace-mapping": "^0.3.7", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-util": "^28.1.0", - "jest-worker": "^28.1.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^9.0.0" - }, - "dependencies": { - "jest-worker": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.0.tgz", - "integrity": "sha512-ZHwM6mNwaWBR52Snff8ZvsCTqQsvhCxP/bT1I6T6DAnb6ygkshsyLQIMxFwHpYxht0HOoqt23JlC01viI7T03A==", - "dev": true, - "peer": true, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - } - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/schemas": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.0.2.tgz", - "integrity": "sha512-YVDJZjd4izeTDkij00vHHAymNXQ6WWsdChFRK86qck6Jpr3DCL5W3Is3vslviRlP+bLuMYRLbdp98amMvqudhA==", - "dev": true, - "requires": { - "@sinclair/typebox": "^0.23.3" - } - }, - "@jest/source-map": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-28.0.2.tgz", - "integrity": "sha512-Y9dxC8ZpN3kImkk0LkK5XCEneYMAXlZ8m5bflmSL5vrwyeUpJfentacCUg6fOb8NOpOO7hz2+l37MV77T6BFPw==", - "dev": true, - "peer": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.7", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - } - }, - "@jest/test-result": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.0.tgz", - "integrity": "sha512-sBBFIyoPzrZho3N+80P35A5oAkSKlGfsEFfXFWuPGBsW40UAjCkGakZhn4UQK4iQlW2vgCDMRDOob9FGKV8YoQ==", - "dev": true, - "requires": { - "@jest/console": "^28.1.0", - "@jest/types": "^28.1.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - } - }, - "@jest/test-sequencer": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-28.1.0.tgz", - "integrity": "sha512-tZCEiVWlWNTs/2iK9yi6o3AlMfbbYgV4uuZInSVdzZ7ftpHZhCMuhvk2HLYhCZzLgPFQ9MnM1YaxMnh3TILFiQ==", - "dev": true, - "peer": true, - "requires": { - "@jest/test-result": "^28.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.0", - "slash": "^3.0.0" - }, - "dependencies": { - "jest-haste-map": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.0.tgz", - "integrity": "sha512-xyZ9sXV8PtKi6NCrJlmq53PyNVHzxmcfXNVvIRHpHmh1j/HChC4pwKgyjj7Z9us19JMw8PpQTJsFWOsIfT93Dw==", - "dev": true, - "peer": true, - "requires": { - "@jest/types": "^28.1.0", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.3.2", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.0", - "jest-worker": "^28.1.0", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - } - }, - "jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "dev": true, - "peer": true - }, - "jest-worker": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.0.tgz", - "integrity": "sha512-ZHwM6mNwaWBR52Snff8ZvsCTqQsvhCxP/bT1I6T6DAnb6ygkshsyLQIMxFwHpYxht0HOoqt23JlC01viI7T03A==", - "dev": true, - "peer": true, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - } - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/transform": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-28.1.0.tgz", - "integrity": "sha512-omy2xe5WxlAfqmsTjTPxw+iXRTRnf+NtX0ToG+4S0tABeb4KsKmPUHq5UBuwunHg3tJRwgEQhEp0M/8oiatLEA==", - "dev": true, - "peer": true, - "requires": { - "@babel/core": "^7.11.6", - "@jest/types": "^28.1.0", - "@jridgewell/trace-mapping": "^0.3.7", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.0", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.1" - }, - "dependencies": { - "jest-haste-map": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.0.tgz", - "integrity": "sha512-xyZ9sXV8PtKi6NCrJlmq53PyNVHzxmcfXNVvIRHpHmh1j/HChC4pwKgyjj7Z9us19JMw8PpQTJsFWOsIfT93Dw==", - "dev": true, - "peer": true, - "requires": { - "@jest/types": "^28.1.0", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.3.2", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.0", - "jest-worker": "^28.1.0", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - } - }, - "jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "dev": true, - "peer": true - }, - "jest-worker": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.0.tgz", - "integrity": "sha512-ZHwM6mNwaWBR52Snff8ZvsCTqQsvhCxP/bT1I6T6DAnb6ygkshsyLQIMxFwHpYxht0HOoqt23JlC01viI7T03A==", - "dev": true, - "peer": true, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - } - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/types": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.0.tgz", - "integrity": "sha512-xmEggMPr317MIOjjDoZ4ejCSr9Lpbt/u34+dvc99t7DS8YirW5rwZEhzKPC2BMUFkUhI48qs6qLUSGw5FuL0GA==", - "dev": true, - "requires": { - "@jest/schemas": "^28.0.2", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz", - "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==", - "dev": true - }, - "@jridgewell/set-array": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.1.tgz", - "integrity": "sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", - "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.11.tgz", - "integrity": "sha512-RllI476aSMsxzeI9TtlSMoNTgHDxEmnl6GkkHwhr0vdL8W+0WuesyI8Vd3rBOfrwtPXbPxdT9ADJdiOKgzxPQA==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "@leichtgewicht/ip-codec": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", - "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", - "dev": true - }, - "@nestjs/common": { - "version": "8.4.4", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-8.4.4.tgz", - "integrity": "sha512-QHi7QcgH/5Jinz+SCfIZJkFHc6Cch1YsAEGFEhi6wSp6MILb0sJMQ1CX06e9tCOAjSlBwaJj4PH0eFCVau5v9Q==", - "dev": true, - "requires": { - "axios": "0.26.1", - "iterare": "1.2.1", - "tslib": "2.3.1", - "uuid": "8.3.2" - }, - "dependencies": { - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true - } - } - }, - "@nestjs/core": { - "version": "8.4.4", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-8.4.4.tgz", - "integrity": "sha512-Ef3yJPuzAttpNfehnGqIV5kHIL9SHptB5F4ERxoU7pT61H3xiYpZw6hSjx68cJO7cc6rm7/N+b4zeuJvFHtvBg==", - "dev": true, - "requires": { - "@nuxtjs/opencollective": "0.3.2", - "fast-safe-stringify": "2.1.1", - "iterare": "1.2.1", - "object-hash": "3.0.0", - "path-to-regexp": "3.2.0", - "tslib": "2.3.1", - "uuid": "8.3.2" - }, - "dependencies": { - "path-to-regexp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", - "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==", - "dev": true - }, - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true - } - } - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.7.tgz", - "integrity": "sha512-BTIhocbPBSrRmHxOAJFtR18oLhxTtAFDAvL8hY1S3iU8k+E60W/YFs4jrixGzQjMpF4qPXxIQHcjVD9dz1C2QA==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@nuxtjs/opencollective": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", - "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "consola": "^2.15.0", - "node-fetch": "^2.6.1" - } - }, - "@openapitools/openapi-generator-cli": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.5.1.tgz", - "integrity": "sha512-WSRQBU0dCSVD+0Qv8iCsv0C4iMaZe/NpJ/CT4SmrEYLH3txoKTE8wEfbdj/kqShS8Or0YEGDPUzhSIKY151L0w==", - "dev": true, - "requires": { - "@nestjs/common": "8.4.4", - "@nestjs/core": "8.4.4", - "@nuxtjs/opencollective": "0.3.2", - "chalk": "4.1.2", - "commander": "8.3.0", - "compare-versions": "4.1.3", - "concurrently": "6.5.1", - "console.table": "0.10.0", - "fs-extra": "10.0.1", - "glob": "7.1.6", - "inquirer": "8.2.2", - "lodash": "4.17.21", - "reflect-metadata": "0.1.13", - "rxjs": "7.5.5", - "tslib": "2.0.3" - }, - "dependencies": { - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "fs-extra": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz", - "integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rxjs": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz", - "integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==", - "dev": true, - "requires": { - "tslib": "^2.1.0" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true - } - } - }, - "tslib": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", - "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", - "dev": true - } - } - }, - "@pmmmwh/react-refresh-webpack-plugin": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.6.tgz", - "integrity": "sha512-IIWxofIYt/AbMwoeBgj+O2aAXLrlCQVg+A4a2zfpXFNHgP8o8rvi3v+oe5t787Lj+KXlKOh8BAiUp9bhuELXhg==", - "dev": true, - "requires": { - "ansi-html-community": "^0.0.8", - "common-path-prefix": "^3.0.0", - "core-js-pure": "^3.8.1", - "error-stack-parser": "^2.0.6", - "find-up": "^5.0.0", - "html-entities": "^2.1.0", - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0", - "source-map": "^0.7.3" - }, - "dependencies": { - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } - } - }, - "@popperjs/core": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.2.tgz", - "integrity": "sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==" - }, - "@reduxjs/toolkit": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.1.tgz", - "integrity": "sha512-Q6mzbTpO9nOYRnkwpDlFOAbQnd3g7zj7CtHAZWz5SzE5lcV97Tf8f3SzOO8BoPOMYBFgfZaqTUZqgGu+a0+Fng==", - "requires": { - "immer": "^9.0.7", - "redux": "^4.1.2", - "redux-thunk": "^2.4.1", - "reselect": "^4.1.5" - } - }, - "@rollup/plugin-babel": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", - "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.10.4", - "@rollup/pluginutils": "^3.1.0" - } - }, - "@rollup/plugin-node-resolve": { - "version": "11.2.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", - "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^3.1.0", - "@types/resolve": "1.17.1", - "builtin-modules": "^3.1.0", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.19.0" - } - }, - "@rollup/plugin-replace": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", - "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^3.1.0", - "magic-string": "^0.25.7" - } - }, - "@rollup/pluginutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", - "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", - "dev": true, - "requires": { - "@types/estree": "0.0.39", - "estree-walker": "^1.0.1", - "picomatch": "^2.2.2" - }, - "dependencies": { - "@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", - "dev": true - } - } - }, - "@rooks/use-outside-click-ref": { - "version": "4.11.2", - "resolved": "https://registry.npmjs.org/@rooks/use-outside-click-ref/-/use-outside-click-ref-4.11.2.tgz", - "integrity": "sha512-w2bCW69zcpLh0KmN/odAuBsQ3sps+73KEu7zMOi0o4YMfDo+tXcqwlTJiLYysd0BEoQC9pNIklzZmI9zZep69g==", - "requires": {} - }, - "@rushstack/eslint-patch": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.3.tgz", - "integrity": "sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==", - "dev": true - }, - "@sinclair/typebox": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.23.5.tgz", - "integrity": "sha512-AFBVi/iT4g20DHoujvMH1aEDn8fGJh4xsRGCP6d8RpLPMqsNPvW01Jcn0QysXTsg++/xj25NmJsGyH9xug/wKg==", - "dev": true - }, - "@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", - "dev": true, - "peer": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, - "@surma/rollup-plugin-off-main-thread": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", - "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", - "dev": true, - "requires": { - "ejs": "^3.1.6", - "json5": "^2.2.0", - "magic-string": "^0.25.0", - "string.prototype.matchall": "^4.0.6" - }, - "dependencies": { - "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true - } - } - }, - "@svgr/babel-plugin-add-jsx-attribute": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz", - "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==", - "dev": true - }, - "@svgr/babel-plugin-remove-jsx-attribute": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz", - "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==", - "dev": true - }, - "@svgr/babel-plugin-remove-jsx-empty-expression": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz", - "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==", - "dev": true - }, - "@svgr/babel-plugin-replace-jsx-attribute-value": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz", - "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==", - "dev": true - }, - "@svgr/babel-plugin-svg-dynamic-title": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz", - "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==", - "dev": true - }, - "@svgr/babel-plugin-svg-em-dimensions": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz", - "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==", - "dev": true - }, - "@svgr/babel-plugin-transform-react-native-svg": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz", - "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==", - "dev": true - }, - "@svgr/babel-plugin-transform-svg-component": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz", - "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==", - "dev": true - }, - "@svgr/babel-preset": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz", - "integrity": "sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==", - "dev": true, - "requires": { - "@svgr/babel-plugin-add-jsx-attribute": "^5.4.0", - "@svgr/babel-plugin-remove-jsx-attribute": "^5.4.0", - "@svgr/babel-plugin-remove-jsx-empty-expression": "^5.0.1", - "@svgr/babel-plugin-replace-jsx-attribute-value": "^5.0.1", - "@svgr/babel-plugin-svg-dynamic-title": "^5.4.0", - "@svgr/babel-plugin-svg-em-dimensions": "^5.4.0", - "@svgr/babel-plugin-transform-react-native-svg": "^5.4.0", - "@svgr/babel-plugin-transform-svg-component": "^5.5.0" - } - }, - "@svgr/core": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz", - "integrity": "sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==", - "dev": true, - "requires": { - "@svgr/plugin-jsx": "^5.5.0", - "camelcase": "^6.2.0", - "cosmiconfig": "^7.0.0" - } - }, - "@svgr/hast-util-to-babel-ast": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz", - "integrity": "sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==", - "dev": true, - "requires": { - "@babel/types": "^7.12.6" - } - }, - "@svgr/plugin-jsx": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz", - "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==", - "dev": true, - "requires": { - "@babel/core": "^7.12.3", - "@svgr/babel-preset": "^5.5.0", - "@svgr/hast-util-to-babel-ast": "^5.5.0", - "svg-parser": "^2.0.2" - } - }, - "@svgr/plugin-svgo": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz", - "integrity": "sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==", - "dev": true, - "requires": { - "cosmiconfig": "^7.0.0", - "deepmerge": "^4.2.2", - "svgo": "^1.2.2" - } - }, - "@svgr/webpack": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz", - "integrity": "sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==", - "dev": true, - "requires": { - "@babel/core": "^7.12.3", - "@babel/plugin-transform-react-constant-elements": "^7.12.1", - "@babel/preset-env": "^7.12.1", - "@babel/preset-react": "^7.12.5", - "@svgr/core": "^5.5.0", - "@svgr/plugin-jsx": "^5.5.0", - "@svgr/plugin-svgo": "^5.5.0", - "loader-utils": "^2.0.0" - } - }, - "@testing-library/dom": { - "version": "8.11.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.11.1.tgz", - "integrity": "sha512-3KQDyx9r0RKYailW2MiYrSSKEfH0GTkI51UGEvJenvcoDoeRYs0PZpi2SXqtnMClQvCqdtTTpOfFETDTVADpAg==", - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^4.2.0", - "aria-query": "^5.0.0", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.4.4", - "pretty-format": "^27.0.2" - }, - "dependencies": { - "aria-query": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz", - "integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==" - }, - "dom-accessibility-api": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.10.tgz", - "integrity": "sha512-Xu9mD0UjrJisTmv7lmVSDMagQcU9R5hwAbxsaAE/35XPnPLJobbuREfV/rraiSaEj/UOvgrzQs66zyTWTlyd+g==" - } - } - }, - "@testing-library/jest-dom": { - "version": "5.16.4", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.4.tgz", - "integrity": "sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA==", - "dev": true, - "requires": { - "@babel/runtime": "^7.9.2", - "@types/testing-library__jest-dom": "^5.9.1", - "aria-query": "^5.0.0", - "chalk": "^3.0.0", - "css": "^3.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.5.6", - "lodash": "^4.17.15", - "redent": "^3.0.0" - }, - "dependencies": { - "aria-query": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz", - "integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==", - "dev": true - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - } - } - }, - "@testing-library/react": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.2.0.tgz", - "integrity": "sha512-Bprbz/SZVONCJy5f7hcihNCv313IJXdYiv0nSJklIs1SQCIHHNlnGNkosSXnGZTmesyGIcBGNppYhXcc11pb7g==", - "requires": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^8.5.0", - "@types/react-dom": "^18.0.0" - }, - "dependencies": { - "@types/react-dom": { - "version": "18.0.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.3.tgz", - "integrity": "sha512-1RRW9kst+67gveJRYPxGmVy8eVJ05O43hg77G2j5m76/RFJtMbcfAs2viQ2UNsvvDg8F7OfQZx8qQcl6ymygaQ==", - "requires": { - "@types/react": "*" - } - } - } - }, - "@testing-library/user-event": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", - "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.12.5" - } - }, - "@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "dev": true - }, - "@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true - }, - "@tsconfig/node10": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", - "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", - "dev": true - }, - "@tsconfig/node12": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", - "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", - "dev": true - }, - "@tsconfig/node14": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", - "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", - "dev": true - }, - "@tsconfig/node16": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", - "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", - "dev": true - }, - "@types/aria-query": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==" - }, - "@types/babel__core": { - "version": "7.1.19", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz", - "integrity": "sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "@types/babel__generator": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@types/babel__traverse": { - "version": "7.17.1", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.17.1.tgz", - "integrity": "sha512-kVzjari1s2YVi77D3w1yuvohV2idweYXMCDzqBiVNN63TcDWrIlTVOYpqVrvbbyOE/IyzBoTKF0fdnLPEORFxA==", - "dev": true, - "requires": { - "@babel/types": "^7.3.0" - } - }, - "@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "dev": true, - "requires": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "@types/bonjour": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", - "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/connect": { - "version": "3.4.34", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", - "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/connect-history-api-fallback": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", - "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", - "dev": true, - "requires": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } - }, - "@types/eslint": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", - "integrity": "sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==", - "dev": true, - "requires": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "@types/eslint-scope": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz", - "integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==", - "dev": true, - "requires": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", - "dev": true - }, - "@types/eventsource": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.1.8.tgz", - "integrity": "sha512-fJQNt9LijJCZwYvM6O30uLzdpAK9zs52Uc9iUW9M2Zsg0HQM6DLf6QysjC/wuFX+0798B8AppVMvgdO6IftPKQ==", - "dev": true - }, - "@types/express": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", - "dev": true, - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.22", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.22.tgz", - "integrity": "sha512-WdqmrUsRS4ootGha6tVwk/IVHM1iorU8tGehftQD2NWiPniw/sm7xdJOIlXLwqdInL9wBw/p7oO8vaYEF3NDmA==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "@types/graceful-fs": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", - "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/history": { - "version": "4.7.11", - "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", - "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", - "dev": true - }, - "@types/hoist-non-react-statics": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", - "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", - "requires": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" - } - }, - "@types/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", - "dev": true - }, - "@types/http-proxy": { - "version": "1.17.8", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.8.tgz", - "integrity": "sha512-5kPLG5BKpWYkw/LVOGWpiq3nEVqxiN32rTgI53Sk12/xHFQ2rG3ehI9IO+O3W2QoKeyB92dJkoka8SUm6BX1pA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", - "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", - "dev": true - }, - "@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*" - } - }, - "@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, - "requires": { - "@types/istanbul-lib-report": "*" - } - }, - "@types/jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.1.tgz", - "integrity": "sha512-fUy7YRpT+rHXto1YlL+J9rs0uLGyiqVt3ZOTQR+4ROc47yNl8WLdVLgUloBRhOxP1PZvguHl44T3H0wAWxahYQ==", - "dev": true, - "requires": { - "jest-matcher-utils": "^27.0.0", - "pretty-format": "^27.0.0" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "dev": true - }, - "@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" - }, - "@types/lodash": { - "version": "4.14.177", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.177.tgz", - "integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==", - "dev": true - }, - "@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", - "dev": true - }, - "@types/node": { - "version": "16.11.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz", - "integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==", - "dev": true - }, - "@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "dev": true - }, - "@types/prettier": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.6.3.tgz", - "integrity": "sha512-ymZk3LEC/fsut+/Q5qejp6R9O1rMxz3XaRHDV6kX8MrGAhOSPqVARbDi+EZvInBpw+BnCX3TD240byVkOfQsHg==", - "dev": true - }, - "@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" - }, - "@types/q": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz", - "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==", - "dev": true - }, - "@types/qs": { - "version": "6.9.6", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz", - "integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==", - "dev": true - }, - "@types/range-parser": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", - "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", - "dev": true - }, - "@types/react": { - "version": "18.0.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.9.tgz", - "integrity": "sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw==", - "requires": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "@types/react-datepicker": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-4.3.4.tgz", - "integrity": "sha512-5nTTz37KdTUgMZ1AAxztMWNtEnIMVRo8oCAEhIv0a6uUqDjvSKaMyPRpBV+8chi6f/A8wlTKJIpojpXca2dx3A==", - "dev": true, - "requires": { - "@popperjs/core": "^2.9.2", - "@types/react": "*", - "date-fns": "^2.0.1", - "react-popper": "^2.2.5" - } - }, - "@types/react-dom": { - "version": "18.0.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.3.tgz", - "integrity": "sha512-1RRW9kst+67gveJRYPxGmVy8eVJ05O43hg77G2j5m76/RFJtMbcfAs2viQ2UNsvvDg8F7OfQZx8qQcl6ymygaQ==", - "dev": true, - "requires": { - "@types/react": "*" - } - }, - "@types/react-redux": { - "version": "7.1.22", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.22.tgz", - "integrity": "sha512-GxIA1kM7ClU73I6wg9IRTVwSO9GS+SAKZKe0Enj+82HMU6aoESFU2HNAdNi3+J53IaOHPiUfT3kSG4L828joDQ==", - "dev": true, - "requires": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, - "@types/react-router": { - "version": "5.1.18", - "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.18.tgz", - "integrity": "sha512-YYknwy0D0iOwKQgz9v8nOzt2J6l4gouBmDnWqUUznltOTaon+r8US8ky8HvN0tXvc38U9m6z/t2RsVsnd1zM0g==", - "dev": true, - "requires": { - "@types/history": "^4.7.11", - "@types/react": "*" - } - }, - "@types/react-router-dom": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", - "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", - "dev": true, - "requires": { - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router": "*" - } - }, - "@types/redux-mock-store": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.3.tgz", - "integrity": "sha512-Wqe3tJa6x9MxMN4DJnMfZoBRBRak1XTPklqj4qkVm5VBpZnC8PSADf4kLuFQ9NAdHaowfWoEeUMz7NWc2GMtnA==", - "dev": true, - "requires": { - "redux": "^4.0.5" - } - }, - "@types/resolve": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", - "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "dev": true - }, - "@types/scheduler": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" - }, - "@types/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", - "dev": true, - "requires": { - "@types/express": "*" - } - }, - "@types/serve-static": { - "version": "1.13.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", - "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", - "dev": true, - "requires": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "@types/sockjs": { - "version": "0.3.33", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", - "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", - "dev": true - }, - "@types/styled-components": { - "version": "5.1.18", - "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.18.tgz", - "integrity": "sha512-xPTYmWP7Mxk5TAD3pYsqjwA9G5fAI8e/S51QUJEl7EQD1siKCdiYXIWiH2lzoHRl+QqbQCJMcGv3YTF3OmyPdQ==", - "dev": true, - "requires": { - "@types/hoist-non-react-statics": "*", - "@types/react": "*", - "csstype": "^3.0.2" - } - }, - "@types/testing-library__jest-dom": { - "version": "5.14.1", - "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.1.tgz", - "integrity": "sha512-Gk9vaXfbzc5zCXI9eYE9BI5BNHEp4D3FWjgqBE/ePGYElLAP+KvxBcsdkwfIVvezs605oiyd/VrpiHe3Oeg+Aw==", - "dev": true, - "requires": { - "@types/jest": "*" - } - }, - "@types/trusted-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", - "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==", - "dev": true - }, - "@types/uuid": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", - "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", - "dev": true - }, - "@types/yargs": { - "version": "17.0.10", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.10.tgz", - "integrity": "sha512-gmEaFwpj/7f/ROdtIlci1R1VYU1J4j95m8T+Tj3iBgiBFKg1foE/PSl93bBd5T9LDXNPo8UlNN6W0qwD8O5OaA==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "@types/yargs-parser": { - "version": "20.2.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz", - "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", - "dev": true - }, - "@types/yup": { - "version": "0.29.13", - "resolved": "https://registry.npmjs.org/@types/yup/-/yup-0.29.13.tgz", - "integrity": "sha512-qRyuv+P/1t1JK1rA+elmK1MmCL1BapEzKKfbEhDBV/LMMse4lmhZ/XbgETI39JveDJRpLjmToOI6uFtMW/WR2g==" - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.10.0.tgz", - "integrity": "sha512-XXVKnMsq2fuu9K2KsIxPUGqb6xAImz8MEChClbXmE3VbveFtBUU5bzM6IPVWqzyADIgdkS2Ws/6Xo7W2TeZWjQ==", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.10.0", - "@typescript-eslint/type-utils": "5.10.0", - "@typescript-eslint/utils": "5.10.0", - "debug": "^4.3.2", - "functional-red-black-tree": "^1.0.1", - "ignore": "^5.1.8", - "regexpp": "^3.2.0", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "@typescript-eslint/experimental-utils": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.23.0.tgz", - "integrity": "sha512-I+3YGQztH1DM9kgWzjslpZzJCBMRz0KhYG2WP62IwpooeZ1L6Qt0mNK8zs+uP+R2HOsr+TeDW35Pitc3PfVv8Q==", - "dev": true, - "requires": { - "@typescript-eslint/utils": "5.23.0" - }, - "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.23.0.tgz", - "integrity": "sha512-EhjaFELQHCRb5wTwlGsNMvzK9b8Oco4aYNleeDlNuL6qXWDF47ch4EhVNPh8Rdhf9tmqbN4sWDk/8g+Z/J8JVw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/visitor-keys": "5.23.0" - } - }, - "@typescript-eslint/types": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.23.0.tgz", - "integrity": "sha512-NfBsV/h4dir/8mJwdZz7JFibaKC3E/QdeMEDJhiAE3/eMkoniZ7MjbEMCGXw6MZnZDMN3G9S0mH/6WUIj91dmw==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.23.0.tgz", - "integrity": "sha512-xE9e0lrHhI647SlGMl+m+3E3CKPF1wzvvOEWnuE3CCjjT7UiRnDGJxmAcVKJIlFgK6DY9RB98eLr1OPigPEOGg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/visitor-keys": "5.23.0", - "debug": "^4.3.2", - "globby": "^11.0.4", - "is-glob": "^4.0.3", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.23.0.tgz", - "integrity": "sha512-dbgaKN21drqpkbbedGMNPCtRPZo1IOUr5EI9Jrrh99r5UW5Q0dz46RKXeSBoPV+56R6dFKpbrdhgUNSJsDDRZA==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.23.0", - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/typescript-estree": "5.23.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.23.0.tgz", - "integrity": "sha512-Vd4mFNchU62sJB8pX19ZSPog05B0Y0CE2UxAZPT5k4iqhRYjPnqyY3woMxCd0++t9OTqkgjST+1ydLBi7e2Fvg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.23.0", - "eslint-visitor-keys": "^3.0.0" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "@typescript-eslint/parser": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.27.0.tgz", - "integrity": "sha512-8oGjQF46c52l7fMiPPvX4It3u3V3JipssqDfHQ2hcR0AeR8Zge+OYyKUCm5b70X72N1qXt0qgHenwN6Gc2SXZA==", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.27.0", - "@typescript-eslint/types": "5.27.0", - "@typescript-eslint/typescript-estree": "5.27.0", - "debug": "^4.3.4" - }, - "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.27.0.tgz", - "integrity": "sha512-VnykheBQ/sHd1Vt0LJ1JLrMH1GzHO+SzX6VTXuStISIsvRiurue/eRkTqSrG0CexHQgKG8shyJfR4o5VYioB9g==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.27.0", - "@typescript-eslint/visitor-keys": "5.27.0" - } - }, - "@typescript-eslint/types": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.27.0.tgz", - "integrity": "sha512-lY6C7oGm9a/GWhmUDOs3xAVRz4ty/XKlQ2fOLr8GAIryGn0+UBOoJDWyHer3UgrHkenorwvBnphhP+zPmzmw0A==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.27.0.tgz", - "integrity": "sha512-QywPMFvgZ+MHSLRofLI7BDL+UczFFHyj0vF5ibeChDAJgdTV8k4xgEwF0geFhVlPc1p8r70eYewzpo6ps+9LJQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.27.0", - "@typescript-eslint/visitor-keys": "5.27.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.27.0.tgz", - "integrity": "sha512-46cYrteA2MrIAjv9ai44OQDUoCZyHeGIc4lsjCUX2WT6r4C+kidz1bNiR4017wHOPUythYeH+Sc7/cFP97KEAA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.27.0", - "eslint-visitor-keys": "^3.3.0" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.10.0.tgz", - "integrity": "sha512-tgNgUgb4MhqK6DoKn3RBhyZ9aJga7EQrw+2/OiDk5hKf3pTVZWyqBi7ukP+Z0iEEDMF5FDa64LqODzlfE4O/Dg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.10.0", - "@typescript-eslint/visitor-keys": "5.10.0" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.10.0.tgz", - "integrity": "sha512-TzlyTmufJO5V886N+hTJBGIfnjQDQ32rJYxPaeiyWKdjsv2Ld5l8cbS7pxim4DeNs62fKzRSt8Q14Evs4JnZyQ==", - "dev": true, - "requires": { - "@typescript-eslint/utils": "5.10.0", - "debug": "^4.3.2", - "tsutils": "^3.21.0" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@typescript-eslint/types": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.10.0.tgz", - "integrity": "sha512-wUljCgkqHsMZbw60IbOqT/puLfyqqD5PquGiBo1u1IS3PLxdi3RDGlyf032IJyh+eQoGhz9kzhtZa+VC4eWTlQ==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.10.0.tgz", - "integrity": "sha512-x+7e5IqfwLwsxTdliHRtlIYkgdtYXzE0CkFeV6ytAqq431ZyxCFzNMNR5sr3WOlIG/ihVZr9K/y71VHTF/DUQA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.10.0", - "@typescript-eslint/visitor-keys": "5.10.0", - "debug": "^4.3.2", - "globby": "^11.0.4", - "is-glob": "^4.0.3", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "@typescript-eslint/utils": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.10.0.tgz", - "integrity": "sha512-IGYwlt1CVcFoE2ueW4/ioEwybR60RAdGeiJX/iDAw0t5w0wK3S7QncDwpmsM70nKgGTuVchEWB8lwZwHqPAWRg==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.10.0", - "@typescript-eslint/types": "5.10.0", - "@typescript-eslint/typescript-estree": "5.10.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - }, - "dependencies": { - "@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.10.0.tgz", - "integrity": "sha512-GMxj0K1uyrFLPKASLmZzCuSddmjZVbVj3Ouy5QVuIGKZopxvOr24JsS7gruz6C3GExE01mublZ3mIBOaon9zuQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.10.0", - "eslint-visitor-keys": "^3.0.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz", - "integrity": "sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==", - "dev": true - } - } - }, - "@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", - "dev": true, - "requires": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" - } - }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", - "dev": true - }, - "@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", - "dev": true - }, - "@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", - "dev": true - }, - "@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", - "dev": true, - "requires": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", - "dev": true - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" - } - }, - "@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", - "dev": true, - "requires": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", - "dev": true, - "requires": { - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", - "dev": true - }, - "@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" - } - }, - "@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" - } - }, - "@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" - } - }, - "@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" - } - }, - "@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@xtuc/long": "4.2.2" - } - }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true - }, - "abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "dev": true - }, - "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - } - }, - "ace-builds": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.13.tgz", - "integrity": "sha512-SOLzdaQkY6ecPKYRDDg+MY1WoGgXA34cIvYJNNoBMGGUswHmlauU2Hy0UL96vW0Fs/LgFbMUjD+6vqzWTldIYQ==" - }, - "acorn": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", - "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==" - }, - "acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", - "dev": true, - "requires": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" - }, - "dependencies": { - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - } - } - }, - "acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true, - "requires": {} - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "requires": {} - }, - "acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "dev": true, - "requires": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - }, - "dependencies": { - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - } - } - }, - "acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true - }, - "address": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/address/-/address-1.2.0.tgz", - "integrity": "sha512-tNEZYz5G/zYunxFm7sfhAxkXEuLj3K6BKwv6ZURlsF6yiUQ65z0Q2wZW9L5cPUl9ocofGvXOdFYbFHp0+6MOig==", - "dev": true - }, - "adjust-sourcemap-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", - "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", - "dev": true, - "requires": { - "loader-utils": "^2.0.0", - "regex-parser": "^2.2.11" - } - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "requires": { - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - }, - "ajv": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", - "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "requires": { - "ajv": "^8.0.0" - } - }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "requires": {} - }, - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - }, - "ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "requires": { - "type-fest": "^0.21.3" - }, - "dependencies": { - "type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true - } - } - }, - "ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "dev": true - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - }, - "dependencies": { - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - } - } - }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "aria-query": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", - "dev": true, - "requires": { - "@babel/runtime": "^7.10.2", - "@babel/runtime-corejs3": "^7.10.2" - } - }, - "array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", - "dev": true - }, - "array-includes": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", - "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5", - "get-intrinsic": "^1.1.1", - "is-string": "^1.0.7" - }, - "dependencies": { - "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - }, - "es-abstract": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.0.tgz", - "integrity": "sha512-URbD8tgRthKD3YcC39vbvSDrX23upXnPcnGAjQfgxXF5ID75YcENawc9ZX/9iTP9ptUyfCLIxTTuMYoRfiOVKA==", - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.1", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - } - }, - "has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" - }, - "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "requires": { - "call-bind": "^1.0.2" - } - }, - "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==" - }, - "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - } - }, - "string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - } - }, - "string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - } - }, - "unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "requires": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - } - } - } - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "array.prototype.flat": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", - "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.2", - "es-shim-unscopables": "^1.0.0" - }, - "dependencies": { - "es-abstract": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.0.tgz", - "integrity": "sha512-URbD8tgRthKD3YcC39vbvSDrX23upXnPcnGAjQfgxXF5ID75YcENawc9ZX/9iTP9ptUyfCLIxTTuMYoRfiOVKA==", - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.1", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - } - }, - "has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" - }, - "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "requires": { - "call-bind": "^1.0.2" - } - }, - "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==" - }, - "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - } - }, - "string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "dependencies": { - "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - } - } - }, - "string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "dependencies": { - "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - } - } - }, - "unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "requires": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - } - } - } - }, - "array.prototype.flatmap": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz", - "integrity": "sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.2", - "es-shim-unscopables": "^1.0.0" - }, - "dependencies": { - "es-abstract": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.0.tgz", - "integrity": "sha512-URbD8tgRthKD3YcC39vbvSDrX23upXnPcnGAjQfgxXF5ID75YcENawc9ZX/9iTP9ptUyfCLIxTTuMYoRfiOVKA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.1", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - } - }, - "has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true - }, - "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true - }, - "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - } - }, - "string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "dependencies": { - "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - } - } - }, - "string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "dependencies": { - "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - } - } - }, - "unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - } - } - } - }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", - "dev": true - }, - "ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", - "dev": true - }, - "astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true - }, - "async": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", - "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, - "at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "autoprefixer": { - "version": "10.4.7", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.7.tgz", - "integrity": "sha512-ypHju4Y2Oav95SipEcCcI5J7CGPuvz8oat7sUtYj3ClK44bldfvtvcxK6IEK++7rqB7YchDGzweZIBG+SD0ZAA==", - "dev": true, - "requires": { - "browserslist": "^4.20.3", - "caniuse-lite": "^1.0.30001335", - "fraction.js": "^4.2.0", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "browserslist": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", - "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" - } - }, - "electron-to-chromium": { - "version": "1.4.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", - "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", - "dev": true - }, - "node-releases": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", - "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", - "dev": true - }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "axe-core": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.3.5.tgz", - "integrity": "sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA==", - "dev": true - }, - "axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "dev": true, - "requires": { - "follow-redirects": "^1.14.8" - } - }, - "axobject-query": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", - "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", - "dev": true - }, - "babel-jest": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.0.tgz", - "integrity": "sha512-zNKk0yhDZ6QUwfxh9k07GII6siNGMJWVUU49gmFj5gfdqDKLqa2RArXOF2CODp4Dr7dLxN2cvAV+667dGJ4b4w==", - "dev": true, - "peer": true, - "requires": { - "@jest/transform": "^28.1.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^28.0.2", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - } - }, - "babel-loader": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.5.tgz", - "integrity": "sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==", - "dev": true, - "requires": { - "find-cache-dir": "^3.3.1", - "loader-utils": "^2.0.0", - "make-dir": "^3.1.0", - "schema-utils": "^2.6.5" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - } - } - } - }, - "babel-plugin-dynamic-import-node": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", - "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", - "dev": true, - "requires": { - "object.assign": "^4.1.0" - } - }, - "babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - } - }, - "babel-plugin-jest-hoist": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.0.2.tgz", - "integrity": "sha512-Kizhn/ZL+68ZQHxSnHyuvJv8IchXD62KQxV77TBDV/xoBFBOfgRAk97GNs6hXdTTCiVES9nB2I6+7MXXrk5llQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - } - }, - "babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - } - }, - "babel-plugin-named-asset-import": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz", - "integrity": "sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==", - "dev": true, - "requires": {} - }, - "babel-plugin-polyfill-corejs2": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz", - "integrity": "sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.13.11", - "@babel/helper-define-polyfill-provider": "^0.3.1", - "semver": "^6.1.1" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "babel-plugin-polyfill-corejs3": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz", - "integrity": "sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ==", - "dev": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.1", - "core-js-compat": "^3.21.0" - } - }, - "babel-plugin-polyfill-regenerator": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz", - "integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==", - "dev": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.1" - } - }, - "babel-plugin-styled-components": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.13.2.tgz", - "integrity": "sha512-Vb1R3d4g+MUfPQPVDMCGjm3cDocJEUTR7Xq7QS95JWWeksN1wdFRYpD2kulDgI3Huuaf1CZd+NK4KQmqUFh5dA==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.0.0", - "@babel/helper-module-imports": "^7.0.0", - "babel-plugin-syntax-jsx": "^6.18.0", - "lodash": "^4.17.11" - } - }, - "babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" - }, - "babel-plugin-transform-react-remove-prop-types": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", - "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==", - "dev": true - }, - "babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "dev": true, - "requires": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" - } - }, - "babel-preset-jest": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-28.0.2.tgz", - "integrity": "sha512-sYzXIdgIXXroJTFeB3S6sNDWtlJ2dllCdTEsnZ65ACrMojj3hVNFRmnJ1HZtomGi+Be7aqpY/HJ92fr8OhKVkQ==", - "dev": true, - "peer": true, - "requires": { - "babel-plugin-jest-hoist": "^28.0.2", - "babel-preset-current-node-syntax": "^1.0.0" - } - }, - "babel-preset-react-app": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.0.1.tgz", - "integrity": "sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg==", - "dev": true, - "requires": { - "@babel/core": "^7.16.0", - "@babel/plugin-proposal-class-properties": "^7.16.0", - "@babel/plugin-proposal-decorators": "^7.16.4", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0", - "@babel/plugin-proposal-numeric-separator": "^7.16.0", - "@babel/plugin-proposal-optional-chaining": "^7.16.0", - "@babel/plugin-proposal-private-methods": "^7.16.0", - "@babel/plugin-transform-flow-strip-types": "^7.16.0", - "@babel/plugin-transform-react-display-name": "^7.16.0", - "@babel/plugin-transform-runtime": "^7.16.4", - "@babel/preset-env": "^7.16.4", - "@babel/preset-react": "^7.16.0", - "@babel/preset-typescript": "^7.16.0", - "@babel/runtime": "^7.16.3", - "babel-plugin-macros": "^3.1.0", - "babel-plugin-transform-react-remove-prop-types": "^0.4.24" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/compat-data": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz", - "integrity": "sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw==", - "dev": true - }, - "@babel/core": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.10.tgz", - "integrity": "sha512-liKoppandF3ZcBnIYFjfSDHZLKdLHGJRkoWtG8zQyGJBQfIYobpnVGI5+pLBNtS6psFLDzyq8+h5HiVljW9PNA==", - "dev": true, - "requires": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-compilation-targets": "^7.17.10", - "@babel/helper-module-transforms": "^7.17.7", - "@babel/helpers": "^7.17.9", - "@babel/parser": "^7.17.10", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.10", - "@babel/types": "^7.17.10", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" - } - }, - "@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "requires": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz", - "integrity": "sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.17.10", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.20.2", - "semver": "^6.3.0" - } - }, - "@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-transforms": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", - "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-simple-access": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", - "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true - }, - "@babel/helpers": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.9.tgz", - "integrity": "sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q==", - "dev": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.9", - "@babel/types": "^7.17.0" - } - }, - "@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/runtime": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz", - "integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "browserslist": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", - "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "electron-to-chromium": { - "version": "1.4.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", - "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", - "dev": true - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node-releases": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", - "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true - }, - "batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", - "dev": true - }, - "bfj": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.0.2.tgz", - "integrity": "sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw==", - "dev": true, - "requires": { - "bluebird": "^3.5.5", - "check-types": "^11.1.1", - "hoopy": "^0.1.4", - "tryer": "^1.0.1" - } - }, - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" - }, - "bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, - "body-parser": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", - "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", - "dev": true, - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.10.3", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "dependencies": { - "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true - } - } - }, - "bonjour-service": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.0.12.tgz", - "integrity": "sha512-pMmguXYCu63Ug37DluMKEHdxc+aaIf/ay4YbF8Gxtba+9d3u+rmEWy61VK3Z3hp8Rskok3BunHYnG0dUHAsblw==", - "dev": true, - "requires": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.4" - } - }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { - "fill-range": "^7.0.1" - } - }, - "browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", - "dev": true - }, - "browserslist": { - "version": "4.16.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", - "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", - "requires": { - "caniuse-lite": "^1.0.30001219", - "colorette": "^1.2.2", - "electron-to-chromium": "^1.3.723", - "escalade": "^3.1.1", - "node-releases": "^1.1.71" - } - }, - "bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "requires": { - "fast-json-stable-stringify": "2.x" - } - }, - "bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "requires": { - "node-int64": "^0.4.0" - } - }, - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, - "builtin-modules": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", - "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", - "dev": true - }, - "bulma": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.3.tgz", - "integrity": "sha512-0d7GNW1PY4ud8TWxdNcP6Cc8Bu7MxcntD/RRLGWuiw/s0a9P+XlH/6QoOIrmbj6o8WWJzJYhytiu9nFjTszk1g==" - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", - "dev": true - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" - }, - "camel-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "dev": true, - "requires": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true - } - } - }, - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true - }, - "camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true - }, - "camelize": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", - "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" - }, - "caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "caniuse-lite": { - "version": "1.0.30001344", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001344.tgz", - "integrity": "sha512-0ZFjnlCaXNOAYcV7i+TtdKBp0L/3XEU2MF/x6Du1lrh+SRX4IfzIVL4HNJg5pB2PmFb8rszIGyOvsZnqqRoc2g==" - }, - "case-sensitive-paths-webpack-plugin": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", - "integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==", - "dev": true - }, - "chalk": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", - "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true - }, - "charcodes": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/charcodes/-/charcodes-0.2.0.tgz", - "integrity": "sha512-Y4kiDb+AM4Ecy58YkuZrrSRJBDQdQ2L+NyS1vHHFtNtUjgutcZfx3yp1dAONI/oPaPmyGfCLx5CxL+zauIMyKQ==", - "dev": true - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, - "check-types": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz", - "integrity": "sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ==", - "dev": true - }, - "chokidar": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", - "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, - "chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "dev": true - }, - "ci-info": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.1.tgz", - "integrity": "sha512-SXgeMX9VwDe7iFFaEWkA5AstuER9YKqy4EhHqr4DVqkwmD9rpVimkMKWHdjn30Ja45txyjhSn63lVX69eVCckg==", - "dev": true - }, - "cjs-module-lexer": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", - "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", - "dev": true - }, - "classnames": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" - }, - "clean-css": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.0.tgz", - "integrity": "sha512-YYuuxv4H/iNb1Z/5IbMRoxgrzjWGhOEFfd+groZ5dMCVkpENiMZmwspdrzBo9286JjM1gZJPAyL7ZIdzuvu2AQ==", - "dev": true, - "requires": { - "source-map": "~0.6.0" - } - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "cli-spinners": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", - "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", - "dev": true - }, - "cli-truncate": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", - "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", - "dev": true, - "requires": { - "slice-ansi": "^5.0.0", - "string-width": "^5.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true - }, - "ansi-styles": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.0.tgz", - "integrity": "sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==", - "dev": true - }, - "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true - }, - "slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "requires": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - } - }, - "string-width": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.0.1.tgz", - "integrity": "sha512-5ohWO/M4//8lErlUUtrFy3b11GtNOuMOU0ysKCDXFcfXuuvUXu95akgj/i8ofmaGdN0hCqyl6uu9i8dS/mQp5g==", - "dev": true, - "requires": { - "emoji-regex": "^9.2.2", - "is-fullwidth-code-point": "^4.0.0", - "strip-ansi": "^7.0.1" - } - }, - "strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - } - } - }, - "cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true - }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", - "dev": true - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true - }, - "coa": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", - "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", - "dev": true, - "requires": { - "@types/q": "^1.5.1", - "chalk": "^2.4.1", - "q": "^1.1.2" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", - "dev": true - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "colord": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz", - "integrity": "sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ==", - "dev": true - }, - "colorette": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", - "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true - }, - "common-path-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", - "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", - "dev": true - }, - "common-tags": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", - "dev": true - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true - }, - "compare-versions": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-4.1.3.tgz", - "integrity": "sha512-WQfnbDcrYnGr55UwbxKiQKASnTtNnaAWVi8jZyy8NTpVAXWACSne8lMD1iaIo9AiU6mnuLvSVshCzewVuWxHUg==", - "dev": true - }, - "compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "requires": { - "mime-db": ">= 1.43.0 < 2" - } - }, - "compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dev": true, - "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - } - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concurrently": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.5.1.tgz", - "integrity": "sha512-FlSwNpGjWQfRwPLXvJ/OgysbBxPkWpiVjy1042b0U7on7S7qwwMIILRj7WTN1mTgqa582bG6NFuScOoh6Zgdag==", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "date-fns": "^2.16.1", - "lodash": "^4.17.21", - "rxjs": "^6.6.3", - "spawn-command": "^0.0.2-1", - "supports-color": "^8.1.0", - "tree-kill": "^1.2.2", - "yargs": "^16.2.0" - }, - "dependencies": { - "rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "confusing-browser-globals": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", - "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", - "dev": true - }, - "connect-history-api-fallback": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", - "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", - "dev": true - }, - "consola": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", - "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", - "dev": true - }, - "console.table": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/console.table/-/console.table-0.10.0.tgz", - "integrity": "sha1-CRcCVYiHW+/XDPLv9L7yxuLXXQQ=", - "dev": true, - "requires": { - "easy-table": "1.1.0" - } - }, - "content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, - "requires": { - "safe-buffer": "5.2.1" - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "dev": true - }, - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "requires": { - "safe-buffer": "~5.1.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } - } - }, - "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "dev": true - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "dev": true - }, - "core-js": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.14.0.tgz", - "integrity": "sha512-3s+ed8er9ahK+zJpp9ZtuVcDoFzHNiZsPbNAAE4KXgrRHbjSqqNN6xGSXq6bq7TZIbKj4NLrLb6bJ5i+vSVjHA==" - }, - "core-js-compat": { - "version": "3.22.5", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.22.5.tgz", - "integrity": "sha512-rEF75n3QtInrYICvJjrAgV03HwKiYvtKHdPtaba1KucG+cNZ4NJnH9isqt979e67KZlhpbCOTwnsvnIr+CVeOg==", - "dev": true, - "requires": { - "browserslist": "^4.20.3", - "semver": "7.0.0" - }, - "dependencies": { - "browserslist": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", - "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" - } - }, - "electron-to-chromium": { - "version": "1.4.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", - "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", - "dev": true - }, - "node-releases": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", - "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", - "dev": true - }, - "semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "dev": true - } - } - }, - "core-js-pure": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.14.0.tgz", - "integrity": "sha512-YVh+LN2FgNU0odThzm61BsdkwrbrchumFq3oztnE9vTKC4KS2fvnPmcx8t6jnqAyOTCTF4ZSiuK8Qhh7SNcL4g==", - "dev": true - }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, - "cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "dev": true - }, - "css": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", - "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", - "dev": true, - "requires": { - "inherits": "^2.0.4", - "source-map": "^0.6.1", - "source-map-resolve": "^0.6.0" - } - }, - "css-blank-pseudo": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", - "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.9" - } - }, - "css-color-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", - "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=" - }, - "css-declaration-sorter": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.2.2.tgz", - "integrity": "sha512-Ufadglr88ZLsrvS11gjeu/40Lw74D9Am/Jpr3LlYm5Q4ZP5KdlUhG+6u2EjyXeZcxmZ2h1ebCKngDjolpeLHpg==", - "dev": true, - "requires": {} - }, - "css-has-pseudo": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz", - "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.9" - } - }, - "css-loader": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz", - "integrity": "sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==", - "dev": true, - "requires": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.7", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.3.5" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "css-minimizer-webpack-plugin": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz", - "integrity": "sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==", - "dev": true, - "requires": { - "cssnano": "^5.0.6", - "jest-worker": "^27.0.2", - "postcss": "^8.3.5", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1" - }, - "dependencies": { - "ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.3" - } - }, - "schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - } - } - } - }, - "css-prefers-color-scheme": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", - "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", - "dev": true, - "requires": {} - }, - "css-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", - "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", - "dev": true, - "requires": { - "boolbase": "^1.0.0", - "css-what": "^3.2.1", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" - } - }, - "css-select-base-adapter": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", - "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", - "dev": true - }, - "css-to-react-native": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz", - "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==", - "requires": { - "camelize": "^1.0.0", - "css-color-keywords": "^1.0.0", - "postcss-value-parser": "^4.0.2" - } - }, - "css-tree": { - "version": "1.0.0-alpha.37", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", - "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", - "dev": true, - "requires": { - "mdn-data": "2.0.4", - "source-map": "^0.6.1" - } - }, - "css-what": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", - "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", - "dev": true - }, - "css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", - "dev": true - }, - "cssdb": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-6.6.1.tgz", - "integrity": "sha512-0/nZEYfp8SFEzJkMud8NxZJsGfD7RHDJti6GRBLZptIwAzco6RTx1KgwFl4mGWsYS0ZNbCrsY9QryhQ4ldF3Mg==", - "dev": true - }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true - }, - "cssnano": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.7.tgz", - "integrity": "sha512-pVsUV6LcTXif7lvKKW9ZrmX+rGRzxkEdJuVJcp5ftUjWITgwam5LMZOgaTvUrWPkcORBey6he7JKb4XAJvrpKg==", - "dev": true, - "requires": { - "cssnano-preset-default": "^5.2.7", - "lilconfig": "^2.0.3", - "yaml": "^1.10.2" - } - }, - "cssnano-preset-default": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.7.tgz", - "integrity": "sha512-JiKP38ymZQK+zVKevphPzNSGHSlTI+AOwlasoSRtSVMUU285O7/6uZyd5NbW92ZHp41m0sSHe6JoZosakj63uA==", - "dev": true, - "requires": { - "css-declaration-sorter": "^6.2.2", - "cssnano-utils": "^3.1.0", - "postcss-calc": "^8.2.3", - "postcss-colormin": "^5.3.0", - "postcss-convert-values": "^5.1.0", - "postcss-discard-comments": "^5.1.1", - "postcss-discard-duplicates": "^5.1.0", - "postcss-discard-empty": "^5.1.1", - "postcss-discard-overridden": "^5.1.0", - "postcss-merge-longhand": "^5.1.4", - "postcss-merge-rules": "^5.1.1", - "postcss-minify-font-values": "^5.1.0", - "postcss-minify-gradients": "^5.1.1", - "postcss-minify-params": "^5.1.2", - "postcss-minify-selectors": "^5.2.0", - "postcss-normalize-charset": "^5.1.0", - "postcss-normalize-display-values": "^5.1.0", - "postcss-normalize-positions": "^5.1.0", - "postcss-normalize-repeat-style": "^5.1.0", - "postcss-normalize-string": "^5.1.0", - "postcss-normalize-timing-functions": "^5.1.0", - "postcss-normalize-unicode": "^5.1.0", - "postcss-normalize-url": "^5.1.0", - "postcss-normalize-whitespace": "^5.1.1", - "postcss-ordered-values": "^5.1.1", - "postcss-reduce-initial": "^5.1.0", - "postcss-reduce-transforms": "^5.1.0", - "postcss-svgo": "^5.1.0", - "postcss-unique-selectors": "^5.1.1" - } - }, - "cssnano-utils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", - "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", - "dev": true, - "requires": {} - }, - "csso": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", - "dev": true, - "requires": { - "css-tree": "^1.1.2" - }, - "dependencies": { - "css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dev": true, - "requires": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - } - }, - "mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "dev": true - } - } - }, - "cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", - "dev": true - }, - "cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dev": true, - "requires": { - "cssom": "~0.3.6" - }, - "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - } - } - }, - "csstype": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz", - "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==" - }, - "damerau-levenshtein": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz", - "integrity": "sha512-VvdQIPGdWP0SqFXghj79Wf/5LArmreyMsGLa6FG6iC4t3j7j5s71TrwWmT/4akbDQIqjfACkLZmjXhA7g2oUZw==", - "dev": true - }, - "data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", - "dev": true, - "requires": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" - }, - "dependencies": { - "tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", - "dev": true, - "requires": { - "punycode": "^2.1.1" - } - }, - "webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "dev": true - }, - "whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", - "dev": true, - "requires": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - } - } - } - }, - "date-fns": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.23.0.tgz", - "integrity": "sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA==", - "dev": true - }, - "dayjs": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz", - "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "decimal.js": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", - "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" - }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true - }, - "default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", - "dev": true, - "requires": { - "execa": "^5.0.0" - } - }, - "defaults": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", - "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", - "dev": true, - "requires": { - "clone": "^1.0.2" - } - }, - "define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "dev": true - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "requires": { - "object-keys": "^1.0.12" - } - }, - "defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", - "dev": true - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true - }, - "destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true - }, - "detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true - }, - "detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true - }, - "detect-port-alt": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", - "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", - "dev": true, - "requires": { - "address": "^1.0.1", - "debug": "^2.6.0" - } - }, - "detective": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", - "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", - "dev": true, - "requires": { - "acorn-node": "^1.6.1", - "defined": "^1.0.0", - "minimist": "^1.1.1" - } - }, - "didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true - }, - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - }, - "diff-match-patch": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", - "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" - }, - "diff-sequences": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.0.2.tgz", - "integrity": "sha512-YtEoNynLDFCRznv/XDalsKGSZDoj0U5kLnXvY0JSq3nBboRrZXjD81+eSiwi+nzcZDwedMmcowcxNwwgFW23mQ==", - "dev": true, - "peer": true - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true - }, - "dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", - "dev": true - }, - "dns-packet": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.3.1.tgz", - "integrity": "sha512-spBwIj0TK0Ey3666GwIdWVfUpLyubpU53BTCu8iPn4r4oXd9O14Hjg3EHw3ts2oed77/SeckunUYCyRlSngqHw==", - "dev": true, - "requires": { - "@leichtgewicht/ip-codec": "^2.0.1" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "requires": { - "esutils": "^2.0.2" - } - }, - "dom-accessibility-api": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.6.tgz", - "integrity": "sha512-DplGLZd8L1lN64jlT27N9TVSESFR5STaEJvX+thCby7fuCHonfPpAlodYc3vuUYbDuDec5w8AMP7oCM5TWFsqw==", - "dev": true - }, - "dom-converter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", - "dev": true, - "requires": { - "utila": "~0.4" - } - }, - "dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - }, - "dependencies": { - "domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true - } - } - }, - "domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true - }, - "domexception": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", - "dev": true, - "requires": { - "webidl-conversions": "^5.0.0" - }, - "dependencies": { - "webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "dev": true - } - } - }, - "domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dev": true, - "requires": { - "domelementtype": "^2.2.0" - }, - "dependencies": { - "domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true - } - } - }, - "domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dev": true, - "requires": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true - } - } - }, - "dotenv": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz", - "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==", - "dev": true - }, - "dotenv-expand": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", - "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", - "dev": true - }, - "duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true - }, - "easy-table": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.1.0.tgz", - "integrity": "sha1-hvmrTBAvA3G3KXuSplHVgkvIy3M=", - "dev": true, - "requires": { - "wcwidth": ">=1.0.1" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", - "dev": true - }, - "ejs": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.7.tgz", - "integrity": "sha512-BIar7R6abbUxDA3bfXrO4DSgwo8I+fB5/1zgujl3HLLjwd6+9iOnrT+t3grn2qbk9vOgBubXOFwX2m9axoFaGw==", - "dev": true, - "requires": { - "jake": "^10.8.5" - } - }, - "electron-to-chromium": { - "version": "1.3.752", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.752.tgz", - "integrity": "sha512-2Tg+7jSl3oPxgsBsWKh5H83QazTkmWG/cnNwJplmyZc7KcN61+I10oUgaXSVk/NwfvN3BdkKDR4FYuRBQQ2v0A==" - }, - "emittery": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", - "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true - }, - "enhanced-resolve": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz", - "integrity": "sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - } - }, - "enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "requires": { - "ansi-colors": "^4.1.1" - } - }, - "entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "error-stack-parser": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.7.tgz", - "integrity": "sha512-chLOW0ZGRf4s8raLrDxa5sdkvPec5YdvwbFnqJme4rk0rFajP8mPtrDL1+I+CwrQDCjswDA5sREX7jYQDQs9vA==", - "dev": true, - "requires": { - "stackframe": "^1.1.1" - } - }, - "es-abstract": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", - "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "is-callable": "^1.2.3", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.10.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - } - }, - "es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", - "dev": true - }, - "es-shim-unscopables": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", - "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", - "requires": { - "has": "^1.0.3" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "escodegen": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", - "dev": true, - "requires": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - } - } - }, - "eslint": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.15.0.tgz", - "integrity": "sha512-GG5USZ1jhCu8HJkzGgeK8/+RGnHaNYZGrGDzUtigK3BsGESW/rs2az23XqE0WVwDxy1VRvvjSSGu5nB0Bu+6SA==", - "requires": { - "@eslint/eslintrc": "^1.2.3", - "@humanwhocodes/config-array": "^0.9.2", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.2", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^6.0.1", - "globals": "^13.6.0", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "regexpp": "^3.2.0", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" - }, - "eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==" - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "requires": { - "is-glob": "^4.0.3" - } - }, - "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==" - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "eslint-config-airbnb": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", - "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", - "dev": true, - "requires": { - "eslint-config-airbnb-base": "^15.0.0", - "object.assign": "^4.1.2", - "object.entries": "^1.1.5" - }, - "dependencies": { - "es-abstract": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", - "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", - "is-string": "^1.0.7", - "is-weakref": "^1.0.1", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - } - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true - }, - "object.entries": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", - "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - } - } - } - }, - "eslint-config-airbnb-base": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", - "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", - "dev": true, - "requires": { - "confusing-browser-globals": "^1.0.10", - "object.assign": "^4.1.2", - "object.entries": "^1.1.5", - "semver": "^6.3.0" - }, - "dependencies": { - "es-abstract": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", - "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", - "is-string": "^1.0.7", - "is-weakref": "^1.0.1", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - } - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true - }, - "object.entries": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", - "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "eslint-config-airbnb-typescript": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-17.0.0.tgz", - "integrity": "sha512-elNiuzD0kPAPTXjFWg+lE24nMdHMtuxgYoD30OyMD6yrW1AhFZPAg27VX7d3tzOErw+dgJTNWfRSDqEcXb4V0g==", - "dev": true, - "requires": { - "eslint-config-airbnb-base": "^15.0.0" - } - }, - "eslint-config-prettier": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", - "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", - "dev": true, - "requires": {} - }, - "eslint-config-react-app": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", - "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==", - "dev": true, - "requires": { - "@babel/core": "^7.16.0", - "@babel/eslint-parser": "^7.16.3", - "@rushstack/eslint-patch": "^1.1.0", - "@typescript-eslint/eslint-plugin": "^5.5.0", - "@typescript-eslint/parser": "^5.5.0", - "babel-preset-react-app": "^10.0.1", - "confusing-browser-globals": "^1.0.11", - "eslint-plugin-flowtype": "^8.0.3", - "eslint-plugin-import": "^2.25.3", - "eslint-plugin-jest": "^25.3.0", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-react": "^7.27.1", - "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-testing-library": "^5.0.1" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/compat-data": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz", - "integrity": "sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw==", - "dev": true - }, - "@babel/core": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.10.tgz", - "integrity": "sha512-liKoppandF3ZcBnIYFjfSDHZLKdLHGJRkoWtG8zQyGJBQfIYobpnVGI5+pLBNtS6psFLDzyq8+h5HiVljW9PNA==", - "dev": true, - "requires": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-compilation-targets": "^7.17.10", - "@babel/helper-module-transforms": "^7.17.7", - "@babel/helpers": "^7.17.9", - "@babel/parser": "^7.17.10", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.10", - "@babel/types": "^7.17.10", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" - } - }, - "@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "requires": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz", - "integrity": "sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.17.10", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.20.2", - "semver": "^6.3.0" - } - }, - "@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-transforms": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", - "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-simple-access": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", - "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true - }, - "@babel/helpers": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.9.tgz", - "integrity": "sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q==", - "dev": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.9", - "@babel/types": "^7.17.0" - } - }, - "@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "browserslist": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", - "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "electron-to-chromium": { - "version": "1.4.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", - "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", - "dev": true - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node-releases": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", - "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "eslint-import-resolver-node": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", - "requires": { - "debug": "^3.2.7", - "resolve": "^1.20.0" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } - } - }, - "eslint-import-resolver-typescript": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz", - "integrity": "sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ==", - "requires": { - "debug": "^4.3.4", - "glob": "^7.2.0", - "is-glob": "^4.0.3", - "resolve": "^1.22.0", - "tsconfig-paths": "^3.14.1" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", - "requires": { - "has": "^1.0.3" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", - "requires": { - "is-core-module": "^2.8.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "tsconfig-paths": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", - "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", - "requires": { - "@types/json5": "^0.0.29", - "json5": "^1.0.1", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - } - } - }, - "eslint-module-utils": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", - "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", - "requires": { - "debug": "^3.2.7", - "find-up": "^2.1.0" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } - } - }, - "eslint-plugin-flowtype": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz", - "integrity": "sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==", - "dev": true, - "requires": { - "lodash": "^4.17.21", - "string-natural-compare": "^3.0.1" - } - }, - "eslint-plugin-import": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", - "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", - "requires": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.3", - "has": "^1.0.3", - "is-core-module": "^2.8.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.values": "^1.1.5", - "resolve": "^1.22.0", - "tsconfig-paths": "^3.14.1" - }, - "dependencies": { - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "requires": { - "esutils": "^2.0.2" - } - }, - "is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", - "requires": { - "has": "^1.0.3" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", - "requires": { - "is-core-module": "^2.8.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - } - } - }, - "eslint-plugin-jest": { - "version": "25.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz", - "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==", - "dev": true, - "requires": { - "@typescript-eslint/experimental-utils": "^5.0.0" - } - }, - "eslint-plugin-jest-dom": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-4.0.2.tgz", - "integrity": "sha512-Jo51Atwyo2TdcUncjmU+UQeSTKh3sc2LF/M5i/R3nTU0Djw9V65KGJisdm/RtuKhy2KH/r7eQ1n6kwYFPNdHlA==", - "dev": true, - "requires": { - "@babel/runtime": "^7.16.3", - "@testing-library/dom": "^8.11.1", - "requireindex": "^1.2.0" - }, - "dependencies": { - "@babel/runtime": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz", - "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } - } - } - }, - "eslint-plugin-jsx-a11y": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.5.1.tgz", - "integrity": "sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g==", - "dev": true, - "requires": { - "@babel/runtime": "^7.16.3", - "aria-query": "^4.2.2", - "array-includes": "^3.1.4", - "ast-types-flow": "^0.0.7", - "axe-core": "^4.3.5", - "axobject-query": "^2.2.0", - "damerau-levenshtein": "^1.0.7", - "emoji-regex": "^9.2.2", - "has": "^1.0.3", - "jsx-ast-utils": "^3.2.1", - "language-tags": "^1.0.5", - "minimatch": "^3.0.4" - }, - "dependencies": { - "@babel/runtime": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz", - "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "array-includes": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", - "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", - "is-string": "^1.0.7" - } - }, - "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "es-abstract": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", - "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", - "is-string": "^1.0.7", - "is-weakref": "^1.0.1", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - } - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "jsx-ast-utils": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz", - "integrity": "sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA==", - "dev": true, - "requires": { - "array-includes": "^3.1.3", - "object.assign": "^4.1.2" - } - }, - "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true - } - } - }, - "eslint-plugin-prettier": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz", - "integrity": "sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ==", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-plugin-react": { - "version": "7.29.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.29.4.tgz", - "integrity": "sha512-CVCXajliVh509PcZYRFyu/BoUEz452+jtQJq2b3Bae4v3xBUWPLCmtmBM+ZinG4MzwmxJgJ2M5rMqhqLVn7MtQ==", - "dev": true, - "requires": { - "array-includes": "^3.1.4", - "array.prototype.flatmap": "^1.2.5", - "doctrine": "^2.1.0", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.5", - "object.fromentries": "^2.0.5", - "object.hasown": "^1.1.0", - "object.values": "^1.1.5", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.3", - "semver": "^6.3.0", - "string.prototype.matchall": "^4.0.6" - }, - "dependencies": { - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "resolve": { - "version": "2.0.0-next.3", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", - "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", - "dev": true, - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } - } - } - }, - "eslint-plugin-react-hooks": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.5.0.tgz", - "integrity": "sha512-8k1gRt7D7h03kd+SAAlzXkQwWK22BnK6GKZG+FJA6BAGy22CFvl8kCIXKpVux0cCxMWDQUPqSok0LKaZ0aOcCw==", - "dev": true, - "requires": {} - }, - "eslint-plugin-testing-library": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.5.0.tgz", - "integrity": "sha512-eWQ19l6uWL7LW8oeMyQVSGjVYFnBqk7DMHjadm0yOHBvX3Xi9OBrsNuxoAMdX4r7wlQ5WWpW46d+CB6FWFL/PQ==", - "dev": true, - "requires": { - "@typescript-eslint/utils": "^5.13.0" - }, - "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.23.0.tgz", - "integrity": "sha512-EhjaFELQHCRb5wTwlGsNMvzK9b8Oco4aYNleeDlNuL6qXWDF47ch4EhVNPh8Rdhf9tmqbN4sWDk/8g+Z/J8JVw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/visitor-keys": "5.23.0" - } - }, - "@typescript-eslint/types": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.23.0.tgz", - "integrity": "sha512-NfBsV/h4dir/8mJwdZz7JFibaKC3E/QdeMEDJhiAE3/eMkoniZ7MjbEMCGXw6MZnZDMN3G9S0mH/6WUIj91dmw==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.23.0.tgz", - "integrity": "sha512-xE9e0lrHhI647SlGMl+m+3E3CKPF1wzvvOEWnuE3CCjjT7UiRnDGJxmAcVKJIlFgK6DY9RB98eLr1OPigPEOGg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/visitor-keys": "5.23.0", - "debug": "^4.3.2", - "globby": "^11.0.4", - "is-glob": "^4.0.3", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.23.0.tgz", - "integrity": "sha512-dbgaKN21drqpkbbedGMNPCtRPZo1IOUr5EI9Jrrh99r5UW5Q0dz46RKXeSBoPV+56R6dFKpbrdhgUNSJsDDRZA==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.23.0", - "@typescript-eslint/types": "5.23.0", - "@typescript-eslint/typescript-estree": "5.23.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.23.0.tgz", - "integrity": "sha512-Vd4mFNchU62sJB8pX19ZSPog05B0Y0CE2UxAZPT5k4iqhRYjPnqyY3woMxCd0++t9OTqkgjST+1ydLBi7e2Fvg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.23.0", - "eslint-visitor-keys": "^3.0.0" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "requires": { - "eslint-visitor-keys": "^2.0.0" - } - }, - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==" - }, - "eslint-webpack-plugin": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.1.1.tgz", - "integrity": "sha512-xSucskTN9tOkfW7so4EaiFIkulWLXwCB/15H917lR6pTv0Zot6/fetFucmENRb7J5whVSFKIvwnrnsa78SG2yg==", - "dev": true, - "requires": { - "@types/eslint": "^7.28.2", - "jest-worker": "^27.3.1", - "micromatch": "^4.0.4", - "normalize-path": "^3.0.0", - "schema-utils": "^3.1.1" - } - }, - "espree": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", - "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", - "requires": { - "acorn": "^8.7.1", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==" - } - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" - } - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "estree-walker": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", - "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "dev": true - }, - "eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true - }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true - }, - "execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true - }, - "expect": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.0.tgz", - "integrity": "sha512-qFXKl8Pmxk8TBGfaFKRtcQjfXEnKAs+dmlxdwvukJZorwrAabT7M3h8oLOG01I2utEhkmUTi17CHaPBovZsKdw==", - "dev": true, - "peer": true, - "requires": { - "@jest/expect-utils": "^28.1.0", - "jest-get-type": "^28.0.2", - "jest-matcher-utils": "^28.1.0", - "jest-message-util": "^28.1.0", - "jest-util": "^28.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true - }, - "jest-matcher-utils": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.0.tgz", - "integrity": "sha512-onnax0n2uTLRQFKAjC7TuaxibrPSvZgKTcSCnNUz/tOjJ9UhxNm7ZmPpoQavmTDUjXvUQ8KesWk2/VdrxIFzTQ==", - "dev": true, - "peer": true, - "requires": { - "chalk": "^4.0.0", - "jest-diff": "^28.1.0", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.0" - } - }, - "pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "requires": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - } - }, - "react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - } - } - }, - "express": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", - "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", - "dev": true, - "requires": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.0", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.10.3", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true - } - } - }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, - "fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" - }, - "fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true - }, - "fastq": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", - "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - }, - "fb-watchman": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", - "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", - "dev": true, - "requires": { - "bser": "2.1.1" - } - }, - "fetch-mock": { - "version": "9.11.0", - "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.11.0.tgz", - "integrity": "sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==", - "requires": { - "@babel/core": "^7.0.0", - "@babel/runtime": "^7.0.0", - "core-js": "^3.0.0", - "debug": "^4.1.1", - "glob-to-regexp": "^0.4.0", - "is-subset": "^0.1.1", - "lodash.isequal": "^4.5.0", - "path-to-regexp": "^2.2.1", - "querystring": "^0.2.0", - "whatwg-url": "^6.5.0" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "path-to-regexp": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz", - "integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==" - } - } - }, - "fetch-mock-jest": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/fetch-mock-jest/-/fetch-mock-jest-1.5.1.tgz", - "integrity": "sha512-+utwzP8C+Pax1GSka3nFXILWMY3Er2L+s090FOgqVNrNCPp0fDqgXnAHAJf12PLHi0z4PhcTaZNTz8e7K3fjqQ==", - "dev": true, - "requires": { - "fetch-mock": "^9.11.0" - } - }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "requires": { - "flat-cache": "^3.0.4" - } - }, - "file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", - "dev": true, - "requires": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - } - }, - "filelist": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.3.tgz", - "integrity": "sha512-LwjCsruLWQULGYKy7TX0OPtrL9kLpojOFKc5VCTxdFTV7w5zbsgqVKfnkKG7Qgjtq50gKfO56hJv88OfcGb70Q==", - "dev": true, - "requires": { - "minimatch": "^5.0.1" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } - } - }, - "filesize": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", - "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", - "dev": true - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dev": true, - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - } - }, - "find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "requires": { - "locate-path": "^2.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", - "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==" - }, - "follow-redirects": { - "version": "1.14.8", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", - "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==", - "dev": true - }, - "fork-ts-checker-webpack-plugin": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.2.tgz", - "integrity": "sha512-m5cUmF30xkZ7h4tWUgTAcEaKmUW7tfyUyTqNNOz7OxWJ0v1VWKTcOvH8FWHUwSjlW/356Ijc9vi3XfcPstpQKA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "chokidar": "^3.4.2", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "glob": "^7.1.6", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - } - }, - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - } - }, - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "dev": true - } - } - }, - "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "format-util": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/format-util/-/format-util-1.0.5.tgz", - "integrity": "sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==" - }, - "forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true - }, - "fraction.js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", - "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", - "dev": true - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "dev": true - }, - "fs-extra": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", - "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "fs-monkey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - }, - "dependencies": { - "es-abstract": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.0.tgz", - "integrity": "sha512-URbD8tgRthKD3YcC39vbvSDrX23upXnPcnGAjQfgxXF5ID75YcENawc9ZX/9iTP9ptUyfCLIxTTuMYoRfiOVKA==", - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.1", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - } - }, - "has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" - }, - "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "requires": { - "call-bind": "^1.0.2" - } - }, - "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==" - }, - "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - } - }, - "string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "dependencies": { - "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - } - } - }, - "string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "dependencies": { - "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - } - } - }, - "unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "requires": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - } - } - } - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" - }, - "functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" - }, - "gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - }, - "get-own-enumerable-property-symbols": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", - "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", - "dev": true - }, - "get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true - }, - "get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true - }, - "get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - } - }, - "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "requires": { - "is-glob": "^4.0.1" - } - }, - "glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" - }, - "global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "dev": true, - "requires": { - "global-prefix": "^3.0.0" - } - }, - "global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "dev": true, - "requires": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - }, - "dependencies": { - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "globals": { - "version": "13.14.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.14.0.tgz", - "integrity": "sha512-ERO68sOYwm5UuLvSJTY7w7NP2c8S4UcXs3X1GBX8cwOr+ShOcDBbCY5mH4zxz0jsYCdJ8ve8Mv9n2YGJMB1aeg==", - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "dependencies": { - "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true - } - } - }, - "graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true - }, - "gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "dev": true, - "requires": { - "duplexer": "^0.1.2" - } - }, - "handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true - }, - "harmony-reflect": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", - "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "requires": { - "get-intrinsic": "^1.1.1" - } - }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" - }, - "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "requires": { - "has-symbols": "^1.0.2" - } - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true - }, - "history": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", - "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", - "requires": { - "@babel/runtime": "^7.7.6" - } - }, - "hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "requires": { - "react-is": "^16.7.0" - } - }, - "hoopy": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", - "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", - "dev": true - }, - "hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "html-encoding-sniffer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", - "dev": true, - "requires": { - "whatwg-encoding": "^1.0.5" - } - }, - "html-entities": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", - "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", - "dev": true - }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", - "dev": true, - "requires": { - "camel-case": "^4.1.2", - "clean-css": "^5.2.2", - "commander": "^8.3.0", - "he": "^1.2.0", - "param-case": "^3.0.4", - "relateurl": "^0.2.7", - "terser": "^5.10.0" - } - }, - "html-webpack-plugin": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", - "integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==", - "dev": true, - "requires": { - "@types/html-minifier-terser": "^6.0.0", - "html-minifier-terser": "^6.0.2", - "lodash": "^4.17.21", - "pretty-error": "^4.0.0", - "tapable": "^2.0.0" - } - }, - "htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - }, - "dependencies": { - "dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - } - }, - "domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true - }, - "domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - } - } - } - }, - "http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", - "dev": true - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "http-parser-js": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.6.tgz", - "integrity": "sha512-vDlkRPDJn93swjcjqMSaGSPABbIarsr1TLAui/gLDXzV5VsJNdXNzMYDyNBLQkjWQCJ1uizu8T2oDMhmGt0PRA==", - "dev": true - }, - "http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, - "requires": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - } - }, - "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dev": true, - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", - "dev": true, - "requires": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - } - }, - "https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "requires": { - "agent-base": "6", - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true - }, - "husky": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/husky/-/husky-7.0.1.tgz", - "integrity": "sha512-gceRaITVZ+cJH9sNHqx5tFwbzlLCVxtVZcusME8JYQ8Edy5mpGDOqD8QBCdMhpyo9a+JXddnujQ4rpY2Ff9SJA==", - "dev": true - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "requires": {} - }, - "idb": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/idb/-/idb-6.1.5.tgz", - "integrity": "sha512-IJtugpKkiVXQn5Y+LteyBCNk1N8xpGV3wWZk9EVtZWH8DYkjBn0bX1XnGP9RkyZF0sAcywa6unHqSWKe7q4LGw==", - "dev": true - }, - "identity-obj-proxy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", - "integrity": "sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ=", - "dev": true, - "requires": { - "harmony-reflect": "^1.4.6" - } - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true - }, - "ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", - "dev": true - }, - "immer": { - "version": "9.0.12", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz", - "integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==" - }, - "immutable": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz", - "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==" - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dev": true, - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "inquirer": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.2.tgz", - "integrity": "sha512-pG7I/si6K/0X7p1qU+rfWnpTE1UIkTONN1wxtzh0d+dHXtT/JG6qBgLxoyHVsQa8cFABxAPh0pD6uUUHiAoaow==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.1", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.21", - "mute-stream": "0.0.8", - "ora": "^5.4.1", - "run-async": "^2.4.0", - "rxjs": "^7.5.5", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "rxjs": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz", - "integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==", - "dev": true, - "requires": { - "tslib": "^2.1.0" - } - }, - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true - } - } - }, - "internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "requires": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - } - }, - "ipaddr.js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", - "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", - "dev": true - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-bigint": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", - "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==" - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-boolean-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", - "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-callable": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", - "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==" - }, - "is-core-module": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", - "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", - "requires": { - "has": "^1.0.3" - } - }, - "is-date-object": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", - "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==" - }, - "is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true - }, - "is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", - "dev": true - }, - "is-negative-zero": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", - "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "is-number-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", - "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==" - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true - }, - "is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "dev": true - }, - "is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true - }, - "is-regex": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", - "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-symbols": "^1.0.2" - } - }, - "is-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=", - "dev": true - }, - "is-root": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", - "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", - "dev": true - }, - "is-shared-array-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", - "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", - "dev": true - }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true - }, - "is-string": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", - "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==" - }, - "is-subset": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", - "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=" - }, - "is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "requires": { - "has-symbols": "^1.0.2" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true - }, - "is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true - }, - "is-weakref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.1.tgz", - "integrity": "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.0" - } - }, - "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "requires": { - "is-docker": "^2.0.0" - } - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz", - "integrity": "sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A==", - "dev": true, - "requires": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - } - }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "istanbul-reports": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", - "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "iterare": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", - "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", - "dev": true - }, - "jake": { - "version": "10.8.5", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", - "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", - "dev": true, - "requires": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.1", - "minimatch": "^3.0.4" - } - }, - "jest": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-28.1.0.tgz", - "integrity": "sha512-TZR+tHxopPhzw3c3560IJXZWLNHgpcz1Zh0w5A65vynLGNcg/5pZ+VildAd7+XGOu6jd58XMY/HNn0IkZIXVXg==", - "dev": true, - "peer": true, - "requires": { - "@jest/core": "^28.1.0", - "import-local": "^3.0.2", - "jest-cli": "^28.1.0" - } - }, - "jest-changed-files": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-28.0.2.tgz", - "integrity": "sha512-QX9u+5I2s54ZnGoMEjiM2WeBvJR2J7w/8ZUmH2um/WLAuGAYFQcsVXY9+1YL6k0H/AGUdH8pXUAv6erDqEsvIA==", - "dev": true, - "peer": true, - "requires": { - "execa": "^5.0.0", - "throat": "^6.0.1" - } - }, - "jest-circus": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-28.1.0.tgz", - "integrity": "sha512-rNYfqfLC0L0zQKRKsg4n4J+W1A2fbyGH7Ss/kDIocp9KXD9iaL111glsLu7+Z7FHuZxwzInMDXq+N1ZIBkI/TQ==", - "dev": true, - "peer": true, - "requires": { - "@jest/environment": "^28.1.0", - "@jest/expect": "^28.1.0", - "@jest/test-result": "^28.1.0", - "@jest/types": "^28.1.0", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^28.1.0", - "jest-matcher-utils": "^28.1.0", - "jest-message-util": "^28.1.0", - "jest-runtime": "^28.1.0", - "jest-snapshot": "^28.1.0", - "jest-util": "^28.1.0", - "pretty-format": "^28.1.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3", - "throat": "^6.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true - }, - "jest-matcher-utils": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.0.tgz", - "integrity": "sha512-onnax0n2uTLRQFKAjC7TuaxibrPSvZgKTcSCnNUz/tOjJ9UhxNm7ZmPpoQavmTDUjXvUQ8KesWk2/VdrxIFzTQ==", - "dev": true, - "peer": true, - "requires": { - "chalk": "^4.0.0", - "jest-diff": "^28.1.0", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.0" - } - }, - "pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "requires": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - } - }, - "react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - } - } - }, - "jest-cli": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-28.1.0.tgz", - "integrity": "sha512-fDJRt6WPRriHrBsvvgb93OxgajHHsJbk4jZxiPqmZbMDRcHskfJBBfTyjFko0jjfprP544hOktdSi9HVgl4VUQ==", - "dev": true, - "peer": true, - "requires": { - "@jest/core": "^28.1.0", - "@jest/test-result": "^28.1.0", - "@jest/types": "^28.1.0", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "import-local": "^3.0.2", - "jest-config": "^28.1.0", - "jest-util": "^28.1.0", - "jest-validate": "^28.1.0", - "prompts": "^2.0.1", - "yargs": "^17.3.1" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true - }, - "jest-validate": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.0.tgz", - "integrity": "sha512-Lly7CJYih3vQBfjLeANGgBSBJ7pEa18cxpQfQEq2go2xyEzehnHfQTjoUia8xUv4x4J80XKFIDwJJThXtRFQXQ==", - "dev": true, - "peer": true, - "requires": { - "@jest/types": "^28.1.0", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^28.0.2", - "leven": "^3.1.0", - "pretty-format": "^28.1.0" - } - }, - "pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "requires": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - } - }, - "react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - }, - "yargs": { - "version": "17.5.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", - "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", - "dev": true, - "peer": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.0.0" - } - }, - "yargs-parser": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", - "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", - "dev": true, - "peer": true - } - } - }, - "jest-config": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-28.1.0.tgz", - "integrity": "sha512-aOV80E9LeWrmflp7hfZNn/zGA4QKv/xsn2w8QCBP0t0+YqObuCWTSgNbHJ0j9YsTuCO08ZR/wsvlxqqHX20iUA==", - "dev": true, - "peer": true, - "requires": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^28.1.0", - "@jest/types": "^28.1.0", - "babel-jest": "^28.1.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^28.1.0", - "jest-environment-node": "^28.1.0", - "jest-get-type": "^28.0.2", - "jest-regex-util": "^28.0.2", - "jest-resolve": "^28.1.0", - "jest-runner": "^28.1.0", - "jest-util": "^28.1.0", - "jest-validate": "^28.1.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^28.1.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true - }, - "jest-haste-map": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.0.tgz", - "integrity": "sha512-xyZ9sXV8PtKi6NCrJlmq53PyNVHzxmcfXNVvIRHpHmh1j/HChC4pwKgyjj7Z9us19JMw8PpQTJsFWOsIfT93Dw==", - "dev": true, - "peer": true, - "requires": { - "@jest/types": "^28.1.0", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.3.2", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.0", - "jest-worker": "^28.1.0", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - } - }, - "jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "dev": true, - "peer": true - }, - "jest-resolve": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-28.1.0.tgz", - "integrity": "sha512-vvfN7+tPNnnhDvISuzD1P+CRVP8cK0FHXRwPAcdDaQv4zgvwvag2n55/h5VjYcM5UJG7L4TwE5tZlzcI0X2Lhw==", - "dev": true, - "peer": true, - "requires": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^28.1.0", - "jest-validate": "^28.1.0", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - } - }, - "jest-validate": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.0.tgz", - "integrity": "sha512-Lly7CJYih3vQBfjLeANGgBSBJ7pEa18cxpQfQEq2go2xyEzehnHfQTjoUia8xUv4x4J80XKFIDwJJThXtRFQXQ==", - "dev": true, - "peer": true, - "requires": { - "@jest/types": "^28.1.0", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^28.0.2", - "leven": "^3.1.0", - "pretty-format": "^28.1.0" - } - }, - "jest-worker": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.0.tgz", - "integrity": "sha512-ZHwM6mNwaWBR52Snff8ZvsCTqQsvhCxP/bT1I6T6DAnb6ygkshsyLQIMxFwHpYxht0HOoqt23JlC01viI7T03A==", - "dev": true, - "peer": true, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - } - }, - "pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "requires": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - } - }, - "react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-diff": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.0.tgz", - "integrity": "sha512-8eFd3U3OkIKRtlasXfiAQfbovgFgRDb0Ngcs2E+FMeBZ4rUezqIaGjuyggJBp+llosQXNEWofk/Sz4Hr5gMUhA==", - "dev": true, - "peer": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^28.0.2", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true - }, - "pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "requires": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - } - }, - "react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - } - } - }, - "jest-docblock": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-28.0.2.tgz", - "integrity": "sha512-FH10WWw5NxLoeSdQlJwu+MTiv60aXV/t8KEwIRGEv74WARE1cXIqh1vGdy2CraHuWOOrnzTWj/azQKqW4fO7xg==", - "dev": true, - "peer": true, - "requires": { - "detect-newline": "^3.0.0" - } - }, - "jest-each": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-28.1.0.tgz", - "integrity": "sha512-a/XX02xF5NTspceMpHujmOexvJ4GftpYXqr6HhhmKmExtMXsyIN/fvanQlt/BcgFoRKN4OCXxLQKth9/n6OPFg==", - "dev": true, - "peer": true, - "requires": { - "@jest/types": "^28.1.0", - "chalk": "^4.0.0", - "jest-get-type": "^28.0.2", - "jest-util": "^28.1.0", - "pretty-format": "^28.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true - }, - "pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "requires": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - } - }, - "react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - } - } - }, - "jest-environment-jsdom": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", - "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", - "dev": true, - "requires": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1", - "jsdom": "^16.6.0" - }, - "dependencies": { - "@jest/environment": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", - "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", - "dev": true, - "requires": { - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1" - } - }, - "@jest/fake-timers": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", - "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@sinonjs/fake-timers": "^8.0.1", - "@types/node": "*", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" - } - }, - "@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "@sinonjs/fake-timers": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", - "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, - "@types/yargs": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", - "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "jest-message-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", - "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^27.5.1", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - } - }, - "jest-mock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", - "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@types/node": "*" - } - }, - "jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - } - } - } - }, - "jest-environment-node": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-28.1.0.tgz", - "integrity": "sha512-gBLZNiyrPw9CSMlTXF1yJhaBgWDPVvH0Pq6bOEwGMXaYNzhzhw2kA/OijNF8egbCgDS0/veRv97249x2CX+udQ==", - "dev": true, - "peer": true, - "requires": { - "@jest/environment": "^28.1.0", - "@jest/fake-timers": "^28.1.0", - "@jest/types": "^28.1.0", - "@types/node": "*", - "jest-mock": "^28.1.0", - "jest-util": "^28.1.0" - } - }, - "jest-get-type": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", - "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", - "dev": true, - "peer": true - }, - "jest-haste-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", - "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@types/graceful-fs": "^4.1.2", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.3.2", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^27.5.1", - "jest-serializer": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - }, - "dependencies": { - "@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", - "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - } - } - } - }, - "jest-jasmine2": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz", - "integrity": "sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==", - "dev": true, - "requires": { - "@jest/environment": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "expect": "^27.5.1", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1", - "throat": "^6.0.1" - }, - "dependencies": { - "@jest/console": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", - "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0" - } - }, - "@jest/environment": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", - "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", - "dev": true, - "requires": { - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1" - } - }, - "@jest/fake-timers": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", - "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@sinonjs/fake-timers": "^8.0.1", - "@types/node": "*", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" - } - }, - "@jest/globals": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz", - "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==", - "dev": true, - "requires": { - "@jest/environment": "^27.5.1", - "@jest/types": "^27.5.1", - "expect": "^27.5.1" - } - }, - "@jest/source-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", - "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==", - "dev": true, - "requires": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9", - "source-map": "^0.6.0" - } - }, - "@jest/test-result": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz", - "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==", - "dev": true, - "requires": { - "@jest/console": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - } - }, - "@jest/transform": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", - "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^27.5.1", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-util": "^27.5.1", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - } - }, - "@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "@sinonjs/fake-timers": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", - "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, - "@types/yargs": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", - "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", - "dev": true - }, - "expect": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", - "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1" - } - }, - "jest-diff": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", - "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - } - }, - "jest-each": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz", - "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1" - } - }, - "jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "dev": true - }, - "jest-message-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", - "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^27.5.1", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - } - }, - "jest-mock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", - "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@types/node": "*" - } - }, - "jest-runtime": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", - "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==", - "dev": true, - "requires": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/globals": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "execa": "^5.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - } - }, - "jest-snapshot": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", - "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", - "dev": true, - "requires": { - "@babel/core": "^7.7.2", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.0.0", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__traverse": "^7.0.4", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "natural-compare": "^1.4.0", - "pretty-format": "^27.5.1", - "semver": "^7.3.2" - } - }, - "jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - } - }, - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - } - } - }, - "jest-leak-detector": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-28.1.0.tgz", - "integrity": "sha512-uIJDQbxwEL2AMMs2xjhZl2hw8s77c3wrPaQ9v6tXJLGaaQ+4QrNJH5vuw7hA7w/uGT/iJ42a83opAqxGHeyRIA==", - "dev": true, - "peer": true, - "requires": { - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true - }, - "pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "requires": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - } - }, - "react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - } - } - }, - "jest-matcher-utils": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", - "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "dependencies": { - "diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", - "dev": true - }, - "jest-diff": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", - "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - } - }, - "jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "dev": true - } - } - }, - "jest-message-util": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.0.tgz", - "integrity": "sha512-RpA8mpaJ/B2HphDMiDlrAZdDytkmwFqgjDZovM21F35lHGeUeCvYmm6W+sbQ0ydaLpg5bFAUuWG1cjqOl8vqrw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^28.1.0", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^28.1.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "requires": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - } - }, - "react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true - } - } - }, - "jest-mock": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-28.1.0.tgz", - "integrity": "sha512-H7BrhggNn77WhdL7O1apG0Q/iwl0Bdd5E1ydhCJzL3oBLh/UYxAwR3EJLsBZ9XA3ZU4PA3UNw4tQjduBTCTmLw==", - "dev": true, - "peer": true, - "requires": { - "@jest/types": "^28.1.0", - "@types/node": "*" - } - }, - "jest-pnp-resolver": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", - "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true, - "requires": {} - }, - "jest-regex-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", - "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", - "dev": true - }, - "jest-resolve": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz", - "integrity": "sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - }, - "dependencies": { - "@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", - "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - } - } - } - }, - "jest-resolve-dependencies": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.0.tgz", - "integrity": "sha512-Ue1VYoSZquPwEvng7Uefw8RmZR+me/1kr30H2jMINjGeHgeO/JgrR6wxj2ofkJ7KSAA11W3cOrhNCbj5Dqqd9g==", - "dev": true, - "peer": true, - "requires": { - "jest-regex-util": "^28.0.2", - "jest-snapshot": "^28.1.0" - }, - "dependencies": { - "jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "dev": true, - "peer": true - } - } - }, - "jest-runner": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-28.1.0.tgz", - "integrity": "sha512-FBpmuh1HB2dsLklAlRdOxNTTHKFR6G1Qmd80pVDvwbZXTriqjWqjei5DKFC1UlM732KjYcE6yuCdiF0WUCOS2w==", - "dev": true, - "peer": true, - "requires": { - "@jest/console": "^28.1.0", - "@jest/environment": "^28.1.0", - "@jest/test-result": "^28.1.0", - "@jest/transform": "^28.1.0", - "@jest/types": "^28.1.0", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.10.2", - "graceful-fs": "^4.2.9", - "jest-docblock": "^28.0.2", - "jest-environment-node": "^28.1.0", - "jest-haste-map": "^28.1.0", - "jest-leak-detector": "^28.1.0", - "jest-message-util": "^28.1.0", - "jest-resolve": "^28.1.0", - "jest-runtime": "^28.1.0", - "jest-util": "^28.1.0", - "jest-watcher": "^28.1.0", - "jest-worker": "^28.1.0", - "source-map-support": "0.5.13", - "throat": "^6.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true - }, - "jest-haste-map": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.0.tgz", - "integrity": "sha512-xyZ9sXV8PtKi6NCrJlmq53PyNVHzxmcfXNVvIRHpHmh1j/HChC4pwKgyjj7Z9us19JMw8PpQTJsFWOsIfT93Dw==", - "dev": true, - "peer": true, - "requires": { - "@jest/types": "^28.1.0", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.3.2", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.0", - "jest-worker": "^28.1.0", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - } - }, - "jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "dev": true, - "peer": true - }, - "jest-resolve": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-28.1.0.tgz", - "integrity": "sha512-vvfN7+tPNnnhDvISuzD1P+CRVP8cK0FHXRwPAcdDaQv4zgvwvag2n55/h5VjYcM5UJG7L4TwE5tZlzcI0X2Lhw==", - "dev": true, - "peer": true, - "requires": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^28.1.0", - "jest-validate": "^28.1.0", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - } - }, - "jest-validate": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.0.tgz", - "integrity": "sha512-Lly7CJYih3vQBfjLeANGgBSBJ7pEa18cxpQfQEq2go2xyEzehnHfQTjoUia8xUv4x4J80XKFIDwJJThXtRFQXQ==", - "dev": true, - "peer": true, - "requires": { - "@jest/types": "^28.1.0", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^28.0.2", - "leven": "^3.1.0", - "pretty-format": "^28.1.0" - } - }, - "jest-worker": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.0.tgz", - "integrity": "sha512-ZHwM6mNwaWBR52Snff8ZvsCTqQsvhCxP/bT1I6T6DAnb6ygkshsyLQIMxFwHpYxht0HOoqt23JlC01viI7T03A==", - "dev": true, - "peer": true, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - } - }, - "pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "requires": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - } - }, - "react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - }, - "source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "peer": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-runtime": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-28.1.0.tgz", - "integrity": "sha512-wNYDiwhdH/TV3agaIyVF0lsJ33MhyujOe+lNTUiolqKt8pchy1Hq4+tDMGbtD5P/oNLA3zYrpx73T9dMTOCAcg==", - "dev": true, - "peer": true, - "requires": { - "@jest/environment": "^28.1.0", - "@jest/fake-timers": "^28.1.0", - "@jest/globals": "^28.1.0", - "@jest/source-map": "^28.0.2", - "@jest/test-result": "^28.1.0", - "@jest/transform": "^28.1.0", - "@jest/types": "^28.1.0", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "execa": "^5.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.0", - "jest-message-util": "^28.1.0", - "jest-mock": "^28.1.0", - "jest-regex-util": "^28.0.2", - "jest-resolve": "^28.1.0", - "jest-snapshot": "^28.1.0", - "jest-util": "^28.1.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true - }, - "jest-haste-map": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.0.tgz", - "integrity": "sha512-xyZ9sXV8PtKi6NCrJlmq53PyNVHzxmcfXNVvIRHpHmh1j/HChC4pwKgyjj7Z9us19JMw8PpQTJsFWOsIfT93Dw==", - "dev": true, - "peer": true, - "requires": { - "@jest/types": "^28.1.0", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.3.2", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.0", - "jest-worker": "^28.1.0", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - } - }, - "jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "dev": true, - "peer": true - }, - "jest-resolve": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-28.1.0.tgz", - "integrity": "sha512-vvfN7+tPNnnhDvISuzD1P+CRVP8cK0FHXRwPAcdDaQv4zgvwvag2n55/h5VjYcM5UJG7L4TwE5tZlzcI0X2Lhw==", - "dev": true, - "peer": true, - "requires": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^28.1.0", - "jest-validate": "^28.1.0", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - } - }, - "jest-validate": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.0.tgz", - "integrity": "sha512-Lly7CJYih3vQBfjLeANGgBSBJ7pEa18cxpQfQEq2go2xyEzehnHfQTjoUia8xUv4x4J80XKFIDwJJThXtRFQXQ==", - "dev": true, - "peer": true, - "requires": { - "@jest/types": "^28.1.0", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^28.0.2", - "leven": "^3.1.0", - "pretty-format": "^28.1.0" - } - }, - "jest-worker": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.0.tgz", - "integrity": "sha512-ZHwM6mNwaWBR52Snff8ZvsCTqQsvhCxP/bT1I6T6DAnb6ygkshsyLQIMxFwHpYxht0HOoqt23JlC01viI7T03A==", - "dev": true, - "peer": true, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - } - }, - "pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "requires": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - } - }, - "react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "peer": true - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-serializer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", - "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", - "dev": true, - "requires": { - "@types/node": "*", - "graceful-fs": "^4.2.9" - } - }, - "jest-snapshot": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-28.1.0.tgz", - "integrity": "sha512-ex49M2ZrZsUyQLpLGxQtDbahvgBjlLPgklkqGM0hq/F7W/f8DyqZxVHjdy19QKBm4O93eDp+H5S23EiTbbUmHw==", - "dev": true, - "peer": true, - "requires": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^28.1.0", - "@jest/transform": "^28.1.0", - "@jest/types": "^28.1.0", - "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^28.1.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^28.1.0", - "jest-get-type": "^28.0.2", - "jest-haste-map": "^28.1.0", - "jest-matcher-utils": "^28.1.0", - "jest-message-util": "^28.1.0", - "jest-util": "^28.1.0", - "natural-compare": "^1.4.0", - "pretty-format": "^28.1.0", - "semver": "^7.3.5" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true - }, - "jest-haste-map": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.0.tgz", - "integrity": "sha512-xyZ9sXV8PtKi6NCrJlmq53PyNVHzxmcfXNVvIRHpHmh1j/HChC4pwKgyjj7Z9us19JMw8PpQTJsFWOsIfT93Dw==", - "dev": true, - "peer": true, - "requires": { - "@jest/types": "^28.1.0", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.3.2", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.0", - "jest-worker": "^28.1.0", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - } - }, - "jest-matcher-utils": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.0.tgz", - "integrity": "sha512-onnax0n2uTLRQFKAjC7TuaxibrPSvZgKTcSCnNUz/tOjJ9UhxNm7ZmPpoQavmTDUjXvUQ8KesWk2/VdrxIFzTQ==", - "dev": true, - "peer": true, - "requires": { - "chalk": "^4.0.0", - "jest-diff": "^28.1.0", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.0" - } - }, - "jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "dev": true, - "peer": true - }, - "jest-worker": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.0.tgz", - "integrity": "sha512-ZHwM6mNwaWBR52Snff8ZvsCTqQsvhCxP/bT1I6T6DAnb6ygkshsyLQIMxFwHpYxht0HOoqt23JlC01viI7T03A==", - "dev": true, - "peer": true, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - } - }, - "pretty-format": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.0.tgz", - "integrity": "sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q==", - "dev": true, - "peer": true, - "requires": { - "@jest/schemas": "^28.0.2", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - } - }, - "react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true, - "peer": true - }, - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "peer": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-sonar-reporter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jest-sonar-reporter/-/jest-sonar-reporter-2.0.0.tgz", - "integrity": "sha512-ZervDCgEX5gdUbdtWsjdipLN3bKJwpxbvhkYNXTAYvAckCihobSLr9OT/IuyNIRT1EZMDDwR6DroWtrq+IL64w==", - "dev": true, - "requires": { - "xml": "^1.0.1" - } - }, - "jest-styled-components": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/jest-styled-components/-/jest-styled-components-7.0.8.tgz", - "integrity": "sha512-0KE54d0yIzKcvtOv8eikyjG3rFRtKYUyQovaoha3nondtZzXYGB3bhsvYgEegU08Iry0ndWx2+g9f5ZzD4I+0Q==", - "dev": true, - "requires": { - "css": "^3.0.0" - } - }, - "jest-util": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.0.tgz", - "integrity": "sha512-qYdCKD77k4Hwkose2YBEqQk7PzUf/NSE+rutzceduFveQREeH6b+89Dc9+wjX9dAwHcgdx4yedGA3FQlU/qCTA==", - "dev": true, - "requires": { - "@jest/types": "^28.1.0", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - } - }, - "jest-validate": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz", - "integrity": "sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", - "leven": "^3.1.0", - "pretty-format": "^27.5.1" - }, - "dependencies": { - "@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", - "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "dev": true - } - } - }, - "jest-watch-typeahead": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-1.1.0.tgz", - "integrity": "sha512-Va5nLSJTN7YFtC2jd+7wsoe1pNe5K4ShLux/E5iHEwlB9AxaxmggY7to9KUqKojhaJw3aXqt5WAb4jGPOolpEw==", - "dev": true, - "requires": { - "ansi-escapes": "^4.3.1", - "chalk": "^4.0.0", - "jest-regex-util": "^28.0.0", - "jest-watcher": "^28.0.0", - "slash": "^4.0.0", - "string-length": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "dependencies": { - "jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "dev": true - }, - "slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "dev": true - }, - "string-length": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz", - "integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==", - "dev": true, - "requires": { - "char-regex": "^2.0.0", - "strip-ansi": "^7.0.1" - }, - "dependencies": { - "char-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.1.tgz", - "integrity": "sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==", - "dev": true - } - } - }, - "strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true - } - } - } - } - }, - "jest-watcher": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.0.tgz", - "integrity": "sha512-tNHMtfLE8Njcr2IRS+5rXYA4BhU90gAOwI9frTGOqd+jX0P/Au/JfRSNqsf5nUTcWdbVYuLxS1KjnzILSoR5hA==", - "dev": true, - "requires": { - "@jest/test-result": "^28.1.0", - "@jest/types": "^28.1.0", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.10.2", - "jest-util": "^28.1.0", - "string-length": "^4.0.1" - } - }, - "jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "dependencies": { - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsdom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", - "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", - "dev": true, - "requires": { - "abab": "^2.0.5", - "acorn": "^8.2.4", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", - "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", - "domexception": "^2.0.1", - "escodegen": "^2.0.0", - "form-data": "^3.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", - "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", - "dev": true, - "requires": { - "punycode": "^2.1.1" - } - }, - "webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "dev": true - }, - "whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", - "dev": true, - "requires": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - } - } - } - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "dev": true - }, - "json-schema-faker": { - "version": "0.5.0-rcv.40", - "resolved": "https://registry.npmjs.org/json-schema-faker/-/json-schema-faker-0.5.0-rcv.40.tgz", - "integrity": "sha512-BczZvu03jKrGh3ovCWrHusiX6MwiaKK2WZeyomKBNA8Nm/n7aBYz0mub1CnONB6cgxOZTNxx4afNmLblbUmZbA==", - "requires": { - "json-schema-ref-parser": "^6.1.0", - "jsonpath-plus": "^5.1.0" - } - }, - "json-schema-ref-parser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-6.1.0.tgz", - "integrity": "sha512-pXe9H1m6IgIpXmE5JSb8epilNTGsmTb2iPohAXpOdhqGFbQjNeHHsZxU+C8w6T81GZxSPFLeUoqDJmzxx5IGuw==", - "requires": { - "call-me-maybe": "^1.0.1", - "js-yaml": "^3.12.1", - "ono": "^4.0.11" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "requires": { - "minimist": "^1.2.0" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "jsonpath-plus": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-5.1.0.tgz", - "integrity": "sha512-890w2Pjtj0iswAxalRlt2kHthi6HKrXEfZcn+ZNZptv7F3rUGIeDuZo+C+h4vXBHLEsVjJrHeCm35nYeZLzSBQ==" - }, - "jsonpointer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.0.tgz", - "integrity": "sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg==", - "dev": true - }, - "jsx-ast-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.0.tgz", - "integrity": "sha512-XzO9luP6L0xkxwhIJMTJQpZo/eeN60K08jHdexfD569AGxeNug6UketeHXEhROoM8aR7EcUoOQmIhcJQjcuq8Q==", - "dev": true, - "requires": { - "array-includes": "^3.1.4", - "object.assign": "^4.1.2" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true - }, - "klona": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", - "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==", - "dev": true - }, - "language-subtag-registry": { - "version": "0.3.21", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", - "integrity": "sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==", - "dev": true - }, - "language-tags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", - "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", - "dev": true, - "requires": { - "language-subtag-registry": "~0.3.2" - } - }, - "leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "lilconfig": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz", - "integrity": "sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==", - "dev": true - }, - "lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "lint-staged": { - "version": "12.1.2", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.1.2.tgz", - "integrity": "sha512-bSMcQVqMW98HLLLR2c2tZ+vnDCnx4fd+0QJBQgN/4XkdspGRPc8DGp7UuOEBe1ApCfJ+wXXumYnJmU+wDo7j9A==", - "dev": true, - "requires": { - "cli-truncate": "^3.1.0", - "colorette": "^2.0.16", - "commander": "^8.3.0", - "debug": "^4.3.2", - "enquirer": "^2.3.6", - "execa": "^5.1.1", - "lilconfig": "2.0.4", - "listr2": "^3.13.3", - "micromatch": "^4.0.4", - "normalize-path": "^3.0.0", - "object-inspect": "^1.11.0", - "string-argv": "^0.3.1", - "supports-color": "^9.0.2", - "yaml": "^1.10.2" - }, - "dependencies": { - "colorette": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", - "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", - "dev": true - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "object-inspect": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.1.tgz", - "integrity": "sha512-If7BjFlpkzzBeV1cqgT3OSWT3azyoxDGajR+iGnFBfVV2EWyDyWaZZW2ERDjUaY2QM8i5jI3Sj7mhsM4DDAqWA==", - "dev": true - }, - "supports-color": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.2.1.tgz", - "integrity": "sha512-Obv7ycoCTG51N7y175StI9BlAXrmgZrFhZOb0/PyjHBher/NmsdBgbbQ1Inhq+gIhz6+7Gb+jWF2Vqi7Mf1xnQ==", - "dev": true - } - } - }, - "listr2": { - "version": "3.13.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.13.5.tgz", - "integrity": "sha512-3n8heFQDSk+NcwBn3CgxEibZGaRzx+pC64n3YjpMD1qguV4nWus3Al+Oo3KooqFKTQEJ1v7MmnbnyyNspgx3NA==", - "dev": true, - "requires": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.16", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rfdc": "^1.3.0", - "rxjs": "^7.4.0", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" - }, - "dependencies": { - "cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "requires": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - } - }, - "colorette": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", - "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", - "dev": true - }, - "slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - } - } - } - }, - "loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "dev": true - }, - "loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "dependencies": { - "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true - } - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" - }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", - "dev": true - }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" - }, - "lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", - "dev": true - }, - "lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", - "dev": true - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true - }, - "log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - } - }, - "log-update": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", - "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", - "dev": true, - "requires": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" - }, - "dependencies": { - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - } - } - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, - "requires": { - "tslib": "^2.0.3" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true - } - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "lz-string": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", - "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=" - }, - "magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", - "dev": true, - "requires": { - "sourcemap-codec": "^1.4.8" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, - "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", - "dev": true, - "requires": { - "tmpl": "1.0.x" - } - }, - "mdn-data": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", - "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", - "dev": true - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "dev": true - }, - "memfs": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz", - "integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==", - "dev": true, - "requires": { - "fs-monkey": "1.0.3" - } - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "requires": { - "mime-db": "1.52.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true - }, - "mini-css-extract-plugin": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.0.tgz", - "integrity": "sha512-ndG8nxCEnAemsg4FSgS+yNyHKgkTB4nPKqCOgh65j3/30qqC5RaSQQXMm++Y6sb6E1zRSxPkztj9fqxhS1Eo6w==", - "dev": true, - "requires": { - "schema-utils": "^4.0.0" - }, - "dependencies": { - "ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.3" - } - }, - "schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - } - } - } - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" - }, - "mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "requires": { - "minimist": "^1.2.6" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "multicast-dns": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.4.tgz", - "integrity": "sha512-XkCYOU+rr2Ft3LI6w4ye51M3VK31qJXFIxu0XLw169PtKG0Zx47OrXeVW/GCYOfpC9s1yyyf1S+L8/4LY0J9Zw==", - "dev": true, - "requires": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - } - }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, - "nanoclone": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", - "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==" - }, - "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" - }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true - }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, - "requires": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true - } - } - }, - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "requires": { - "whatwg-url": "^5.0.0" - }, - "dependencies": { - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - } - } - }, - "node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "dev": true - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true - }, - "node-releases": { - "version": "1.1.73", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz", - "integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==" - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", - "dev": true - }, - "normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dev": true, - "requires": { - "boolbase": "~1.0.0" - } - }, - "nwsapi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", - "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true - }, - "object-inspect": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", - "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==" - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - }, - "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - } - }, - "object.entries": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", - "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.0.tgz", - "integrity": "sha512-URbD8tgRthKD3YcC39vbvSDrX23upXnPcnGAjQfgxXF5ID75YcENawc9ZX/9iTP9ptUyfCLIxTTuMYoRfiOVKA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.1", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - } - }, - "has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true - }, - "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true - }, - "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - } - }, - "string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "dependencies": { - "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - } - } - }, - "string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "dependencies": { - "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - } - } - }, - "unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - } - } - } - }, - "object.fromentries": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", - "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.0.tgz", - "integrity": "sha512-URbD8tgRthKD3YcC39vbvSDrX23upXnPcnGAjQfgxXF5ID75YcENawc9ZX/9iTP9ptUyfCLIxTTuMYoRfiOVKA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.1", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - } - }, - "has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true - }, - "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true - }, - "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - } - }, - "string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "dependencies": { - "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - } - } - }, - "string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "dependencies": { - "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - } - } - }, - "unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - } - } - } - }, - "object.getownpropertydescriptors": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz", - "integrity": "sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.0.tgz", - "integrity": "sha512-URbD8tgRthKD3YcC39vbvSDrX23upXnPcnGAjQfgxXF5ID75YcENawc9ZX/9iTP9ptUyfCLIxTTuMYoRfiOVKA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.1", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - } - }, - "has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true - }, - "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true - }, - "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - } - }, - "string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "dependencies": { - "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - } - } - }, - "string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "dependencies": { - "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - } - } - }, - "unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - } - } - } - }, - "object.hasown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.1.tgz", - "integrity": "sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==", - "dev": true, - "requires": { - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "dependencies": { - "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - }, - "es-abstract": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.0.tgz", - "integrity": "sha512-URbD8tgRthKD3YcC39vbvSDrX23upXnPcnGAjQfgxXF5ID75YcENawc9ZX/9iTP9ptUyfCLIxTTuMYoRfiOVKA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.1", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - } - }, - "has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true - }, - "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true - }, - "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - } - }, - "string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - } - }, - "string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - } - }, - "unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - } - } - } - }, - "object.values": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", - "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.0.tgz", - "integrity": "sha512-URbD8tgRthKD3YcC39vbvSDrX23upXnPcnGAjQfgxXF5ID75YcENawc9ZX/9iTP9ptUyfCLIxTTuMYoRfiOVKA==", - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.1", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - } - }, - "has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" - }, - "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "requires": { - "call-bind": "^1.0.2" - } - }, - "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==" - }, - "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - } - }, - "string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "dependencies": { - "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - } - } - }, - "string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "dependencies": { - "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - } - } - }, - "unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "requires": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - } - } - } - }, - "obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true - }, - "on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "ono": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/ono/-/ono-4.0.11.tgz", - "integrity": "sha512-jQ31cORBFE6td25deYeD80wxKBMj+zBmHTrVxnc6CKhx8gho6ipmWM5zj/oeoqioZ99yqBls9Z/9Nss7J26G2g==", - "requires": { - "format-util": "^1.0.3" - } - }, - "open": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", - "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", - "dev": true, - "requires": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - } - }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "requires": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "dev": true, - "requires": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" - }, - "param-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "dev": true, - "requires": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true - } - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "parse-ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", - "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==" - }, - "parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true - }, - "pascal-case": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dev": true, - "requires": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true - } - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==" - }, - "pirates": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", - "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } - } - }, - "pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - } - } - }, - "postcss": { - "version": "8.4.13", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.13.tgz", - "integrity": "sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA==", - "dev": true, - "requires": { - "nanoid": "^3.3.3", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "postcss-attribute-case-insensitive": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.0.tgz", - "integrity": "sha512-b4g9eagFGq9T5SWX4+USfVyjIb3liPnjhHHRMP7FMB2kFVpYyfEscV0wP3eaXhKlcHKUut8lt5BGoeylWA/dBQ==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.2" - } - }, - "postcss-browser-comments": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz", - "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==", - "dev": true, - "requires": {} - }, - "postcss-calc": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", - "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.9", - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-clamp": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", - "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-color-functional-notation": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.2.tgz", - "integrity": "sha512-DXVtwUhIk4f49KK5EGuEdgx4Gnyj6+t2jBSEmxvpIK9QI40tWrpS2Pua8Q7iIZWBrki2QOaeUdEaLPPa91K0RQ==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-color-hex-alpha": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.3.tgz", - "integrity": "sha512-fESawWJCrBV035DcbKRPAVmy21LpoyiXdPTuHUfWJ14ZRjY7Y7PA6P4g8z6LQGYhU1WAxkTxjIjurXzoe68Glw==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-color-rebeccapurple": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.0.2.tgz", - "integrity": "sha512-SFc3MaocHaQ6k3oZaFwH8io6MdypkUtEy/eXzXEB1vEQlO3S3oDc/FSZA8AsS04Z25RirQhlDlHLh3dn7XewWw==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-colormin": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.0.tgz", - "integrity": "sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg==", - "dev": true, - "requires": { - "browserslist": "^4.16.6", - "caniuse-api": "^3.0.0", - "colord": "^2.9.1", - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-convert-values": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.0.tgz", - "integrity": "sha512-GkyPbZEYJiWtQB0KZ0X6qusqFHUepguBCNFi9t5JJc7I2OTXG7C0twbTLvCfaKOLl3rSXmpAwV7W5txd91V84g==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-custom-media": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.0.tgz", - "integrity": "sha512-FvO2GzMUaTN0t1fBULDeIvxr5IvbDXcIatt6pnJghc736nqNgsGao5NT+5+WVLAQiTt6Cb3YUms0jiPaXhL//g==", - "dev": true, - "requires": {} - }, - "postcss-custom-properties": { - "version": "12.1.7", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.7.tgz", - "integrity": "sha512-N/hYP5gSoFhaqxi2DPCmvto/ZcRDVjE3T1LiAMzc/bg53hvhcHOLpXOHb526LzBBp5ZlAUhkuot/bfpmpgStJg==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-custom-selectors": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.0.tgz", - "integrity": "sha512-/1iyBhz/W8jUepjGyu7V1OPcGbc636snN1yXEQCinb6Bwt7KxsiU7/bLQlp8GwAXzCh7cobBU5odNn/2zQWR8Q==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.4" - } - }, - "postcss-dir-pseudo-class": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.4.tgz", - "integrity": "sha512-I8epwGy5ftdzNWEYok9VjW9whC4xnelAtbajGv4adql4FIF09rnrxnA9Y8xSHN47y7gqFIv10C5+ImsLeJpKBw==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.9" - } - }, - "postcss-discard-comments": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.1.tgz", - "integrity": "sha512-5JscyFmvkUxz/5/+TB3QTTT9Gi9jHkcn8dcmmuN68JQcv3aQg4y88yEHHhwFB52l/NkaJ43O0dbksGMAo49nfQ==", - "dev": true, - "requires": {} - }, - "postcss-discard-duplicates": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", - "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", - "dev": true, - "requires": {} - }, - "postcss-discard-empty": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", - "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", - "dev": true, - "requires": {} - }, - "postcss-discard-overridden": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", - "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", - "dev": true, - "requires": {} - }, - "postcss-double-position-gradients": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.1.tgz", - "integrity": "sha512-jM+CGkTs4FcG53sMPjrrGE0rIvLDdCrqMzgDC5fLI7JHDO7o6QG8C5TQBtExb13hdBdoH9C2QVbG4jo2y9lErQ==", - "dev": true, - "requires": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-env-function": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz", - "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-flexbugs-fixes": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", - "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==", - "dev": true, - "requires": {} - }, - "postcss-focus-visible": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", - "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.9" - } - }, - "postcss-focus-within": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", - "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.9" - } - }, - "postcss-font-variant": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", - "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", - "dev": true, - "requires": {} - }, - "postcss-gap-properties": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.3.tgz", - "integrity": "sha512-rPPZRLPmEKgLk/KlXMqRaNkYTUpE7YC+bOIQFN5xcu1Vp11Y4faIXv6/Jpft6FMnl6YRxZqDZG0qQOW80stzxQ==", - "dev": true, - "requires": {} - }, - "postcss-image-set-function": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.6.tgz", - "integrity": "sha512-KfdC6vg53GC+vPd2+HYzsZ6obmPqOk6HY09kttU19+Gj1nC3S3XBVEXDHxkhxTohgZqzbUb94bKXvKDnYWBm/A==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-initial": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", - "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", - "dev": true, - "requires": {} - }, - "postcss-js": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", - "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", - "dev": true, - "requires": { - "camelcase-css": "^2.0.1" - } - }, - "postcss-lab-function": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.0.tgz", - "integrity": "sha512-Zb1EO9DGYfa3CP8LhINHCcTTCTLI+R3t7AX2mKsDzdgVQ/GkCpHOTgOr6HBHslP7XDdVbqgHW5vvRPMdVANQ8w==", - "dev": true, - "requires": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", - "dev": true, - "requires": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" - }, - "dependencies": { - "lilconfig": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz", - "integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==", - "dev": true - } - } - }, - "postcss-loader": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", - "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", - "dev": true, - "requires": { - "cosmiconfig": "^7.0.0", - "klona": "^2.0.5", - "semver": "^7.3.5" - }, - "dependencies": { - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "postcss-logical": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", - "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", - "dev": true, - "requires": {} - }, - "postcss-media-minmax": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", - "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", - "dev": true, - "requires": {} - }, - "postcss-merge-longhand": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.4.tgz", - "integrity": "sha512-hbqRRqYfmXoGpzYKeW0/NCZhvNyQIlQeWVSao5iKWdyx7skLvCfQFGIUsP9NUs3dSbPac2IC4Go85/zG+7MlmA==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^5.1.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-merge-rules": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.1.tgz", - "integrity": "sha512-8wv8q2cXjEuCcgpIB1Xx1pIy8/rhMPIQqYKNzEdyx37m6gpq83mQQdCxgIkFgliyEnKvdwJf/C61vN4tQDq4Ww==", - "dev": true, - "requires": { - "browserslist": "^4.16.6", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^3.1.0", - "postcss-selector-parser": "^6.0.5" - } - }, - "postcss-minify-font-values": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", - "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-minify-gradients": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", - "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", - "dev": true, - "requires": { - "colord": "^2.9.1", - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-minify-params": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.2.tgz", - "integrity": "sha512-aEP+p71S/urY48HWaRHasyx4WHQJyOYaKpQ6eXl8k0kxg66Wt/30VR6/woh8THgcpRbonJD5IeD+CzNhPi1L8g==", - "dev": true, - "requires": { - "browserslist": "^4.16.6", - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-minify-selectors": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.0.tgz", - "integrity": "sha512-vYxvHkW+iULstA+ctVNx0VoRAR4THQQRkG77o0oa4/mBS0OzGvvzLIvHDv/nNEM0crzN2WIyFU5X7wZhaUK3RA==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.5" - } - }, - "postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true, - "requires": {} - }, - "postcss-modules-local-by-default": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", - "dev": true, - "requires": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - } - }, - "postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.4" - } - }, - "postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "requires": { - "icss-utils": "^5.0.0" - } - }, - "postcss-nested": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", - "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.6" - } - }, - "postcss-nesting": { - "version": "10.1.5", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.1.5.tgz", - "integrity": "sha512-+NyBBE/wUcJ+NJgVd2FyKIZ414lul6ExqkOt1qXXw7oRzpQ0iT68cVpx+QfHh42QUMHXNoVLlN9InFY9XXK8ng==", - "dev": true, - "requires": { - "@csstools/selector-specificity": "1.0.0", - "postcss-selector-parser": "^6.0.10" - } - }, - "postcss-normalize": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-10.0.1.tgz", - "integrity": "sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA==", - "dev": true, - "requires": { - "@csstools/normalize.css": "*", - "postcss-browser-comments": "^4", - "sanitize.css": "*" - } - }, - "postcss-normalize-charset": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", - "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", - "dev": true, - "requires": {} - }, - "postcss-normalize-display-values": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", - "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-normalize-positions": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.0.tgz", - "integrity": "sha512-8gmItgA4H5xiUxgN/3TVvXRoJxkAWLW6f/KKhdsH03atg0cB8ilXnrB5PpSshwVu/dD2ZsRFQcR1OEmSBDAgcQ==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-normalize-repeat-style": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.0.tgz", - "integrity": "sha512-IR3uBjc+7mcWGL6CtniKNQ4Rr5fTxwkaDHwMBDGGs1x9IVRkYIT/M4NelZWkAOBdV6v3Z9S46zqaKGlyzHSchw==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-normalize-string": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", - "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-normalize-timing-functions": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", - "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-normalize-unicode": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.0.tgz", - "integrity": "sha512-J6M3MizAAZ2dOdSjy2caayJLQT8E8K9XjLce8AUQMwOrCvjCHv24aLC/Lps1R1ylOfol5VIDMaM/Lo9NGlk1SQ==", - "dev": true, - "requires": { - "browserslist": "^4.16.6", - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-normalize-url": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", - "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", - "dev": true, - "requires": { - "normalize-url": "^6.0.1", - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-normalize-whitespace": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", - "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-opacity-percentage": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.2.tgz", - "integrity": "sha512-lyUfF7miG+yewZ8EAk9XUBIlrHyUE6fijnesuz+Mj5zrIHIEw6KcIZSOk/elVMqzLvREmXB83Zi/5QpNRYd47w==", - "dev": true - }, - "postcss-ordered-values": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.1.tgz", - "integrity": "sha512-7lxgXF0NaoMIgyihL/2boNAEZKiW0+HkMhdKMTD93CjW8TdCy2hSdj8lsAo+uwm7EDG16Da2Jdmtqpedl0cMfw==", - "dev": true, - "requires": { - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-overflow-shorthand": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.3.tgz", - "integrity": "sha512-CxZwoWup9KXzQeeIxtgOciQ00tDtnylYIlJBBODqkgS/PU2jISuWOL/mYLHmZb9ZhZiCaNKsCRiLp22dZUtNsg==", - "dev": true, - "requires": {} - }, - "postcss-page-break": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", - "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", - "dev": true, - "requires": {} - }, - "postcss-place": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.4.tgz", - "integrity": "sha512-MrgKeiiu5OC/TETQO45kV3npRjOFxEHthsqGtkh3I1rPbZSbXGD/lZVi9j13cYh+NA8PIAPyk6sGjT9QbRyvSg==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-preset-env": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.5.0.tgz", - "integrity": "sha512-0BJzWEfCdTtK2R3EiKKSdkE51/DI/BwnhlnicSW482Ym6/DGHud8K0wGLcdjip1epVX0HKo4c8zzTeV/SkiejQ==", - "dev": true, - "requires": { - "@csstools/postcss-color-function": "^1.1.0", - "@csstools/postcss-font-format-keywords": "^1.0.0", - "@csstools/postcss-hwb-function": "^1.0.0", - "@csstools/postcss-ic-unit": "^1.0.0", - "@csstools/postcss-is-pseudo-class": "^2.0.2", - "@csstools/postcss-normalize-display-values": "^1.0.0", - "@csstools/postcss-oklab-function": "^1.1.0", - "@csstools/postcss-progressive-custom-properties": "^1.3.0", - "@csstools/postcss-stepped-value-functions": "^1.0.0", - "@csstools/postcss-unset-value": "^1.0.0", - "autoprefixer": "^10.4.6", - "browserslist": "^4.20.3", - "css-blank-pseudo": "^3.0.3", - "css-has-pseudo": "^3.0.4", - "css-prefers-color-scheme": "^6.0.3", - "cssdb": "^6.6.1", - "postcss-attribute-case-insensitive": "^5.0.0", - "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^4.2.2", - "postcss-color-hex-alpha": "^8.0.3", - "postcss-color-rebeccapurple": "^7.0.2", - "postcss-custom-media": "^8.0.0", - "postcss-custom-properties": "^12.1.7", - "postcss-custom-selectors": "^6.0.0", - "postcss-dir-pseudo-class": "^6.0.4", - "postcss-double-position-gradients": "^3.1.1", - "postcss-env-function": "^4.0.6", - "postcss-focus-visible": "^6.0.4", - "postcss-focus-within": "^5.0.4", - "postcss-font-variant": "^5.0.0", - "postcss-gap-properties": "^3.0.3", - "postcss-image-set-function": "^4.0.6", - "postcss-initial": "^4.0.1", - "postcss-lab-function": "^4.2.0", - "postcss-logical": "^5.0.4", - "postcss-media-minmax": "^5.0.0", - "postcss-nesting": "^10.1.4", - "postcss-opacity-percentage": "^1.1.2", - "postcss-overflow-shorthand": "^3.0.3", - "postcss-page-break": "^3.0.4", - "postcss-place": "^7.0.4", - "postcss-pseudo-class-any-link": "^7.1.2", - "postcss-replace-overflow-wrap": "^4.0.0", - "postcss-selector-not": "^5.0.0", - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "browserslist": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", - "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" - } - }, - "electron-to-chromium": { - "version": "1.4.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", - "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", - "dev": true - }, - "node-releases": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", - "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", - "dev": true - }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-pseudo-class-any-link": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.3.tgz", - "integrity": "sha512-I9Yp1VV2r8xFwg/JrnAlPCcKmutv6f6Ig6/CHFPqGJiDgYXM9C+0kgLfK4KOXbKNw+63QYl4agRUB0Wi9ftUIg==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.10" - } - }, - "postcss-reduce-initial": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.0.tgz", - "integrity": "sha512-5OgTUviz0aeH6MtBjHfbr57tml13PuedK/Ecg8szzd4XRMbYxH4572JFG067z+FqBIf6Zp/d+0581glkvvWMFw==", - "dev": true, - "requires": { - "browserslist": "^4.16.6", - "caniuse-api": "^3.0.0" - } - }, - "postcss-reduce-transforms": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", - "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-replace-overflow-wrap": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", - "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", - "dev": true, - "requires": {} - }, - "postcss-selector-not": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-5.0.0.tgz", - "integrity": "sha512-/2K3A4TCP9orP4TNS7u3tGdRFVKqz/E6pX3aGnriPG0jU78of8wsUcqE4QAhWEU0d+WnMSF93Ah3F//vUtK+iQ==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", - "dev": true, - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - } - }, - "postcss-svgo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", - "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0", - "svgo": "^2.7.0" - }, - "dependencies": { - "commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true - }, - "css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "dev": true, - "requires": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - } - }, - "css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dev": true, - "requires": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - } - }, - "css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true - }, - "dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - } - }, - "domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true - }, - "domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - } - }, - "mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "dev": true - }, - "nth-check": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", - "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", - "dev": true, - "requires": { - "boolbase": "^1.0.0" - } - }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "svgo": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", - "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", - "dev": true, - "requires": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^4.1.3", - "css-tree": "^1.1.3", - "csso": "^4.2.0", - "picocolors": "^1.0.0", - "stable": "^0.1.8" - } - } - } - }, - "postcss-unique-selectors": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", - "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.5" - } - }, - "postcss-value-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" - }, - "prettier": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", - "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "pretty-bytes": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", - "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", - "dev": true - }, - "pretty-error": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", - "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", - "dev": true, - "requires": { - "lodash": "^4.17.20", - "renderkid": "^3.0.0" - } - }, - "pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "requires": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" - }, - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" - } - } - }, - "pretty-ms": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", - "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", - "requires": { - "parse-ms": "^2.1.0" - } - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "promise": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz", - "integrity": "sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==", - "dev": true, - "requires": { - "asap": "~2.0.6" - } - }, - "prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "requires": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - } - }, - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, - "property-expr": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz", - "integrity": "sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg==" - }, - "proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "requires": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "dependencies": { - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true - } - } - }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", - "dev": true - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", - "dev": true - }, - "qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", - "dev": true, - "requires": { - "side-channel": "^1.0.4" - } - }, - "querystring": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", - "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==" - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true - }, - "raf": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", - "dev": true, - "requires": { - "performance-now": "^2.1.0" - } - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true - }, - "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dev": true, - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "dependencies": { - "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true - } - } - }, - "react": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz", - "integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==", - "requires": { - "loose-envify": "^1.1.0" - } - }, - "react-ace": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-9.5.0.tgz", - "integrity": "sha512-4l5FgwGh6K7A0yWVMQlPIXDItM4Q9zzXRqOae8KkCl6MkOob7sC1CzHxZdOGvV+QioKWbX2p5HcdOVUv6cAdSg==", - "requires": { - "ace-builds": "^1.4.13", - "diff-match-patch": "^1.0.5", - "lodash.get": "^4.4.2", - "lodash.isequal": "^4.5.0", - "prop-types": "^15.7.2" - }, - "dependencies": { - "ace-builds": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.13.tgz", - "integrity": "sha512-SOLzdaQkY6ecPKYRDDg+MY1WoGgXA34cIvYJNNoBMGGUswHmlauU2Hy0UL96vW0Fs/LgFbMUjD+6vqzWTldIYQ==" - } - } - }, - "react-app-polyfill": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz", - "integrity": "sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w==", - "dev": true, - "requires": { - "core-js": "^3.19.2", - "object-assign": "^4.1.1", - "promise": "^8.1.0", - "raf": "^3.4.1", - "regenerator-runtime": "^0.13.9", - "whatwg-fetch": "^3.6.2" - }, - "dependencies": { - "core-js": { - "version": "3.22.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.22.5.tgz", - "integrity": "sha512-VP/xYuvJ0MJWRAobcmQ8F2H6Bsn+s7zqAAjFaHGBMc5AQm7zaelhD1LGduFn2EehEcQcU+br6t+fwbpQ5d1ZWA==", - "dev": true - }, - "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true - } - } - }, - "react-datepicker": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.5.0.tgz", - "integrity": "sha512-mFP/SbtFSXx21Wx3Nfv+RREwd/x0q14x7QL79ZCi/PVkHSFLwLWhXyOtj3OIzi1AcVYb/fMMcvi8e5b12n8/sg==", - "requires": { - "@popperjs/core": "^2.9.2", - "classnames": "^2.2.6", - "date-fns": "^2.24.0", - "prop-types": "^15.7.2", - "react-onclickoutside": "^6.12.0", - "react-popper": "^2.2.5" - }, - "dependencies": { - "date-fns": { - "version": "2.27.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.27.0.tgz", - "integrity": "sha512-sj+J0Mo2p2X1e306MHq282WS4/A8Pz/95GIFcsPNMPMZVI3EUrAdSv90al1k+p74WGLCruMXk23bfEDZa71X9Q==" - } - } - }, - "react-dev-utils": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", - "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.0", - "address": "^1.1.2", - "browserslist": "^4.18.1", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "detect-port-alt": "^1.1.6", - "escape-string-regexp": "^4.0.0", - "filesize": "^8.0.6", - "find-up": "^5.0.0", - "fork-ts-checker-webpack-plugin": "^6.5.0", - "global-modules": "^2.0.0", - "globby": "^11.0.4", - "gzip-size": "^6.0.0", - "immer": "^9.0.7", - "is-root": "^2.1.0", - "loader-utils": "^3.2.0", - "open": "^8.4.0", - "pkg-up": "^3.1.0", - "prompts": "^2.4.2", - "react-error-overlay": "^6.0.11", - "recursive-readdir": "^2.2.2", - "shell-quote": "^1.7.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - } - } - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "browserslist": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", - "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.4.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", - "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", - "dev": true - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "loader-utils": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.0.tgz", - "integrity": "sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ==", - "dev": true - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "node-releases": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", - "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", - "dev": true - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "react-dom": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", - "integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==", - "requires": { - "loose-envify": "^1.1.0", - "scheduler": "^0.22.0" - } - }, - "react-error-overlay": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", - "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", - "dev": true - }, - "react-fast-compare": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", - "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" - }, - "react-hook-form": { - "version": "7.6.9", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.6.9.tgz", - "integrity": "sha512-nz+btC4WFIm3zPBjw22K3t9nnJtlMMwj8slcbPYoTKlkSVA5l+q3Ai+VF0YzeRi7vbyyeGQvpyibov1xd/TV7A==", - "requires": {} - }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "react-multi-select-component": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/react-multi-select-component/-/react-multi-select-component-4.0.6.tgz", - "integrity": "sha512-cNpDv8vh1kWkJiMsa097tTUqWLVTQn+La4aXlgoGOQVpOSH9u1fbj1+MsvnLQjTBySuDx+pzm/DpbIoma/i1Fw==", - "requires": {} - }, - "react-onclickoutside": { - "version": "6.12.1", - "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.1.tgz", - "integrity": "sha512-a5Q7CkWznBRUWPmocCvE8b6lEYw1s6+opp/60dCunhO+G6E4tDTO2Sd2jKE+leEnnrLAE2Wj5DlDHNqj5wPv1Q==", - "requires": {} - }, - "react-popper": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.2.5.tgz", - "integrity": "sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw==", - "requires": { - "react-fast-compare": "^3.0.1", - "warning": "^4.0.2" - } - }, - "react-redux": { - "version": "7.2.6", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz", - "integrity": "sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==", - "requires": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" - }, - "dependencies": { - "@babel/runtime": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz", - "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==", - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "@types/react-redux": { - "version": "7.1.22", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.22.tgz", - "integrity": "sha512-GxIA1kM7ClU73I6wg9IRTVwSO9GS+SAKZKe0Enj+82HMU6aoESFU2HNAdNi3+J53IaOHPiUfT3kSG4L828joDQ==", - "requires": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" - } - } - }, - "react-refresh": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", - "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", - "dev": true - }, - "react-router-dom": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz", - "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==", - "requires": { - "history": "^5.2.0", - "react-router": "6.3.0" - }, - "dependencies": { - "react-router": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", - "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==", - "requires": { - "history": "^5.2.0" - } - } - } - }, - "react-scripts": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", - "integrity": "sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==", - "dev": true, - "requires": { - "@babel/core": "^7.16.0", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", - "@svgr/webpack": "^5.5.0", - "babel-jest": "^27.4.2", - "babel-loader": "^8.2.3", - "babel-plugin-named-asset-import": "^0.3.8", - "babel-preset-react-app": "^10.0.1", - "bfj": "^7.0.2", - "browserslist": "^4.18.1", - "camelcase": "^6.2.1", - "case-sensitive-paths-webpack-plugin": "^2.4.0", - "css-loader": "^6.5.1", - "css-minimizer-webpack-plugin": "^3.2.0", - "dotenv": "^10.0.0", - "dotenv-expand": "^5.1.0", - "eslint": "^8.3.0", - "eslint-config-react-app": "^7.0.1", - "eslint-webpack-plugin": "^3.1.1", - "file-loader": "^6.2.0", - "fs-extra": "^10.0.0", - "fsevents": "^2.3.2", - "html-webpack-plugin": "^5.5.0", - "identity-obj-proxy": "^3.0.0", - "jest": "^27.4.3", - "jest-resolve": "^27.4.2", - "jest-watch-typeahead": "^1.0.0", - "mini-css-extract-plugin": "^2.4.5", - "postcss": "^8.4.4", - "postcss-flexbugs-fixes": "^5.0.2", - "postcss-loader": "^6.2.1", - "postcss-normalize": "^10.0.1", - "postcss-preset-env": "^7.0.1", - "prompts": "^2.4.2", - "react-app-polyfill": "^3.0.0", - "react-dev-utils": "^12.0.1", - "react-refresh": "^0.11.0", - "resolve": "^1.20.0", - "resolve-url-loader": "^4.0.0", - "sass-loader": "^12.3.0", - "semver": "^7.3.5", - "source-map-loader": "^3.0.0", - "style-loader": "^3.3.1", - "tailwindcss": "^3.0.2", - "terser-webpack-plugin": "^5.2.5", - "webpack": "^5.64.4", - "webpack-dev-server": "^4.6.0", - "webpack-manifest-plugin": "^4.0.2", - "workbox-webpack-plugin": "^6.4.1" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/compat-data": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz", - "integrity": "sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw==", - "dev": true - }, - "@babel/core": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.10.tgz", - "integrity": "sha512-liKoppandF3ZcBnIYFjfSDHZLKdLHGJRkoWtG8zQyGJBQfIYobpnVGI5+pLBNtS6psFLDzyq8+h5HiVljW9PNA==", - "dev": true, - "requires": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-compilation-targets": "^7.17.10", - "@babel/helper-module-transforms": "^7.17.7", - "@babel/helpers": "^7.17.9", - "@babel/parser": "^7.17.10", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.10", - "@babel/types": "^7.17.10", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz", - "integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==", - "dev": true, - "requires": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", - "jsesc": "^2.5.1" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz", - "integrity": "sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.17.10", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.20.2", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", - "dev": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-transforms": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", - "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-simple-access": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", - "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true - }, - "@babel/helpers": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.9.tgz", - "integrity": "sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q==", - "dev": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.9", - "@babel/types": "^7.17.0" - } - }, - "@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz", - "integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz", - "integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "@jest/console": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", - "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/core": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz", - "integrity": "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==", - "dev": true, - "requires": { - "@jest/console": "^27.5.1", - "@jest/reporters": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.8.1", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^27.5.1", - "jest-config": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-resolve-dependencies": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "jest-watcher": "^27.5.1", - "micromatch": "^4.0.4", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/environment": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", - "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", - "dev": true, - "requires": { - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1" - } - }, - "@jest/fake-timers": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", - "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@sinonjs/fake-timers": "^8.0.1", - "@types/node": "*", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" - } - }, - "@jest/globals": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz", - "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==", - "dev": true, - "requires": { - "@jest/environment": "^27.5.1", - "@jest/types": "^27.5.1", - "expect": "^27.5.1" - } - }, - "@jest/reporters": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz", - "integrity": "sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==", - "dev": true, - "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-haste-map": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "slash": "^3.0.0", - "source-map": "^0.6.0", - "string-length": "^4.0.1", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^8.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/source-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", - "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==", - "dev": true, - "requires": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9", - "source-map": "^0.6.0" - } - }, - "@jest/test-result": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz", - "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==", - "dev": true, - "requires": { - "@jest/console": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - } - }, - "@jest/test-sequencer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz", - "integrity": "sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==", - "dev": true, - "requires": { - "@jest/test-result": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-runtime": "^27.5.1" - } - }, - "@jest/transform": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", - "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^27.5.1", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-util": "^27.5.1", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@sinonjs/fake-timers": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", - "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, - "@types/yargs": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", - "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "babel-jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", - "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==", - "dev": true, - "requires": { - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^27.5.1", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "babel-plugin-jest-hoist": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz", - "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==", - "dev": true, - "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.0.0", - "@types/babel__traverse": "^7.0.6" - } - }, - "babel-preset-jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", - "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "^27.5.1", - "babel-preset-current-node-syntax": "^1.0.0" - } - }, - "browserslist": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", - "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", - "dev": true - }, - "dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.4.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz", - "integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==", - "dev": true - }, - "emittery": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", - "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", - "dev": true - }, - "expect": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", - "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", - "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", - "dev": true, - "requires": { - "@jest/core": "^27.5.1", - "import-local": "^3.0.2", - "jest-cli": "^27.5.1" - } - }, - "jest-changed-files": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz", - "integrity": "sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "execa": "^5.0.0", - "throat": "^6.0.1" - } - }, - "jest-circus": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz", - "integrity": "sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==", - "dev": true, - "requires": { - "@jest/environment": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "expect": "^27.5.1", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3", - "throat": "^6.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-cli": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", - "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==", - "dev": true, - "requires": { - "@jest/core": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "import-local": "^3.0.2", - "jest-config": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "prompts": "^2.0.1", - "yargs": "^16.2.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-config": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", - "integrity": "sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==", - "dev": true, - "requires": { - "@babel/core": "^7.8.0", - "@jest/test-sequencer": "^27.5.1", - "@jest/types": "^27.5.1", - "babel-jest": "^27.5.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.1", - "graceful-fs": "^4.2.9", - "jest-circus": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-jasmine2": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-diff": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", - "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-docblock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz", - "integrity": "sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==", - "dev": true, - "requires": { - "detect-newline": "^3.0.0" - } - }, - "jest-each": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz", - "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-environment-node": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz", - "integrity": "sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==", - "dev": true, - "requires": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" - } - }, - "jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "dev": true - }, - "jest-leak-detector": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", - "integrity": "sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==", - "dev": true, - "requires": { - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - } - }, - "jest-message-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", - "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^27.5.1", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-mock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", - "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@types/node": "*" - } - }, - "jest-resolve-dependencies": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz", - "integrity": "sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-snapshot": "^27.5.1" - } - }, - "jest-runner": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz", - "integrity": "sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==", - "dev": true, - "requires": { - "@jest/console": "^27.5.1", - "@jest/environment": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.8.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-leak-detector": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "source-map-support": "^0.5.6", - "throat": "^6.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-runtime": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", - "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==", - "dev": true, - "requires": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/globals": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "execa": "^5.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-snapshot": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", - "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", - "dev": true, - "requires": { - "@babel/core": "^7.7.2", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.0.0", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__traverse": "^7.0.4", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "natural-compare": "^1.4.0", - "pretty-format": "^27.5.1", - "semver": "^7.3.2" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-watcher": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz", - "integrity": "sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==", - "dev": true, - "requires": { - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "jest-util": "^27.5.1", - "string-length": "^4.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node-releases": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz", - "integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==", - "dev": true - }, - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "v8-to-istanbul": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", - "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0", - "source-map": "^0.7.3" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } - } - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - } - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "requires": { - "picomatch": "^2.2.1" - } - }, - "recursive-readdir": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", - "integrity": "sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==", - "dev": true, - "requires": { - "minimatch": "3.0.4" - } - }, - "redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - } - }, - "redux": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz", - "integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==", - "requires": { - "@babel/runtime": "^7.9.2" - } - }, - "redux-mock-store": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.4.tgz", - "integrity": "sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==", - "dev": true, - "requires": { - "lodash.isplainobject": "^4.0.6" - } - }, - "redux-thunk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", - "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==", - "requires": {} - }, - "reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", - "dev": true - }, - "regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true - }, - "regenerate-unicode-properties": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz", - "integrity": "sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw==", - "dev": true, - "requires": { - "regenerate": "^1.4.2" - } - }, - "regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" - }, - "regenerator-transform": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.0.tgz", - "integrity": "sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.8.4" - } - }, - "regex-parser": { - "version": "2.2.11", - "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", - "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", - "dev": true - }, - "regexp.prototype.flags": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", - "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==" - }, - "regexpu-core": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.0.1.tgz", - "integrity": "sha512-CriEZlrKK9VJw/xQGJpQM5rY88BtuL8DM+AEwvcThHilbxiTAy8vq4iJnd2tqq8wLmjbGZzP7ZcKFjbGkmEFrw==", - "dev": true, - "requires": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.0.1", - "regjsgen": "^0.6.0", - "regjsparser": "^0.8.2", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.0.0" - } - }, - "regjsgen": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.6.0.tgz", - "integrity": "sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA==", - "dev": true - }, - "regjsparser": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.8.4.tgz", - "integrity": "sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA==", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true - } - } - }, - "relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", - "dev": true - }, - "renderkid": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", - "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", - "dev": true, - "requires": { - "css-select": "^4.1.3", - "dom-converter": "^0.2.0", - "htmlparser2": "^6.1.0", - "lodash": "^4.17.21", - "strip-ansi": "^6.0.1" - }, - "dependencies": { - "css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "dev": true, - "requires": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - } - }, - "css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true - }, - "dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - } - }, - "domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true - }, - "domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - } - }, - "nth-check": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", - "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", - "dev": true, - "requires": { - "boolbase": "^1.0.0" - } - } - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" - }, - "requireindex": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", - "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", - "dev": true - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true - }, - "reselect": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.5.tgz", - "integrity": "sha512-uVdlz8J7OO+ASpBYoz1Zypgx0KasCY20H+N8JD13oUMtPvSHQuscrHop4KbXrbsBcdB9Ds7lVK7eRkBIfO43vQ==" - }, - "resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } - }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "requires": { - "resolve-from": "^5.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" - }, - "resolve-url-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz", - "integrity": "sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA==", - "dev": true, - "requires": { - "adjust-sourcemap-loader": "^4.0.0", - "convert-source-map": "^1.7.0", - "loader-utils": "^2.0.0", - "postcss": "^7.0.35", - "source-map": "0.6.1" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "resolve.exports": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz", - "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==", - "dev": true - }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, - "retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, - "rollup": { - "version": "2.72.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.72.1.tgz", - "integrity": "sha512-NTc5UGy/NWFGpSqF1lFY8z9Adri6uhyMLI6LvPAXdBKoPRFhIIiBUpt+Qg2awixqO3xvzSijjhnb4+QEZwJmxA==", - "dev": true, - "requires": { - "fsevents": "~2.3.2" - } - }, - "rollup-plugin-terser": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", - "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "jest-worker": "^26.2.1", - "serialize-javascript": "^4.0.0", - "terser": "^5.0.0" - }, - "dependencies": { - "jest-worker": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", - "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", - "dev": true, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - } - }, - "serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - } - } - }, - "run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "rxjs": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz", - "integrity": "sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w==", - "dev": true, - "requires": { - "tslib": "~2.1.0" - }, - "dependencies": { - "tslib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", - "dev": true - } - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sanitize.css": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", - "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==", - "dev": true - }, - "sass": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.44.0.tgz", - "integrity": "sha512-0hLREbHFXGQqls/K8X+koeP+ogFRPF4ZqetVB19b7Cst9Er8cOR0rc6RU7MaI4W1JmUShd1BPgPoeqmmgMMYFw==", - "requires": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0" - } - }, - "sass-loader": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", - "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", - "dev": true, - "requires": { - "klona": "^2.0.4", - "neo-async": "^2.6.2" - } - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "dev": true, - "requires": { - "xmlchars": "^2.2.0" - } - }, - "scheduler": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", - "integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==", - "requires": { - "loose-envify": "^1.1.0" - } - }, - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - } - } - }, - "select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", - "dev": true - }, - "selfsigned": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.1.tgz", - "integrity": "sha512-LmME957M1zOsUhG+67rAjKfiWFox3SBxE/yymatMZsAx+oMrJ0YQ8AToOnyCm7xbeg2ep37IHLxdu0o2MavQOQ==", - "dev": true, - "requires": { - "node-forge": "^1" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dev": true, - "requires": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "dependencies": { - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - } - } - }, - "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, - "serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", - "dev": true, - "requires": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "dependencies": { - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true - }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "dev": true - } - } - }, - "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dev": true, - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - } - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true - }, - "shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "shell-quote": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", - "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", - "dev": true - }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - } - }, - "sockjs": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", - "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", - "dev": true, - "requires": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" - } - }, - "source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true - }, - "source-map-loader": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.1.tgz", - "integrity": "sha512-Vp1UsfyPvgujKQzi4pyDiTOnE3E4H+yHvkVRN3c/9PJmQS4CQJExvcDvaX/D+RV+xQben9HJ56jMJS3CgUeWyA==", - "dev": true, - "requires": { - "abab": "^2.0.5", - "iconv-lite": "^0.6.3", - "source-map-js": "^1.0.1" - }, - "dependencies": { - "iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - } - } - }, - "source-map-resolve": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", - "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", - "dev": true, - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0" - } - }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "dev": true - }, - "spawn-command": { - "version": "0.0.2-1", - "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", - "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=", - "dev": true - }, - "spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "dev": true - }, - "stack-utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", - "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", - "dev": true, - "requires": { - "escape-string-regexp": "^2.0.0" - }, - "dependencies": { - "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true - } - } - }, - "stackframe": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.1.tgz", - "integrity": "sha512-h88QkzREN/hy8eRdyNhhsO7RSJ5oyTqxxmmn0dzBIMUclZsjpfmrsg81vp8mjjAs2vAZ72nyWxRUwSwmh0e4xg==", - "dev": true - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "string-argv": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", - "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", - "dev": true - }, - "string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "requires": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - } - }, - "string-natural-compare": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", - "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "string.prototype.matchall": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz", - "integrity": "sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.3.1", - "side-channel": "^1.0.4" - }, - "dependencies": { - "es-abstract": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", - "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", - "is-string": "^1.0.7", - "is-weakref": "^1.0.1", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - } - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true - } - } - }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "stringify-object": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", - "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", - "dev": true, - "requires": { - "get-own-enumerable-property-symbols": "^3.0.0", - "is-obj": "^1.0.1", - "is-regexp": "^1.0.0" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" - }, - "strip-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", - "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", - "dev": true - }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true - }, - "strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "requires": { - "min-indent": "^1.0.0" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" - }, - "style-loader": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", - "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", - "dev": true, - "requires": {} - }, - "styled-components": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.1.tgz", - "integrity": "sha512-JThv2JRzyH0NOIURrk9iskdxMSAAtCfj/b2Sf1WJaCUsloQkblepy1jaCLX/bYE+mhYo3unmwVSI9I5d9ncSiQ==", - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/traverse": "^7.4.5", - "@emotion/is-prop-valid": "^0.8.8", - "@emotion/stylis": "^0.8.4", - "@emotion/unitless": "^0.7.4", - "babel-plugin-styled-components": ">= 1.12.0", - "css-to-react-native": "^3.0.0", - "hoist-non-react-statics": "^3.0.0", - "shallowequal": "^1.1.0", - "supports-color": "^5.5.0" - }, - "dependencies": { - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "stylehacks": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.0.tgz", - "integrity": "sha512-SzLmvHQTrIWfSgljkQCw2++C9+Ne91d/6Sp92I8c5uHTcy/PgeHamwITIbBW9wnFTY/3ZfSXR9HIL6Ikqmcu6Q==", - "dev": true, - "requires": { - "browserslist": "^4.16.6", - "postcss-selector-parser": "^6.0.4" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - }, - "supports-hyperlinks": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", - "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", - "dev": true, - "requires": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" - }, - "svg-parser": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", - "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", - "dev": true - }, - "svgo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", - "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "coa": "^2.0.2", - "css-select": "^2.0.0", - "css-select-base-adapter": "^0.1.1", - "css-tree": "1.0.0-alpha.37", - "csso": "^4.0.2", - "js-yaml": "^3.13.1", - "mkdirp": "~0.5.1", - "object.values": "^1.1.0", - "sax": "~1.2.4", - "stable": "^0.1.8", - "unquote": "~1.1.1", - "util.promisify": "~1.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true - }, - "tailwindcss": { - "version": "3.0.24", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.0.24.tgz", - "integrity": "sha512-H3uMmZNWzG6aqmg9q07ZIRNIawoiEcNFKDfL+YzOPuPsXuDXxJxB9icqzLgdzKNwjG3SAro2h9SYav8ewXNgig==", - "dev": true, - "requires": { - "arg": "^5.0.1", - "chokidar": "^3.5.3", - "color-name": "^1.1.4", - "detective": "^5.2.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.2.11", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "lilconfig": "^2.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.12", - "postcss-js": "^4.0.0", - "postcss-load-config": "^3.1.4", - "postcss-nested": "5.0.6", - "postcss-selector-parser": "^6.0.10", - "postcss-value-parser": "^4.2.0", - "quick-lru": "^5.1.1", - "resolve": "^1.22.0" - }, - "dependencies": { - "arg": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.1.tgz", - "integrity": "sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA==", - "dev": true - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "lilconfig": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz", - "integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==", - "dev": true - }, - "object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true - }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", - "dev": true, - "requires": { - "is-core-module": "^2.8.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - } - } - }, - "tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true - }, - "temp-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", - "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", - "dev": true - }, - "tempy": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", - "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", - "dev": true, - "requires": { - "is-stream": "^2.0.0", - "temp-dir": "^2.0.0", - "type-fest": "^0.16.0", - "unique-string": "^2.0.0" - }, - "dependencies": { - "type-fest": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", - "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", - "dev": true - } - } - }, - "terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - } - }, - "terser": { - "version": "5.13.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.13.1.tgz", - "integrity": "sha512-hn4WKOfwnwbYfe48NgrQjqNOH9jzLqRcIfbYytOXCOv46LBfWr9bDS17MQqOi+BWGD0sJK3Sj5NC/gJjiojaoA==", - "dev": true, - "requires": { - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map": "~0.8.0-beta.0", - "source-map-support": "~0.5.20" - }, - "dependencies": { - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "dev": true, - "requires": { - "whatwg-url": "^7.0.0" - } - }, - "whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - } - } - }, - "terser-webpack-plugin": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz", - "integrity": "sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g==", - "dev": true, - "requires": { - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1", - "terser": "^5.7.2" - } - }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" - }, - "throat": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", - "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { - "is-number": "^7.0.0" - } - }, - "toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true - }, - "toposort": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", - "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=" - }, - "tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", - "dev": true, - "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.1.2" - }, - "dependencies": { - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true - } - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "requires": { - "punycode": "^2.1.0" - } - }, - "tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true - }, - "tryer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", - "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", - "dev": true - }, - "ts-jest": { - "version": "28.0.3", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.3.tgz", - "integrity": "sha512-HzgbEDQ2KgVtDmpXToqAcKTyGHdHsG23i/iUjfxji92G5eT09S1m9UHZd7csF0Bfgh9txM4JzwHnv7r1waFPlw==", - "dev": true, - "requires": { - "bs-logger": "0.x", - "fast-json-stable-stringify": "2.x", - "jest-util": "^28.0.0", - "json5": "^2.2.1", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "7.x", - "yargs-parser": "^20.x" - }, - "dependencies": { - "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "ts-node": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.0.tgz", - "integrity": "sha512-/fNd5Qh+zTt8Vt1KbYZjRHCE9sI5i7nqfD/dzBBRDeVXZXS6kToW6R7tTU6Nd4XavFs0mAVCg29Q//ML7WsZYA==", - "dev": true, - "requires": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "dependencies": { - "acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true - } - } - }, - "tsconfig-paths": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", - "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", - "requires": { - "@types/json5": "^0.0.29", - "json5": "^1.0.1", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "tslib": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "typescript": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", - "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", - "dev": true - }, - "unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", - "which-boxed-primitive": "^1.0.2" - } - }, - "unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "dev": true - }, - "unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "requires": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - } - }, - "unicode-match-property-value-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz", - "integrity": "sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==", - "dev": true - }, - "unicode-property-aliases-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz", - "integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==", - "dev": true - }, - "unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "dev": true, - "requires": { - "crypto-random-string": "^2.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "dev": true - }, - "unquote": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", - "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", - "dev": true - }, - "upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "requires": { - "punycode": "^2.1.0" - } - }, - "use-debounce": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-8.0.1.tgz", - "integrity": "sha512-6tGAFJKJ0qCalecaV7/gm/M6n238nmitNppvR89ff1yfwSFjwFKR7IQZzIZf1KZRQhqNireBzytzU6jgb29oVg==", - "requires": {} - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "util.promisify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", - "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.2", - "has-symbols": "^1.0.1", - "object.getownpropertydescriptors": "^2.1.0" - } - }, - "utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", - "dev": true - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "dev": true - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - }, - "v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==" - }, - "v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true - }, - "v8-to-istanbul": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.0.tgz", - "integrity": "sha512-HcvgY/xaRm7isYmyx+lFKA4uQmfUbN0J4M0nNItvzTvH/iQ9kW5j/t4YSR+Ge323/lrgDAWJoF46tzGQHwBHFw==", - "dev": true, - "peer": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.7", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" - } - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "dev": true - }, - "w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "dev": true, - "requires": { - "browser-process-hrtime": "^1.0.0" - } - }, - "w3c-xmlserializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", - "dev": true, - "requires": { - "xml-name-validator": "^3.0.0" - } - }, - "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", - "dev": true, - "requires": { - "makeerror": "1.0.x" - } - }, - "warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "requires": { - "loose-envify": "^1.0.0" - } - }, - "watchpack": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz", - "integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==", - "dev": true, - "requires": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - } - }, - "wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "requires": { - "minimalistic-assert": "^1.0.0" - } - }, - "wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", - "dev": true, - "requires": { - "defaults": "^1.0.3" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - }, - "webpack": { - "version": "5.72.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.72.1.tgz", - "integrity": "sha512-dXG5zXCLspQR4krZVR6QgajnZOjW2K/djHvdcRaDQvsjV9z9vaW6+ja5dZOYbqBBjF6kGXka/2ZyxNdc+8Jung==", - "dev": true, - "requires": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.4.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.9.3", - "es-module-lexer": "^0.9.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.3.1", - "webpack-sources": "^3.2.3" - } - }, - "webpack-dev-middleware": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.1.tgz", - "integrity": "sha512-81EujCKkyles2wphtdrnPg/QqegC/AtqNH//mQkBYSMqwFVCQrxM6ktB2O/SPlZy7LqeEfTbV3cZARGQz6umhg==", - "dev": true, - "requires": { - "colorette": "^2.0.10", - "memfs": "^3.4.1", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "dependencies": { - "ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.3" - } - }, - "colorette": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", - "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", - "dev": true - }, - "schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - } - } - } - }, - "webpack-dev-server": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.0.tgz", - "integrity": "sha512-+Nlb39iQSOSsFv0lWUuUTim3jDQO8nhK3E68f//J2r5rIcp4lULHXz2oZ0UVdEeWXEh5lSzYUlzarZhDAeAVQw==", - "dev": true, - "requires": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.1", - "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^1.6.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", - "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.0.1", - "serve-index": "^1.9.1", - "sockjs": "^0.3.21", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", - "ws": "^8.4.2" - }, - "dependencies": { - "@types/ws": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", - "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.3" - } - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, - "colorette": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", - "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", - "dev": true - }, - "schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - } - }, - "ws": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.6.0.tgz", - "integrity": "sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw==", - "dev": true, - "requires": {} - } - } - }, - "webpack-manifest-plugin": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz", - "integrity": "sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow==", - "dev": true, - "requires": { - "tapable": "^2.0.0", - "webpack-sources": "^2.2.0" - }, - "dependencies": { - "webpack-sources": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", - "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", - "dev": true, - "requires": { - "source-list-map": "^2.0.1", - "source-map": "^0.6.1" - } - } - } - }, - "webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "dev": true - }, - "websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "dev": true, - "requires": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - } - }, - "websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true - }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dev": true, - "requires": { - "iconv-lite": "0.4.24" - } - }, - "whatwg-fetch": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", - "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", - "dev": true - }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true - }, - "whatwg-url": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", - "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - }, - "which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" - }, - "workbox-background-sync": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.5.3.tgz", - "integrity": "sha512-0DD/V05FAcek6tWv9XYj2w5T/plxhDSpclIcAGjA/b7t/6PdaRkQ7ZgtAX6Q/L7kV7wZ8uYRJUoH11VjNipMZw==", - "dev": true, - "requires": { - "idb": "^6.1.4", - "workbox-core": "6.5.3" - } - }, - "workbox-broadcast-update": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.5.3.tgz", - "integrity": "sha512-4AwCIA5DiDrYhlN+Miv/fp5T3/whNmSL+KqhTwRBTZIL6pvTgE4lVuRzAt1JltmqyMcQ3SEfCdfxczuI4kwFQg==", - "dev": true, - "requires": { - "workbox-core": "6.5.3" - } - }, - "workbox-build": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.5.3.tgz", - "integrity": "sha512-8JNHHS7u13nhwIYCDea9MNXBNPHXCs5KDZPKI/ZNTr3f4sMGoD7hgFGecbyjX1gw4z6e9bMpMsOEJNyH5htA/w==", - "dev": true, - "requires": { - "@apideck/better-ajv-errors": "^0.3.1", - "@babel/core": "^7.11.1", - "@babel/preset-env": "^7.11.0", - "@babel/runtime": "^7.11.2", - "@rollup/plugin-babel": "^5.2.0", - "@rollup/plugin-node-resolve": "^11.2.1", - "@rollup/plugin-replace": "^2.4.1", - "@surma/rollup-plugin-off-main-thread": "^2.2.3", - "ajv": "^8.6.0", - "common-tags": "^1.8.0", - "fast-json-stable-stringify": "^2.1.0", - "fs-extra": "^9.0.1", - "glob": "^7.1.6", - "lodash": "^4.17.20", - "pretty-bytes": "^5.3.0", - "rollup": "^2.43.1", - "rollup-plugin-terser": "^7.0.0", - "source-map": "^0.8.0-beta.0", - "stringify-object": "^3.3.0", - "strip-comments": "^2.0.1", - "tempy": "^0.6.0", - "upath": "^1.2.0", - "workbox-background-sync": "6.5.3", - "workbox-broadcast-update": "6.5.3", - "workbox-cacheable-response": "6.5.3", - "workbox-core": "6.5.3", - "workbox-expiration": "6.5.3", - "workbox-google-analytics": "6.5.3", - "workbox-navigation-preload": "6.5.3", - "workbox-precaching": "6.5.3", - "workbox-range-requests": "6.5.3", - "workbox-recipes": "6.5.3", - "workbox-routing": "6.5.3", - "workbox-strategies": "6.5.3", - "workbox-streams": "6.5.3", - "workbox-sw": "6.5.3", - "workbox-window": "6.5.3" - }, - "dependencies": { - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "dev": true, - "requires": { - "whatwg-url": "^7.0.0" - } - }, - "whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - } - } - }, - "workbox-cacheable-response": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.5.3.tgz", - "integrity": "sha512-6JE/Zm05hNasHzzAGKDkqqgYtZZL2H06ic2GxuRLStA4S/rHUfm2mnLFFXuHAaGR1XuuYyVCEey1M6H3PdZ7SQ==", - "dev": true, - "requires": { - "workbox-core": "6.5.3" - } - }, - "workbox-core": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.5.3.tgz", - "integrity": "sha512-Bb9ey5n/M9x+l3fBTlLpHt9ASTzgSGj6vxni7pY72ilB/Pb3XtN+cZ9yueboVhD5+9cNQrC9n/E1fSrqWsUz7Q==", - "dev": true - }, - "workbox-expiration": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.5.3.tgz", - "integrity": "sha512-jzYopYR1zD04ZMdlbn/R2Ik6ixiXbi15c9iX5H8CTi6RPDz7uhvMLZPKEndZTpfgmUk8mdmT9Vx/AhbuCl5Sqw==", - "dev": true, - "requires": { - "idb": "^6.1.4", - "workbox-core": "6.5.3" - } - }, - "workbox-google-analytics": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.5.3.tgz", - "integrity": "sha512-3GLCHotz5umoRSb4aNQeTbILETcrTVEozSfLhHSBaegHs1PnqCmN0zbIy2TjTpph2AGXiNwDrWGF0AN+UgDNTw==", - "dev": true, - "requires": { - "workbox-background-sync": "6.5.3", - "workbox-core": "6.5.3", - "workbox-routing": "6.5.3", - "workbox-strategies": "6.5.3" - } - }, - "workbox-navigation-preload": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.5.3.tgz", - "integrity": "sha512-bK1gDFTc5iu6lH3UQ07QVo+0ovErhRNGvJJO/1ngknT0UQ702nmOUhoN9qE5mhuQSrnK+cqu7O7xeaJ+Rd9Tmg==", - "dev": true, - "requires": { - "workbox-core": "6.5.3" - } - }, - "workbox-precaching": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.5.3.tgz", - "integrity": "sha512-sjNfgNLSsRX5zcc63H/ar/hCf+T19fRtTqvWh795gdpghWb5xsfEkecXEvZ8biEi1QD7X/ljtHphdaPvXDygMQ==", - "dev": true, - "requires": { - "workbox-core": "6.5.3", - "workbox-routing": "6.5.3", - "workbox-strategies": "6.5.3" - } - }, - "workbox-range-requests": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.5.3.tgz", - "integrity": "sha512-pGCP80Bpn/0Q0MQsfETSfmtXsQcu3M2QCJwSFuJ6cDp8s2XmbUXkzbuQhCUzKR86ZH2Vex/VUjb2UaZBGamijA==", - "dev": true, - "requires": { - "workbox-core": "6.5.3" - } - }, - "workbox-recipes": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.5.3.tgz", - "integrity": "sha512-IcgiKYmbGiDvvf3PMSEtmwqxwfQ5zwI7OZPio3GWu4PfehA8jI8JHI3KZj+PCfRiUPZhjQHJ3v1HbNs+SiSkig==", - "dev": true, - "requires": { - "workbox-cacheable-response": "6.5.3", - "workbox-core": "6.5.3", - "workbox-expiration": "6.5.3", - "workbox-precaching": "6.5.3", - "workbox-routing": "6.5.3", - "workbox-strategies": "6.5.3" - } - }, - "workbox-routing": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.5.3.tgz", - "integrity": "sha512-DFjxcuRAJjjt4T34RbMm3MCn+xnd36UT/2RfPRfa8VWJGItGJIn7tG+GwVTdHmvE54i/QmVTJepyAGWtoLPTmg==", - "dev": true, - "requires": { - "workbox-core": "6.5.3" - } - }, - "workbox-strategies": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.5.3.tgz", - "integrity": "sha512-MgmGRrDVXs7rtSCcetZgkSZyMpRGw8HqL2aguszOc3nUmzGZsT238z/NN9ZouCxSzDu3PQ3ZSKmovAacaIhu1w==", - "dev": true, - "requires": { - "workbox-core": "6.5.3" - } - }, - "workbox-streams": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.5.3.tgz", - "integrity": "sha512-vN4Qi8o+b7zj1FDVNZ+PlmAcy1sBoV7SC956uhqYvZ9Sg1fViSbOpydULOssVJ4tOyKRifH/eoi6h99d+sJ33w==", - "dev": true, - "requires": { - "workbox-core": "6.5.3", - "workbox-routing": "6.5.3" - } - }, - "workbox-sw": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.5.3.tgz", - "integrity": "sha512-BQBzm092w+NqdIEF2yhl32dERt9j9MDGUTa2Eaa+o3YKL4Qqw55W9yQC6f44FdAHdAJrJvp0t+HVrfh8AiGj8A==", - "dev": true - }, - "workbox-webpack-plugin": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.5.3.tgz", - "integrity": "sha512-Es8Xr02Gi6Kc3zaUwR691ZLy61hz3vhhs5GztcklQ7kl5k2qAusPh0s6LF3wEtlpfs9ZDErnmy5SErwoll7jBA==", - "dev": true, - "requires": { - "fast-json-stable-stringify": "^2.1.0", - "pretty-bytes": "^5.4.1", - "upath": "^1.2.0", - "webpack-sources": "^1.4.3", - "workbox-build": "6.5.3" - }, - "dependencies": { - "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - } - } - }, - "workbox-window": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.5.3.tgz", - "integrity": "sha512-GnJbx1kcKXDtoJBVZs/P7ddP0Yt52NNy4nocjBpYPiRhMqTpJCNrSL+fGHZ/i/oP6p/vhE8II0sA6AZGKGnssw==", - "dev": true, - "requires": { - "@types/trusted-types": "^2.0.2", - "workbox-core": "6.5.3" - } - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write-file-atomic": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.1.tgz", - "integrity": "sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==", - "dev": true, - "peer": true, - "requires": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - } - }, - "ws": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", - "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", - "dev": true, - "requires": {} - }, - "xml": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", - "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", - "dev": true - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, - "xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.7", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", - "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", - "dev": true - }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true - }, - "yup": { - "version": "0.32.11", - "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz", - "integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==", - "requires": { - "@babel/runtime": "^7.15.4", - "@types/lodash": "^4.14.175", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "nanoclone": "^0.2.1", - "property-expr": "^2.0.4", - "toposort": "^2.0.2" - }, - "dependencies": { - "@babel/runtime": { - "version": "7.16.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz", - "integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==", - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "@types/lodash": { - "version": "4.14.177", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.177.tgz", - "integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==" - } - } - } - } -} diff --git a/kafka-ui-react-app/package.json b/kafka-ui-react-app/package.json index f30fba154b7..172ec4466ab 100644 --- a/kafka-ui-react-app/package.json +++ b/kafka-ui-react-app/package.json @@ -4,123 +4,109 @@ "homepage": "./", "private": true, "dependencies": { - "@fortawesome/fontawesome-free": "^6.1.1", + "@floating-ui/react": "^0.19.2", "@hookform/error-message": "^2.0.0", "@hookform/resolvers": "^2.7.1", - "@reduxjs/toolkit": "^1.8.1", - "@rooks/use-outside-click-ref": "^4.10.1", - "@testing-library/react": "^13.2.0", - "@types/yup": "^0.29.13", - "ace-builds": "^1.4.12", + "@microsoft/fetch-event-source": "^2.0.1", + "@reduxjs/toolkit": "^1.8.3", + "@szhsin/react-menu": "^3.5.3", + "@tanstack/react-query": "^4.0.5", + "@tanstack/react-table": "^8.5.10", + "@testing-library/react": "^14.0.0", + "@types/testing-library__jest-dom": "^5.14.5", + "ace-builds": "^1.7.1", "ajv": "^8.6.3", - "bulma": "^0.9.3", + "ajv-formats": "^2.1.1", "classnames": "^2.2.6", - "dayjs": "^1.11.2", - "eslint-import-resolver-node": "^0.3.6", - "eslint-import-resolver-typescript": "^2.7.1", "fetch-mock": "^9.11.0", - "json-schema-faker": "^0.5.0-rcv.39", + "jest": "^29.4.3", + "jest-watch-typeahead": "^2.2.2", + "json-schema-faker": "^0.5.0-rcv.44", + "jsonpath-plus": "^7.2.0", "lodash": "^4.17.21", - "node-fetch": "^2.6.1", - "pretty-ms": "^7.0.1", + "lossless-json": "^2.0.8", + "pretty-ms": "7.0.1", "react": "^18.1.0", - "react-ace": "^9.4.3", - "react-datepicker": "^4.2.0", + "react-ace": "^10.1.0", + "react-datepicker": "^4.10.0", "react-dom": "^18.1.0", - "react-hook-form": "7.6.9", - "react-multi-select-component": "^4.0.6", - "react-redux": "^7.2.6", + "react-error-boundary": "^3.1.4", + "react-hook-form": "7.43.1", + "react-hot-toast": "^2.4.0", + "react-is": "^18.2.0", + "react-multi-select-component": "^4.3.3", + "react-redux": "^8.0.2", "react-router-dom": "^6.3.0", - "redux": "^4.1.1", - "redux-thunk": "^2.3.0", - "sass": "^1.43.4", + "redux": "^4.2.0", + "sass": "^1.52.3", "styled-components": "^5.3.1", - "use-debounce": "^8.0.1", - "uuid": "^8.3.1", - "yup": "^0.32.9" - }, - "lint-staged": { - "*.{js,ts,jsx,tsx}": [ - "eslint -c .eslintrc.json --fix", - "npm test -- --bail --findRelatedTests --watchAll=false" - ] + "use-debounce": "^9.0.3", + "vite": "^4.0.0", + "vite-tsconfig-paths": "^4.0.2", + "whatwg-fetch": "^3.6.2", + "yup": "^1.0.0", + "zustand": "^4.1.1" }, "scripts": { - "start": "react-scripts start", + "start": "vite", + "dev": "vite", "gen:sources": "rimraf src/generated-sources && openapi-generator-cli generate", - "build": "react-scripts build", + "build": "vite build", + "preview": "vite preview", "lint": "eslint --ext .tsx,.ts src/", "lint:fix": "eslint --ext .tsx,.ts src/ --fix", "lint:CI": "eslint --ext .tsx,.ts src/ --max-warnings=0", - "test": "react-scripts test", - "test:CI": "CI=true npm test -- --coverage --ci --testResultsProcessor=\"jest-sonar-reporter\" --watchAll=false", - "eject": "react-scripts eject", - "tsc": "tsc", - "prepare": "cd .. && husky install kafka-ui-react-app/.husky", - "pre-commit": "npm run tsc --noEmit && lint-staged" - }, - "eslintConfig": { - "extends": "react-app" - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] + "test": "jest --watch", + "test:coverage": "jest --watchAll --coverage", + "test:CI": "CI=true pnpm test:coverage --ci --testResultsProcessor=\"jest-sonar-reporter\" --watchAll=false", + "tsc": "tsc --pretty --noEmit", + "deadcode": "ts-prune -i src/generated-sources" }, "devDependencies": { - "@jest/types": "^28.1.0", - "@openapitools/openapi-generator-cli": "^2.5.1", - "@testing-library/dom": "^8.11.1", - "@testing-library/jest-dom": "^5.16.4", - "@testing-library/user-event": "^13.5.0", + "@jest/types": "^29.4.3", + "@openapitools/openapi-generator-cli": "^2.5.2", + "@swc/core": "^1.3.36", + "@swc/jest": "^0.2.24", + "@testing-library/dom": "^9.0.0", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/user-event": "^14.4.3", "@types/eventsource": "^1.1.8", - "@types/jest": "^27.5.1", "@types/lodash": "^4.14.172", + "@types/lossless-json": "^1.0.1", "@types/node": "^16.4.13", "@types/react": "^18.0.9", - "@types/react-datepicker": "^4.1.4", + "@types/react-datepicker": "^4.8.0", "@types/react-dom": "^18.0.3", - "@types/react-redux": "^7.1.18", "@types/react-router-dom": "^5.3.3", - "@types/redux-mock-store": "^1.0.3", "@types/styled-components": "^5.1.13", - "@types/uuid": "^8.3.1", - "@typescript-eslint/eslint-plugin": "^5.10.0", - "@typescript-eslint/parser": "^5.27.0", + "@typescript-eslint/eslint-plugin": "^5.29.0", + "@typescript-eslint/parser": "^5.29.0", + "@vitejs/plugin-react-swc": "^3.0.0", "dotenv": "^16.0.1", - "eslint": "^8.15.0", + "eslint": "^8.3.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-prettier": "^8.5.0", + "eslint-config-prettier": "^9.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.2.7", "eslint-plugin-import": "^2.26.0", - "eslint-plugin-jest-dom": "^4.0.2", + "eslint-plugin-jest-dom": "^4.0.3", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.29.4", + "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.5.0", - "fetch-mock-jest": "^1.5.1", - "http-proxy-middleware": "^2.0.6", - "husky": "^7.0.1", + "jest-environment-jsdom": "^29.4.3", "jest-sonar-reporter": "^2.0.0", - "jest-styled-components": "^7.0.8", - "lint-staged": "^12.1.2", - "prettier": "^2.3.1", - "react-scripts": "5.0.1", - "redux-mock-store": "^1.5.4", - "rimraf": "^3.0.2", - "ts-jest": "^28.0.3", - "ts-node": "^10.8.0", - "typescript": "^4.3.5" + "jest-styled-components": "^7.1.1", + "prettier": "^2.8.4", + "rimraf": "^4.1.2", + "ts-node": "^10.9.1", + "ts-prune": "^0.10.3", + "typescript": "^4.7.4", + "vite-plugin-ejs": "^1.6.4" }, "engines": { - "node": "v16.15.0", - "npm": "8.5.5" + "node": "v18.17.1", + "pnpm": "^8.6.12" } } diff --git a/kafka-ui-react-app/pnpm-lock.yaml b/kafka-ui-react-app/pnpm-lock.yaml new file mode 100644 index 00000000000..01862dd3bbe --- /dev/null +++ b/kafka-ui-react-app/pnpm-lock.yaml @@ -0,0 +1,7006 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@floating-ui/react': + specifier: ^0.19.2 + version: 0.19.2(@types/react@18.2.21)(react-dom@18.1.0)(react@18.2.0) + '@hookform/error-message': + specifier: ^2.0.0 + version: 2.0.1(react-dom@18.1.0)(react-hook-form@7.43.1)(react@18.2.0) + '@hookform/resolvers': + specifier: ^2.7.1 + version: 2.8.9(react-hook-form@7.43.1) + '@microsoft/fetch-event-source': + specifier: ^2.0.1 + version: 2.0.1 + '@reduxjs/toolkit': + specifier: ^1.8.3 + version: 1.8.3(react-redux@8.0.2)(react@18.2.0) + '@szhsin/react-menu': + specifier: ^3.5.3 + version: 3.5.3(react-dom@18.1.0)(react@18.2.0) + '@tanstack/react-query': + specifier: ^4.0.5 + version: 4.0.5(react-dom@18.1.0)(react@18.2.0) + '@tanstack/react-table': + specifier: ^8.5.10 + version: 8.5.10(react-dom@18.1.0)(react@18.2.0) + '@testing-library/react': + specifier: ^14.0.0 + version: 14.0.0(react-dom@18.1.0)(react@18.2.0) + '@types/testing-library__jest-dom': + specifier: ^5.14.5 + version: 5.14.5 + ace-builds: + specifier: ^1.7.1 + version: 1.7.1 + ajv: + specifier: ^8.6.3 + version: 8.8.2 + ajv-formats: + specifier: ^2.1.1 + version: 2.1.1(ajv@8.8.2) + classnames: + specifier: ^2.2.6 + version: 2.3.1 + fetch-mock: + specifier: ^9.11.0 + version: 9.11.0 + jest: + specifier: ^29.4.3 + version: 29.6.4(@types/node@16.11.7)(ts-node@10.9.1) + jest-watch-typeahead: + specifier: ^2.2.2 + version: 2.2.2(jest@29.6.4) + json-schema-faker: + specifier: ^0.5.0-rcv.44 + version: 0.5.3 + jsonpath-plus: + specifier: ^7.2.0 + version: 7.2.0 + lodash: + specifier: ^4.17.21 + version: 4.17.21 + lossless-json: + specifier: ^2.0.8 + version: 2.0.11 + pretty-ms: + specifier: 7.0.1 + version: 7.0.1 + react: + specifier: ^18.1.0 + version: 18.2.0 + react-ace: + specifier: ^10.1.0 + version: 10.1.0(react-dom@18.1.0)(react@18.2.0) + react-datepicker: + specifier: ^4.10.0 + version: 4.10.0(react-dom@18.1.0)(react@18.2.0) + react-dom: + specifier: ^18.1.0 + version: 18.1.0(react@18.2.0) + react-error-boundary: + specifier: ^3.1.4 + version: 3.1.4(react@18.2.0) + react-hook-form: + specifier: 7.43.1 + version: 7.43.1(react@18.2.0) + react-hot-toast: + specifier: ^2.4.0 + version: 2.4.1(csstype@3.1.2)(react-dom@18.1.0)(react@18.2.0) + react-is: + specifier: ^18.2.0 + version: 18.2.0 + react-multi-select-component: + specifier: ^4.3.3 + version: 4.3.3(react-dom@18.1.0)(react@18.2.0) + react-redux: + specifier: ^8.0.2 + version: 8.0.2(@types/react-dom@18.0.5)(@types/react@18.2.21)(react-dom@18.1.0)(react@18.2.0)(redux@4.2.0) + react-router-dom: + specifier: ^6.3.0 + version: 6.15.0(react-dom@18.1.0)(react@18.2.0) + redux: + specifier: ^4.2.0 + version: 4.2.0 + sass: + specifier: ^1.52.3 + version: 1.66.1 + styled-components: + specifier: ^5.3.1 + version: 5.3.1(react-dom@18.1.0)(react-is@18.2.0)(react@18.2.0) + use-debounce: + specifier: ^9.0.3 + version: 9.0.4(react@18.2.0) + vite: + specifier: ^4.0.0 + version: 4.0.5(@types/node@16.11.7)(sass@1.66.1) + vite-tsconfig-paths: + specifier: ^4.0.2 + version: 4.2.1(typescript@4.7.4)(vite@4.0.5) + whatwg-fetch: + specifier: ^3.6.2 + version: 3.6.2 + yup: + specifier: ^1.0.0 + version: 1.0.2 + zustand: + specifier: ^4.1.1 + version: 4.1.1(react@18.2.0) + +devDependencies: + '@jest/types': + specifier: ^29.4.3 + version: 29.6.3 + '@openapitools/openapi-generator-cli': + specifier: ^2.5.2 + version: 2.7.0 + '@swc/core': + specifier: ^1.3.36 + version: 1.3.38 + '@swc/jest': + specifier: ^0.2.24 + version: 0.2.24(@swc/core@1.3.38) + '@testing-library/dom': + specifier: ^9.0.0 + version: 9.3.1 + '@testing-library/jest-dom': + specifier: ^5.16.5 + version: 5.16.5 + '@testing-library/user-event': + specifier: ^14.4.3 + version: 14.4.3(@testing-library/dom@9.3.1) + '@types/eventsource': + specifier: ^1.1.8 + version: 1.1.11 + '@types/lodash': + specifier: ^4.14.172 + version: 4.14.177 + '@types/lossless-json': + specifier: ^1.0.1 + version: 1.0.2 + '@types/node': + specifier: ^16.4.13 + version: 16.11.7 + '@types/react': + specifier: ^18.0.9 + version: 18.2.21 + '@types/react-datepicker': + specifier: ^4.8.0 + version: 4.10.0(react@18.2.0) + '@types/react-dom': + specifier: ^18.0.3 + version: 18.0.5 + '@types/react-router-dom': + specifier: ^5.3.3 + version: 5.3.3 + '@types/styled-components': + specifier: ^5.1.13 + version: 5.1.18 + '@typescript-eslint/eslint-plugin': + specifier: ^5.29.0 + version: 5.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.48.0)(typescript@4.7.4) + '@typescript-eslint/parser': + specifier: ^5.29.0 + version: 5.62.0(eslint@8.48.0)(typescript@4.7.4) + '@vitejs/plugin-react-swc': + specifier: ^3.0.0 + version: 3.0.0(vite@4.0.5) + dotenv: + specifier: ^16.0.1 + version: 16.0.1 + eslint: + specifier: ^8.3.0 + version: 8.48.0 + eslint-config-airbnb: + specifier: ^19.0.4 + version: 19.0.4(eslint-plugin-import@2.26.0)(eslint-plugin-jsx-a11y@6.7.1)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.30.1)(eslint@8.48.0) + eslint-config-airbnb-typescript: + specifier: ^17.0.0 + version: 17.0.0(@typescript-eslint/eslint-plugin@5.29.0)(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.26.0)(eslint@8.48.0) + eslint-config-prettier: + specifier: ^9.0.0 + version: 9.0.0(eslint@8.48.0) + eslint-import-resolver-node: + specifier: ^0.3.6 + version: 0.3.9 + eslint-import-resolver-typescript: + specifier: ^3.2.7 + version: 3.2.7(eslint-plugin-import@2.26.0)(eslint@8.48.0) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.26.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.2.7)(eslint@8.48.0) + eslint-plugin-jest-dom: + specifier: ^4.0.3 + version: 4.0.3(eslint@8.48.0) + eslint-plugin-jsx-a11y: + specifier: ^6.5.1 + version: 6.7.1(eslint@8.48.0) + eslint-plugin-prettier: + specifier: ^4.0.0 + version: 4.2.1(eslint-config-prettier@9.0.0)(eslint@8.48.0)(prettier@2.8.4) + eslint-plugin-react: + specifier: ^7.30.1 + version: 7.30.1(eslint@8.48.0) + eslint-plugin-react-hooks: + specifier: ^4.5.0 + version: 4.6.0(eslint@8.48.0) + jest-environment-jsdom: + specifier: ^29.4.3 + version: 29.6.4 + jest-sonar-reporter: + specifier: ^2.0.0 + version: 2.0.0 + jest-styled-components: + specifier: ^7.1.1 + version: 7.1.1(styled-components@5.3.1) + prettier: + specifier: ^2.8.4 + version: 2.8.4 + rimraf: + specifier: ^4.1.2 + version: 4.3.1 + ts-node: + specifier: ^10.9.1 + version: 10.9.1(@swc/core@1.3.38)(@types/node@16.11.7)(typescript@4.7.4) + ts-prune: + specifier: ^0.10.3 + version: 0.10.3 + typescript: + specifier: ^4.7.4 + version: 4.7.4 + vite-plugin-ejs: + specifier: ^1.6.4 + version: 1.6.4 + +packages: + + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: true + + /@adobe/css-tools@4.2.0: + resolution: {integrity: sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==} + dev: true + + /@ampproject/remapping@2.2.0: + resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.1.1 + '@jridgewell/trace-mapping': 0.3.19 + dev: false + + /@babel/code-frame@7.18.6: + resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.18.6 + + /@babel/compat-data@7.18.8: + resolution: {integrity: sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/core@7.18.2: + resolution: {integrity: sha512-A8pri1YJiC5UnkdrWcmfZTJTV85b4UXTAfImGmCfYmax4TR9Cw8sDS0MOk++Gp2mE/BefVJ5nwy5yzqNJbP/DQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.0 + '@babel/code-frame': 7.18.6 + '@babel/generator': 7.18.9 + '@babel/helper-compilation-targets': 7.18.9(@babel/core@7.18.2) + '@babel/helper-module-transforms': 7.18.9 + '@babel/helpers': 7.18.9 + '@babel/parser': 7.18.9 + '@babel/template': 7.18.6 + '@babel/traverse': 7.18.9 + '@babel/types': 7.18.9 + convert-source-map: 1.7.0 + debug: 4.3.4(supports-color@5.5.0) + gensync: 1.0.0-beta.2 + json5: 2.2.1 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/core@7.18.9: + resolution: {integrity: sha512-1LIb1eL8APMy91/IMW+31ckrfBM4yCoLaVzoDhZUKSM4cu1L1nIidyxkCgzPAgrC5WEz36IPEr/eSeSF9pIn+g==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.0 + '@babel/code-frame': 7.18.6 + '@babel/generator': 7.18.9 + '@babel/helper-compilation-targets': 7.18.9(@babel/core@7.18.9) + '@babel/helper-module-transforms': 7.18.9 + '@babel/helpers': 7.18.9 + '@babel/parser': 7.18.9 + '@babel/template': 7.18.6 + '@babel/traverse': 7.18.9 + '@babel/types': 7.18.9 + convert-source-map: 1.7.0 + debug: 4.3.4(supports-color@5.5.0) + gensync: 1.0.0-beta.2 + json5: 2.2.1 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/generator@7.18.9: + resolution: {integrity: sha512-wt5Naw6lJrL1/SGkipMiFxJjtyczUWTP38deiP1PO60HsBjDeKk08CGC3S8iVuvf0FmTdgKwU1KIXzSKL1G0Ug==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.18.9 + '@jridgewell/gen-mapping': 0.3.2 + jsesc: 2.5.2 + + /@babel/helper-annotate-as-pure@7.18.6: + resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.18.9 + + /@babel/helper-compilation-targets@7.18.9(@babel/core@7.18.2): + resolution: {integrity: sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/compat-data': 7.18.8 + '@babel/core': 7.18.2 + '@babel/helper-validator-option': 7.18.6 + browserslist: 4.20.4 + semver: 6.3.1 + dev: false + + /@babel/helper-compilation-targets@7.18.9(@babel/core@7.18.9): + resolution: {integrity: sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/compat-data': 7.18.8 + '@babel/core': 7.18.9 + '@babel/helper-validator-option': 7.18.6 + browserslist: 4.20.4 + semver: 6.3.1 + dev: false + + /@babel/helper-environment-visitor@7.18.9: + resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} + engines: {node: '>=6.9.0'} + + /@babel/helper-function-name@7.18.9: + resolution: {integrity: sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.18.6 + '@babel/types': 7.18.9 + + /@babel/helper-hoist-variables@7.18.6: + resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.18.9 + + /@babel/helper-module-imports@7.16.7: + resolution: {integrity: sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.18.9 + + /@babel/helper-module-imports@7.18.6: + resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.18.9 + + /@babel/helper-module-transforms@7.18.9: + resolution: {integrity: sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-module-imports': 7.18.6 + '@babel/helper-simple-access': 7.18.6 + '@babel/helper-split-export-declaration': 7.18.6 + '@babel/helper-validator-identifier': 7.18.6 + '@babel/template': 7.18.6 + '@babel/traverse': 7.18.9 + '@babel/types': 7.18.9 + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/helper-plugin-utils@7.18.6: + resolution: {integrity: sha512-gvZnm1YAAxh13eJdkb9EWHBnF3eAub3XTLCZEehHT2kWxiKVRL64+ae5Y6Ivne0mVHmMYKT+xWgZO+gQhuLUBg==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/helper-simple-access@7.18.6: + resolution: {integrity: sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.18.9 + dev: false + + /@babel/helper-split-export-declaration@7.18.6: + resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.18.9 + + /@babel/helper-validator-identifier@7.18.6: + resolution: {integrity: sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==} + engines: {node: '>=6.9.0'} + + /@babel/helper-validator-option@7.18.6: + resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/helpers@7.18.9: + resolution: {integrity: sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.18.6 + '@babel/traverse': 7.18.9 + '@babel/types': 7.18.9 + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/highlight@7.18.6: + resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.18.6 + chalk: 2.4.2 + js-tokens: 4.0.0 + + /@babel/parser@7.18.9: + resolution: {integrity: sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.18.9 + + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.18.9): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.9 + '@babel/helper-plugin-utils': 7.18.6 + dev: false + + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.18.9): + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.9 + '@babel/helper-plugin-utils': 7.18.6 + dev: false + + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.18.9): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.9 + '@babel/helper-plugin-utils': 7.18.6 + dev: false + + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.18.9): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.9 + '@babel/helper-plugin-utils': 7.18.6 + dev: false + + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.18.9): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.9 + '@babel/helper-plugin-utils': 7.18.6 + dev: false + + /@babel/plugin-syntax-jsx@7.18.6(@babel/core@7.18.9): + resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.9 + '@babel/helper-plugin-utils': 7.18.6 + dev: false + + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.18.9): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.9 + '@babel/helper-plugin-utils': 7.18.6 + dev: false + + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.18.9): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.9 + '@babel/helper-plugin-utils': 7.18.6 + dev: false + + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.18.9): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.9 + '@babel/helper-plugin-utils': 7.18.6 + dev: false + + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.18.9): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.9 + '@babel/helper-plugin-utils': 7.18.6 + dev: false + + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.18.9): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.9 + '@babel/helper-plugin-utils': 7.18.6 + dev: false + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.18.9): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.9 + '@babel/helper-plugin-utils': 7.18.6 + dev: false + + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.18.9): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.9 + '@babel/helper-plugin-utils': 7.18.6 + dev: false + + /@babel/plugin-syntax-typescript@7.17.12(@babel/core@7.18.9): + resolution: {integrity: sha512-TYY0SXFiO31YXtNg3HtFwNJHjLsAyIIhAhNWkQ5whPPS7HWUFlg9z0Ta4qAQNjQbP1wsSt/oKkmZ/4/WWdMUpw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.9 + '@babel/helper-plugin-utils': 7.18.6 + dev: false + + /@babel/runtime@7.17.9: + resolution: {integrity: sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.7 + + /@babel/runtime@7.22.11: + resolution: {integrity: sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.0 + + /@babel/template@7.18.6: + resolution: {integrity: sha512-JoDWzPe+wgBsTTgdnIma3iHNFC7YVJoPssVBDjiHfNlyt4YcunDtcDOUmfVDfCK5MfdsaIoX9PkijPhjH3nYUw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/parser': 7.18.9 + '@babel/types': 7.18.9 + + /@babel/traverse@7.18.2(supports-color@5.5.0): + resolution: {integrity: sha512-9eNwoeovJ6KH9zcCNnENY7DMFwTU9JdGCFtqNLfUAqtUHRCOsTOqWoffosP8vKmNYeSBUv3yVJXjfd8ucwOjUA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/generator': 7.18.9 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-function-name': 7.18.9 + '@babel/helper-hoist-variables': 7.18.6 + '@babel/helper-split-export-declaration': 7.18.6 + '@babel/parser': 7.18.9 + '@babel/types': 7.18.9 + debug: 4.3.4(supports-color@5.5.0) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + /@babel/traverse@7.18.9: + resolution: {integrity: sha512-LcPAnujXGwBgv3/WHv01pHtb2tihcyW1XuL9wd7jqh1Z8AQkTd+QVjMrMijrln0T7ED3UXLIy36P9Ao7W75rYg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/generator': 7.18.9 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-function-name': 7.18.9 + '@babel/helper-hoist-variables': 7.18.6 + '@babel/helper-split-export-declaration': 7.18.6 + '@babel/parser': 7.18.9 + '@babel/types': 7.18.9 + debug: 4.3.4(supports-color@5.5.0) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/types@7.18.9: + resolution: {integrity: sha512-WwMLAg2MvJmt/rKEVQBBhIVffMmnilX4oe0sRe7iPOHIGsqpruFHHdrfj4O1CMMtgMtCU4oPafZjDPCRgO57Wg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.18.6 + to-fast-properties: 2.0.0 + + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: false + + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + /@emotion/is-prop-valid@0.8.8: + resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} + dependencies: + '@emotion/memoize': 0.7.4 + + /@emotion/memoize@0.7.4: + resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} + + /@emotion/stylis@0.8.5: + resolution: {integrity: sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==} + + /@emotion/unitless@0.7.5: + resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==} + + /@esbuild/android-arm64@0.16.4: + resolution: {integrity: sha512-VPuTzXFm/m2fcGfN6CiwZTlLzxrKsWbPkG7ArRFpuxyaHUm/XFHQPD4xNwZT6uUmpIHhnSjcaCmcla8COzmZ5Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + optional: true + + /@esbuild/android-arm@0.16.4: + resolution: {integrity: sha512-rZzb7r22m20S1S7ufIc6DC6W659yxoOrl7sKP1nCYhuvUlnCFHVSbATG4keGUtV8rDz11sRRDbWkvQZpzPaHiw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + optional: true + + /@esbuild/android-x64@0.16.4: + resolution: {integrity: sha512-MW+B2O++BkcOfMWmuHXB15/l1i7wXhJFqbJhp82IBOais8RBEQv2vQz/jHrDEHaY2X0QY7Wfw86SBL2PbVOr0g==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + optional: true + + /@esbuild/darwin-arm64@0.16.4: + resolution: {integrity: sha512-a28X1O//aOfxwJVZVs7ZfM8Tyih2Za4nKJrBwW5Wm4yKsnwBy9aiS/xwpxiiTRttw3EaTg4Srerhcm6z0bu9Wg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optional: true + + /@esbuild/darwin-x64@0.16.4: + resolution: {integrity: sha512-e3doCr6Ecfwd7VzlaQqEPrnbvvPjE9uoTpxG5pyLzr2rI2NMjDHmvY1E5EO81O/e9TUOLLkXA5m6T8lfjK9yAA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + optional: true + + /@esbuild/freebsd-arm64@0.16.4: + resolution: {integrity: sha512-Oup3G/QxBgvvqnXWrBed7xxkFNwAwJVHZcklWyQt7YCAL5bfUkaa6FVWnR78rNQiM8MqqLiT6ZTZSdUFuVIg1w==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + optional: true + + /@esbuild/freebsd-x64@0.16.4: + resolution: {integrity: sha512-vAP+eYOxlN/Bpo/TZmzEQapNS8W1njECrqkTpNgvXskkkJC2AwOXwZWai/Kc2vEFZUXQttx6UJbj9grqjD/+9Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + optional: true + + /@esbuild/linux-arm64@0.16.4: + resolution: {integrity: sha512-2zXoBhv4r5pZiyjBKrOdFP4CXOChxXiYD50LRUU+65DkdS5niPFHbboKZd/c81l0ezpw7AQnHeoCy5hFrzzs4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-arm@0.16.4: + resolution: {integrity: sha512-A47ZmtpIPyERxkSvIv+zLd6kNIOtJH03XA0Hy7jaceRDdQaQVGSDt4mZqpWqJYgDk9rg96aglbF6kCRvPGDSUA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-ia32@0.16.4: + resolution: {integrity: sha512-uxdSrpe9wFhz4yBwt2kl2TxS/NWEINYBUFIxQtaEVtglm1eECvsj1vEKI0KX2k2wCe17zDdQ3v+jVxfwVfvvjw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-loong64@0.16.4: + resolution: {integrity: sha512-peDrrUuxbZ9Jw+DwLCh/9xmZAk0p0K1iY5d2IcwmnN+B87xw7kujOkig6ZRcZqgrXgeRGurRHn0ENMAjjD5DEg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-mips64el@0.16.4: + resolution: {integrity: sha512-sD9EEUoGtVhFjjsauWjflZklTNr57KdQ6xfloO4yH1u7vNQlOfAlhEzbyBKfgbJlW7rwXYBdl5/NcZ+Mg2XhQA==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-ppc64@0.16.4: + resolution: {integrity: sha512-X1HSqHUX9D+d0l6/nIh4ZZJ94eQky8d8z6yxAptpZE3FxCWYWvTDd9X9ST84MGZEJx04VYUD/AGgciddwO0b8g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-riscv64@0.16.4: + resolution: {integrity: sha512-97ANpzyNp0GTXCt6SRdIx1ngwncpkV/z453ZuxbnBROCJ5p/55UjhbaG23UdHj88fGWLKPFtMoU4CBacz4j9FA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-s390x@0.16.4: + resolution: {integrity: sha512-pUvPQLPmbEeJRPjP0DYTC1vjHyhrnCklQmCGYbipkep+oyfTn7GTBJXoPodR7ZS5upmEyc8lzAkn2o29wD786A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-x64@0.16.4: + resolution: {integrity: sha512-N55Q0mJs3Sl8+utPRPBrL6NLYZKBCLLx0bme/+RbjvMforTGGzFvsRl4xLTZMUBFC1poDzBEPTEu5nxizQ9Nlw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/netbsd-x64@0.16.4: + resolution: {integrity: sha512-LHSJLit8jCObEQNYkgsDYBh2JrJT53oJO2HVdkSYLa6+zuLJh0lAr06brXIkljrlI+N7NNW1IAXGn/6IZPi3YQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + optional: true + + /@esbuild/openbsd-x64@0.16.4: + resolution: {integrity: sha512-nLgdc6tWEhcCFg/WVFaUxHcPK3AP/bh+KEwKtl69Ay5IBqUwKDaq/6Xk0E+fh/FGjnLwqFSsarsbPHeKM8t8Sw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + optional: true + + /@esbuild/sunos-x64@0.16.4: + resolution: {integrity: sha512-08SluG24GjPO3tXKk95/85n9kpyZtXCVwURR2i4myhrOfi3jspClV0xQQ0W0PYWHioJj+LejFMt41q+PG3mlAQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + optional: true + + /@esbuild/win32-arm64@0.16.4: + resolution: {integrity: sha512-yYiRDQcqLYQSvNQcBKN7XogbrSvBE45FEQdH8fuXPl7cngzkCvpsG2H9Uey39IjQ6gqqc+Q4VXYHsQcKW0OMjQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + optional: true + + /@esbuild/win32-ia32@0.16.4: + resolution: {integrity: sha512-5rabnGIqexekYkh9zXG5waotq8mrdlRoBqAktjx2W3kb0zsI83mdCwrcAeKYirnUaTGztR5TxXcXmQrEzny83w==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + optional: true + + /@esbuild/win32-x64@0.16.4: + resolution: {integrity: sha512-sN/I8FMPtmtT2Yw+Dly8Ur5vQ5a/RmC8hW7jO9PtPSQUPkowxWpcUZnqOggU7VwyT3Xkj6vcXWd3V/qTXwultQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + optional: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.48.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.48.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/regexpp@4.8.0: + resolution: {integrity: sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/eslintrc@2.1.2: + resolution: {integrity: sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4(supports-color@5.5.0) + espree: 9.6.1 + globals: 13.21.0 + ignore: 5.2.0 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/js@8.48.0: + resolution: {integrity: sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@floating-ui/core@1.2.1: + resolution: {integrity: sha512-LSqwPZkK3rYfD7GKoIeExXOyYx6Q1O4iqZWwIehDNuv3Dv425FIAE8PRwtAx1imEolFTHgBEcoFHm9MDnYgPCg==} + dev: false + + /@floating-ui/dom@1.2.1: + resolution: {integrity: sha512-Rt45SmRiV8eU+xXSB9t0uMYiQ/ZWGE/jumse2o3i5RGlyvcbqOF4q+1qBnzLE2kZ5JGhq0iMkcGXUKbFe7MpTA==} + dependencies: + '@floating-ui/core': 1.2.1 + dev: false + + /@floating-ui/react-dom@1.3.0(react-dom@18.1.0)(react@18.2.0): + resolution: {integrity: sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@floating-ui/dom': 1.2.1 + react: 18.2.0 + react-dom: 18.1.0(react@18.2.0) + dev: false + + /@floating-ui/react@0.19.2(@types/react@18.2.21)(react-dom@18.1.0)(react@18.2.0): + resolution: {integrity: sha512-JyNk4A0Ezirq8FlXECvRtQOX/iBe5Ize0W/pLkrZjfHW9GUV7Xnq6zm6fyZuQzaHHqEnVizmvlA96e1/CkZv+w==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@floating-ui/react-dom': 1.3.0(react-dom@18.1.0)(react@18.2.0) + aria-hidden: 1.2.1(@types/react@18.2.21)(react@18.2.0) + react: 18.2.0 + react-dom: 18.1.0(react@18.2.0) + tabbable: 6.1.1 + transitivePeerDependencies: + - '@types/react' + dev: false + + /@hookform/error-message@2.0.1(react-dom@18.1.0)(react-hook-form@7.43.1)(react@18.2.0): + resolution: {integrity: sha512-U410sAr92xgxT1idlu9WWOVjndxLdgPUHEB8Schr27C9eh7/xUnITWpCMF93s+lGiG++D4JnbSnrb5A21AdSNg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + react-hook-form: ^7.0.0 + dependencies: + react: 18.2.0 + react-dom: 18.1.0(react@18.2.0) + react-hook-form: 7.43.1(react@18.2.0) + dev: false + + /@hookform/resolvers@2.8.9(react-hook-form@7.43.1): + resolution: {integrity: sha512-IXwGpjewxScF4N2kuyYDip6ABqH4lCg9n1f1mp0vbmKik+u+nestpbtdEs6U1WQZxwaoK/2APv1+MEr4czX7XA==} + peerDependencies: + react-hook-form: ^7.0.0 + dependencies: + react-hook-form: 7.43.1(react@18.2.0) + dev: false + + /@humanwhocodes/config-array@0.11.11: + resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.3.4(supports-color@5.5.0) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema@1.2.1: + resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + dev: true + + /@istanbuljs/load-nyc-config@1.1.0: + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + dev: false + + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + dev: false + + /@jest/console@29.6.4: + resolution: {integrity: sha512-wNK6gC0Ha9QeEPSkeJedQuTQqxZYnDPuDcDhVuVatRvMkL4D0VTvFVZj+Yuh6caG2aOfzkUZ36KtCmLNtR02hw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 16.11.7 + chalk: 4.1.2 + jest-message-util: 29.6.3 + jest-util: 29.6.3 + slash: 3.0.0 + dev: false + + /@jest/core@29.6.4(ts-node@10.9.1): + resolution: {integrity: sha512-U/vq5ccNTSVgYH7mHnodHmCffGWHJnz/E1BEWlLuK5pM4FZmGfBn/nrJGLjUsSmyx3otCeqc1T31F4y08AMDLg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 29.6.4 + '@jest/reporters': 29.6.4 + '@jest/test-result': 29.6.4 + '@jest/transform': 29.6.4 + '@jest/types': 29.6.3 + '@types/node': 16.11.7 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.3.1 + exit: 0.1.2 + graceful-fs: 4.2.10 + jest-changed-files: 29.6.3 + jest-config: 29.6.4(@types/node@16.11.7)(ts-node@10.9.1) + jest-haste-map: 29.6.4 + jest-message-util: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.6.4 + jest-resolve-dependencies: 29.6.4 + jest-runner: 29.6.4 + jest-runtime: 29.6.4 + jest-snapshot: 29.6.4 + jest-util: 29.6.3 + jest-validate: 29.6.3 + jest-watcher: 29.6.4 + micromatch: 4.0.5 + pretty-format: 29.6.3 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + dev: false + + /@jest/create-cache-key-function@27.5.1: + resolution: {integrity: sha512-dmH1yW+makpTSURTy8VzdUwFnfQh1G8R+DxO2Ho2FFmBbKFEVm+3jWdvFhE2VqB/LATCTokkP0dotjyQyw5/AQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/types': 27.5.1 + dev: true + + /@jest/environment@29.6.4: + resolution: {integrity: sha512-sQ0SULEjA1XUTHmkBRl7A1dyITM9yb1yb3ZNKPX3KlTd6IG7mWUe3e2yfExtC2Zz1Q+mMckOLHmL/qLiuQJrBQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.6.4 + '@jest/types': 29.6.3 + '@types/node': 16.11.7 + jest-mock: 29.6.3 + + /@jest/expect-utils@29.6.4: + resolution: {integrity: sha512-FEhkJhqtvBwgSpiTrocquJCdXPsyvNKcl/n7A3u7X4pVoF4bswm11c9d4AV+kfq2Gpv/mM8x7E7DsRvH+djkrg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + + /@jest/expect@29.6.4: + resolution: {integrity: sha512-Warhsa7d23+3X5bLbrbYvaehcgX5TLYhI03JKoedTiI8uJU4IhqYBWF7OSSgUyz4IgLpUYPkK0AehA5/fRclAA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + expect: 29.6.4 + jest-snapshot: 29.6.4 + transitivePeerDependencies: + - supports-color + dev: false + + /@jest/fake-timers@29.6.4: + resolution: {integrity: sha512-6UkCwzoBK60edXIIWb0/KWkuj7R7Qq91vVInOe3De6DSpaEiqjKcJw4F7XUet24Wupahj9J6PlR09JqJ5ySDHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.0.2 + '@types/node': 16.11.7 + jest-message-util: 29.6.3 + jest-mock: 29.6.3 + jest-util: 29.6.3 + + /@jest/globals@29.6.4: + resolution: {integrity: sha512-wVIn5bdtjlChhXAzVXavcY/3PEjf4VqM174BM3eGL5kMxLiZD5CLnbmkEyA1Dwh9q8XjP6E8RwjBsY/iCWrWsA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.6.4 + '@jest/expect': 29.6.4 + '@jest/types': 29.6.3 + jest-mock: 29.6.3 + transitivePeerDependencies: + - supports-color + dev: false + + /@jest/reporters@29.6.4: + resolution: {integrity: sha512-sxUjWxm7QdchdrD3NfWKrL8FBsortZeibSJv4XLjESOOjSUOkjQcb0ZHJwfhEGIvBvTluTzfG2yZWZhkrXJu8g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.6.4 + '@jest/test-result': 29.6.4 + '@jest/transform': 29.6.4 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.19 + '@types/node': 16.11.7 + chalk: 4.1.2 + collect-v8-coverage: 1.0.1 + exit: 0.1.2 + glob: 7.2.0 + graceful-fs: 4.2.10 + istanbul-lib-coverage: 3.2.0 + istanbul-lib-instrument: 6.0.0 + istanbul-lib-report: 3.0.0 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.4 + jest-message-util: 29.6.3 + jest-util: 29.6.3 + jest-worker: 29.6.4 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + + /@jest/source-map@29.6.3: + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jridgewell/trace-mapping': 0.3.19 + callsites: 3.1.0 + graceful-fs: 4.2.10 + dev: false + + /@jest/test-result@29.6.4: + resolution: {integrity: sha512-uQ1C0AUEN90/dsyEirgMLlouROgSY+Wc/JanVVk0OiUKa5UFh7sJpMEM3aoUBAz2BRNvUJ8j3d294WFuRxSyOQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.6.4 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.3 + collect-v8-coverage: 1.0.1 + dev: false + + /@jest/test-sequencer@29.6.4: + resolution: {integrity: sha512-E84M6LbpcRq3fT4ckfKs9ryVanwkaIB0Ws9bw3/yP4seRLg/VaCZ/LgW0MCq5wwk4/iP/qnilD41aj2fsw2RMg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.6.4 + graceful-fs: 4.2.10 + jest-haste-map: 29.6.4 + slash: 3.0.0 + dev: false + + /@jest/transform@29.6.4: + resolution: {integrity: sha512-8thgRSiXUqtr/pPGY/OsyHuMjGyhVnWrFAwoxmIemlBuiMyU1WFs0tXoNxzcr4A4uErs/ABre76SGmrr5ab/AA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.18.9 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.19 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.10 + jest-haste-map: 29.6.4 + jest-regex-util: 29.6.3 + jest-util: 29.6.3 + micromatch: 4.0.5 + pirates: 4.0.5 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + dev: false + + /@jest/types@27.5.1: + resolution: {integrity: sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@types/istanbul-lib-coverage': 2.0.3 + '@types/istanbul-reports': 3.0.1 + '@types/node': 16.11.7 + '@types/yargs': 16.0.4 + chalk: 4.1.2 + dev: true + + /@jest/types@29.6.3: + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.3 + '@types/istanbul-reports': 3.0.1 + '@types/node': 16.11.7 + '@types/yargs': 17.0.10 + chalk: 4.1.2 + + /@jridgewell/gen-mapping@0.1.1: + resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: false + + /@jridgewell/gen-mapping@0.3.2: + resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.19 + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + + /@jridgewell/set-array@1.1.1: + resolution: {integrity: sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==} + engines: {node: '>=6.0.0'} + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + + /@jridgewell/trace-mapping@0.3.19: + resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + + /@lukeed/csprng@1.1.0: + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + dev: true + + /@microsoft/fetch-event-source@2.0.1: + resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==} + dev: false + + /@nestjs/axios@0.1.0(@nestjs/common@9.3.11)(reflect-metadata@0.1.13)(rxjs@7.8.0): + resolution: {integrity: sha512-b2TT2X6BFbnNoeteiaxCIiHaFcSbVW+S5yygYqiIq5i6H77yIU3IVuLdpQkHq8/EqOWFwMopLN8jdkUT71Am9w==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 + reflect-metadata: ^0.1.12 + rxjs: ^6.0.0 || ^7.0.0 + dependencies: + '@nestjs/common': 9.3.11(reflect-metadata@0.1.13)(rxjs@7.8.0) + axios: 0.27.2 + reflect-metadata: 0.1.13 + rxjs: 7.8.0 + transitivePeerDependencies: + - debug + dev: true + + /@nestjs/common@9.3.11(reflect-metadata@0.1.13)(rxjs@7.8.0): + resolution: {integrity: sha512-IFZ2G/5UKWC2Uo7tJ4SxGed2+aiA+sJyWeWsGTogKVDhq90oxVBToh+uCDeI31HNUpqYGoWmkletfty42zUd8A==} + peerDependencies: + cache-manager: <=5 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 + rxjs: ^7.1.0 + peerDependenciesMeta: + cache-manager: + optional: true + class-transformer: + optional: true + class-validator: + optional: true + dependencies: + iterare: 1.2.1 + reflect-metadata: 0.1.13 + rxjs: 7.8.0 + tslib: 2.5.0 + uid: 2.0.1 + dev: true + + /@nestjs/core@9.3.11(@nestjs/common@9.3.11)(reflect-metadata@0.1.13)(rxjs@7.8.0): + resolution: {integrity: sha512-CI27a2JFd5rvvbgkalWqsiwQNhcP4EAG5BUK8usjp29wVp1kx30ghfBT8FLqIgmkRVo65A0IcEnWsxeXMntkxQ==} + requiresBuild: true + peerDependencies: + '@nestjs/common': ^9.0.0 + '@nestjs/microservices': ^9.0.0 + '@nestjs/platform-express': ^9.0.0 + '@nestjs/websockets': ^9.0.0 + reflect-metadata: ^0.1.12 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + '@nestjs/websockets': + optional: true + dependencies: + '@nestjs/common': 9.3.11(reflect-metadata@0.1.13)(rxjs@7.8.0) + '@nuxtjs/opencollective': 0.3.2 + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 3.2.0 + reflect-metadata: 0.1.13 + rxjs: 7.8.0 + tslib: 2.5.0 + uid: 2.0.1 + transitivePeerDependencies: + - encoding + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.11.0 + dev: true + + /@nuxtjs/opencollective@0.3.2: + resolution: {integrity: sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + dependencies: + chalk: 4.1.2 + consola: 2.15.3 + node-fetch: 2.6.7 + transitivePeerDependencies: + - encoding + dev: true + + /@openapitools/openapi-generator-cli@2.7.0: + resolution: {integrity: sha512-ieEpHTA/KsDz7ANw03lLPYyjdedDEXYEyYoGBRWdduqXWSX65CJtttjqa8ZaB1mNmIjMtchUHwAYQmTLVQ8HYg==} + engines: {node: '>=10.0.0'} + hasBin: true + requiresBuild: true + dependencies: + '@nestjs/axios': 0.1.0(@nestjs/common@9.3.11)(reflect-metadata@0.1.13)(rxjs@7.8.0) + '@nestjs/common': 9.3.11(reflect-metadata@0.1.13)(rxjs@7.8.0) + '@nestjs/core': 9.3.11(@nestjs/common@9.3.11)(reflect-metadata@0.1.13)(rxjs@7.8.0) + '@nuxtjs/opencollective': 0.3.2 + chalk: 4.1.2 + commander: 8.3.0 + compare-versions: 4.1.4 + concurrently: 6.5.1 + console.table: 0.10.0 + fs-extra: 10.1.0 + glob: 7.1.6 + inquirer: 8.2.5 + lodash: 4.17.21 + reflect-metadata: 0.1.13 + rxjs: 7.8.0 + tslib: 2.0.3 + transitivePeerDependencies: + - '@nestjs/microservices' + - '@nestjs/platform-express' + - '@nestjs/websockets' + - cache-manager + - class-transformer + - class-validator + - debug + - encoding + dev: true + + /@pkgr/utils@2.3.0: + resolution: {integrity: sha512-7dIJ9CRVzBnqyEl7diUHPUFJf/oty2SeoVzcMocc5PeOUDK9KGzvgIBjGRRzzlRDaOjh3ADwH0WeibQvi3ls2Q==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + dependencies: + cross-spawn: 7.0.3 + is-glob: 4.0.3 + open: 8.4.0 + picocolors: 1.0.0 + tiny-glob: 0.2.9 + tslib: 2.5.0 + dev: true + + /@popperjs/core@2.9.2: + resolution: {integrity: sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==} + + /@reduxjs/toolkit@1.8.3(react-redux@8.0.2)(react@18.2.0): + resolution: {integrity: sha512-lU/LDIfORmjBbyDLaqFN2JB9YmAT1BElET9y0ZszwhSBa5Ef3t6o5CrHupw5J1iOXwd+o92QfQZ8OJpwXvsssg==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 + react-redux: ^7.2.1 || ^8.0.2 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + dependencies: + immer: 9.0.12 + react: 18.2.0 + react-redux: 8.0.2(@types/react-dom@18.0.5)(@types/react@18.2.21)(react-dom@18.1.0)(react@18.2.0)(redux@4.2.0) + redux: 4.2.0 + redux-thunk: 2.4.1(redux@4.2.0) + reselect: 4.1.5 + dev: false + + /@remix-run/router@1.8.0: + resolution: {integrity: sha512-mrfKqIHnSZRyIzBcanNJmVQELTnX+qagEDlcKO90RgRBVOZGSGvZKeDihTRfWcqoDn5N/NkUcwWTccnpN18Tfg==} + engines: {node: '>=14.0.0'} + dev: false + + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + /@sinonjs/commons@2.0.0: + resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==} + dependencies: + type-detect: 4.0.8 + + /@sinonjs/fake-timers@10.0.2: + resolution: {integrity: sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==} + dependencies: + '@sinonjs/commons': 2.0.0 + + /@swc/core-darwin-arm64@1.3.38: + resolution: {integrity: sha512-4ZTJJ/cR0EsXW5UxFCifZoGfzQ07a8s4ayt1nLvLQ5QoB1GTAf9zsACpvWG8e7cmCR0L76R5xt8uJuyr+noIXA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optional: true + + /@swc/core-darwin-x64@1.3.38: + resolution: {integrity: sha512-Kim727rNo4Dl8kk0CR8aJQe4zFFtsT1TZGlNrNMUgN1WC3CRX7dLZ6ZJi/VVcTG1cbHp5Fp3mUzwHsMxEh87Mg==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + optional: true + + /@swc/core-linux-arm-gnueabihf@1.3.38: + resolution: {integrity: sha512-yaRdnPNU2enlJDRcIMvYVSyodY+Amhf5QuXdUbAj6rkDD6wUs/s9C6yPYrFDmoTltrG+nBv72mUZj+R46wVfSw==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + requiresBuild: true + optional: true + + /@swc/core-linux-arm64-gnu@1.3.38: + resolution: {integrity: sha512-iNY1HqKo/wBSu3QOGBUlZaLdBP/EHcwNjBAqIzpb8J64q2jEN02RizqVW0mDxyXktJ3lxr3g7VW9uqklMeXbjQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@swc/core-linux-arm64-musl@1.3.38: + resolution: {integrity: sha512-LJCFgLZoPRkPCPmux+Q5ctgXRp6AsWhvWuY61bh5bIPBDlaG9pZk94DeHyvtiwT0syhTtXb2LieBOx6NqN3zeA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@swc/core-linux-x64-gnu@1.3.38: + resolution: {integrity: sha512-hRQGRIWHmv2PvKQM/mMV45mVXckM2+xLB8TYLLgUG66mmtyGTUJPyxjnJkbI86WNGqo18k+lAuMG2mn6QmzYwQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@swc/core-linux-x64-musl@1.3.38: + resolution: {integrity: sha512-PTYSqtsIfPHLKDDNbueI5e0sc130vyHRiFOeeC6qqzA2FAiVvIxuvXHLr0soPvKAR1WyhtYmFB9QarcctemL2w==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@swc/core-win32-arm64-msvc@1.3.38: + resolution: {integrity: sha512-9lHfs5TPNs+QdkyZFhZledSmzBEbqml/J1rqPSb9Fy8zB6QlspixE6OLZ3nTlUOdoGWkcTTdrOn77Sd7YGf1AA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + optional: true + + /@swc/core-win32-ia32-msvc@1.3.38: + resolution: {integrity: sha512-SbL6pfA2lqvDKnwTHwOfKWvfHAdcbAwJS4dBkFidr7BiPTgI5Uk8wAPcRb8mBECpmIa9yFo+N0cAFRvMnf+cNw==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + optional: true + + /@swc/core-win32-x64-msvc@1.3.38: + resolution: {integrity: sha512-UFveLrL6eGvViOD8OVqUQa6QoQwdqwRvLtL5elF304OT8eCPZa8BhuXnWk25X8UcOyns8gFcb8Fhp3oaLi/Rlw==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + requiresBuild: true + optional: true + + /@swc/core@1.3.38: + resolution: {integrity: sha512-AiEVehRFws//AiiLx9DPDp1WDXt+yAoGD1kMYewhoF6QLdTz8AtYu6i8j/yAxk26L8xnegy0CDwcNnub9qenyQ==} + engines: {node: '>=10'} + requiresBuild: true + optionalDependencies: + '@swc/core-darwin-arm64': 1.3.38 + '@swc/core-darwin-x64': 1.3.38 + '@swc/core-linux-arm-gnueabihf': 1.3.38 + '@swc/core-linux-arm64-gnu': 1.3.38 + '@swc/core-linux-arm64-musl': 1.3.38 + '@swc/core-linux-x64-gnu': 1.3.38 + '@swc/core-linux-x64-musl': 1.3.38 + '@swc/core-win32-arm64-msvc': 1.3.38 + '@swc/core-win32-ia32-msvc': 1.3.38 + '@swc/core-win32-x64-msvc': 1.3.38 + + /@swc/jest@0.2.24(@swc/core@1.3.38): + resolution: {integrity: sha512-fwgxQbM1wXzyKzl1+IW0aGrRvAA8k0Y3NxFhKigbPjOJ4mCKnWEcNX9HQS3gshflcxq8YKhadabGUVfdwjCr6Q==} + engines: {npm: '>= 7.0.0'} + peerDependencies: + '@swc/core': '*' + dependencies: + '@jest/create-cache-key-function': 27.5.1 + '@swc/core': 1.3.38 + jsonc-parser: 3.2.0 + dev: true + + /@szhsin/react-menu@3.5.3(react-dom@18.1.0)(react@18.2.0): + resolution: {integrity: sha512-jxo8oaRwxmVjUzkyOi/ZJiXaZiuFPMIxFzyJdUKfnhBLYiEOVTU9M2CiPuEkirILoareR2GJj2K3y8a81CBPlw==} + peerDependencies: + react: '>=16.14.0' + react-dom: '>=16.14.0' + dependencies: + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.1.0(react@18.2.0) + react-transition-state: 1.1.5(react-dom@18.1.0)(react@18.2.0) + dev: false + + /@tanstack/query-core@4.0.5: + resolution: {integrity: sha512-QOJ2gLbwlf8p0487pMey6vv8EF5X2ib1zINayaD7mb9/LibUtXmZ12uJgTqcnjgNY/4tWZn5qJnEk2ePG5AVGA==} + dev: false + + /@tanstack/react-query@4.0.5(react-dom@18.1.0)(react@18.2.0): + resolution: {integrity: sha512-tIggVlhoFevVpY/LkZroPmrERFHN8tw4aZLtgwSArzHmMJ03WQcaNvbbHy6GERidXtaMdUz+IeQryrE7cO7WPQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@tanstack/query-core': 4.0.5 + '@types/use-sync-external-store': 0.0.3 + react: 18.2.0 + react-dom: 18.1.0(react@18.2.0) + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false + + /@tanstack/react-table@8.5.10(react-dom@18.1.0)(react@18.2.0): + resolution: {integrity: sha512-TG+iyqtZD5/N7gCDNM8HJc+ZWbUAkSjv8JaVqk2eYs4xaTUfPnTTsG0vJYqGkoxp8i2GFY78dRx1FDCsctYPGA==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + dependencies: + '@tanstack/table-core': 8.5.10 + react: 18.2.0 + react-dom: 18.1.0(react@18.2.0) + dev: false + + /@tanstack/table-core@8.5.10: + resolution: {integrity: sha512-L1GU/BAF7k50vfk1qDvHkRLhEKSjE46EtCuWRrbdu2UKP4mKClTEeL4/zMr6iefMo8QgWa+Gc0CTVVfYcFLlLA==} + engines: {node: '>=12'} + dev: false + + /@testing-library/dom@8.13.0: + resolution: {integrity: sha512-9VHgfIatKNXQNaZTtLnalIy0jNZzY35a4S3oi08YAt9Hv1VsfZ/DfA45lM8D/UhtHBGJ4/lGwp0PZkVndRkoOQ==} + engines: {node: '>=12'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/runtime': 7.22.11 + '@types/aria-query': 4.2.2 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.10 + lz-string: 1.5.0 + pretty-format: 27.5.1 + dev: true + + /@testing-library/dom@9.3.1: + resolution: {integrity: sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==} + engines: {node: '>=14'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/runtime': 7.22.11 + '@types/aria-query': 5.0.1 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.10 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + /@testing-library/jest-dom@5.16.5: + resolution: {integrity: sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==} + engines: {node: '>=8', npm: '>=6', yarn: '>=1'} + dependencies: + '@adobe/css-tools': 4.2.0 + '@babel/runtime': 7.17.9 + '@types/testing-library__jest-dom': 5.14.5 + aria-query: 5.0.0 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.5.10 + lodash: 4.17.21 + redent: 3.0.0 + dev: true + + /@testing-library/react@14.0.0(react-dom@18.1.0)(react@18.2.0): + resolution: {integrity: sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg==} + engines: {node: '>=14'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@babel/runtime': 7.17.9 + '@testing-library/dom': 9.3.1 + '@types/react-dom': 18.0.5 + react: 18.2.0 + react-dom: 18.1.0(react@18.2.0) + dev: false + + /@testing-library/user-event@14.4.3(@testing-library/dom@9.3.1): + resolution: {integrity: sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + dependencies: + '@testing-library/dom': 9.3.1 + dev: true + + /@tootallnate/once@2.0.0: + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + dev: true + + /@ts-morph/common@0.12.3: + resolution: {integrity: sha512-4tUmeLyXJnJWvTFOKtcNJ1yh0a3SsTLi2MUoyj8iUNznFRN1ZquaNe7Oukqrnki2FzZkm0J9adCNLDZxUzvj+w==} + dependencies: + fast-glob: 3.2.11 + minimatch: 3.1.2 + mkdirp: 1.0.4 + path-browserify: 1.0.1 + dev: true + + /@tsconfig/node10@1.0.9: + resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + /@tsconfig/node16@1.0.3: + resolution: {integrity: sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==} + + /@types/aria-query@4.2.2: + resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==} + dev: true + + /@types/aria-query@5.0.1: + resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==} + + /@types/babel__core@7.1.19: + resolution: {integrity: sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==} + dependencies: + '@babel/parser': 7.18.9 + '@babel/types': 7.18.9 + '@types/babel__generator': 7.6.4 + '@types/babel__template': 7.4.1 + '@types/babel__traverse': 7.17.1 + dev: false + + /@types/babel__generator@7.6.4: + resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} + dependencies: + '@babel/types': 7.18.9 + dev: false + + /@types/babel__template@7.4.1: + resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} + dependencies: + '@babel/parser': 7.18.9 + '@babel/types': 7.18.9 + dev: false + + /@types/babel__traverse@7.17.1: + resolution: {integrity: sha512-kVzjari1s2YVi77D3w1yuvohV2idweYXMCDzqBiVNN63TcDWrIlTVOYpqVrvbbyOE/IyzBoTKF0fdnLPEORFxA==} + dependencies: + '@babel/types': 7.18.9 + dev: false + + /@types/eventsource@1.1.11: + resolution: {integrity: sha512-L7wLDZlWm5mROzv87W0ofIYeQP5K2UhoFnnUyEWLKM6UBb0ZNRgAqp98qE5DkgfBXdWfc2kYmw9KZm4NLjRbsw==} + dev: true + + /@types/graceful-fs@4.1.5: + resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==} + dependencies: + '@types/node': 16.11.7 + dev: false + + /@types/history@4.7.11: + resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} + dev: true + + /@types/hoist-non-react-statics@3.3.1: + resolution: {integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==} + dependencies: + '@types/react': 18.2.21 + hoist-non-react-statics: 3.3.2 + + /@types/istanbul-lib-coverage@2.0.3: + resolution: {integrity: sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==} + + /@types/istanbul-lib-report@3.0.0: + resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==} + dependencies: + '@types/istanbul-lib-coverage': 2.0.3 + + /@types/istanbul-reports@3.0.1: + resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==} + dependencies: + '@types/istanbul-lib-report': 3.0.0 + + /@types/jest@29.0.1: + resolution: {integrity: sha512-CAZrjLRZs4xEdIrfrdV74xK1Vo/BKQZwUcjJv3gp6gMeV3BsVxMnXTcgtYOKyphT4DPPo7jxVEVhuwJTQn3oPQ==} + dependencies: + expect: 29.6.4 + pretty-format: 29.6.3 + + /@types/jsdom@20.0.0: + resolution: {integrity: sha512-YfAchFs0yM1QPDrLm2VHe+WHGtqms3NXnXAMolrgrVP6fgBHHXy1ozAbo/dFtPNtZC/m66bPiCTWYmqp1F14gA==} + dependencies: + '@types/node': 16.11.7 + '@types/tough-cookie': 4.0.2 + parse5: 7.1.1 + dev: true + + /@types/json-schema@7.0.11: + resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} + dev: true + + /@types/json5@0.0.29: + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + dev: true + + /@types/lodash@4.14.177: + resolution: {integrity: sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==} + dev: true + + /@types/lossless-json@1.0.2: + resolution: {integrity: sha512-RdV8M8qlWUpmk7gnY3fBB4TNn3Ab8hMMqhJC/sG77t8Zk+hjVwvZGTFv+upEBUkxXbq0+UxGAPhOml83w1IkIQ==} + dev: true + + /@types/node@16.11.7: + resolution: {integrity: sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==} + + /@types/parse-json@4.0.0: + resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} + dev: true + + /@types/prop-types@15.7.5: + resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} + + /@types/react-datepicker@4.10.0(react@18.2.0): + resolution: {integrity: sha512-Cq+ks20vBIU6XN67TbkCHu8M7V46Y6vJrKE2n+8q/GfueJyWWTIKeC3Z7cz/d+qxGDq/VCrqA929R0U4lNuztg==} + dependencies: + '@popperjs/core': 2.9.2 + '@types/react': 18.2.21 + date-fns: 2.27.0 + react-popper: 2.2.5(@popperjs/core@2.9.2)(react@18.2.0) + transitivePeerDependencies: + - react + dev: true + + /@types/react-dom@18.0.5: + resolution: {integrity: sha512-OWPWTUrY/NIrjsAPkAk1wW9LZeIjSvkXRhclsFO8CZcZGCOg2G0YZy4ft+rOyYxy8B7ui5iZzi9OkDebZ7/QSA==} + dependencies: + '@types/react': 18.2.21 + + /@types/react-router-dom@5.3.3: + resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} + dependencies: + '@types/history': 4.7.11 + '@types/react': 18.2.21 + '@types/react-router': 5.1.18 + dev: true + + /@types/react-router@5.1.18: + resolution: {integrity: sha512-YYknwy0D0iOwKQgz9v8nOzt2J6l4gouBmDnWqUUznltOTaon+r8US8ky8HvN0tXvc38U9m6z/t2RsVsnd1zM0g==} + dependencies: + '@types/history': 4.7.11 + '@types/react': 18.2.21 + dev: true + + /@types/react@18.2.21: + resolution: {integrity: sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==} + dependencies: + '@types/prop-types': 15.7.5 + '@types/scheduler': 0.16.2 + csstype: 3.1.2 + + /@types/scheduler@0.16.2: + resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} + + /@types/stack-utils@2.0.1: + resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} + + /@types/styled-components@5.1.18: + resolution: {integrity: sha512-xPTYmWP7Mxk5TAD3pYsqjwA9G5fAI8e/S51QUJEl7EQD1siKCdiYXIWiH2lzoHRl+QqbQCJMcGv3YTF3OmyPdQ==} + dependencies: + '@types/hoist-non-react-statics': 3.3.1 + '@types/react': 18.2.21 + csstype: 3.0.8 + dev: true + + /@types/testing-library__jest-dom@5.14.5: + resolution: {integrity: sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==} + dependencies: + '@types/jest': 29.0.1 + + /@types/tough-cookie@4.0.2: + resolution: {integrity: sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==} + dev: true + + /@types/use-sync-external-store@0.0.3: + resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} + dev: false + + /@types/yargs-parser@20.2.0: + resolution: {integrity: sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==} + + /@types/yargs@16.0.4: + resolution: {integrity: sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==} + dependencies: + '@types/yargs-parser': 20.2.0 + dev: true + + /@types/yargs@17.0.10: + resolution: {integrity: sha512-gmEaFwpj/7f/ROdtIlci1R1VYU1J4j95m8T+Tj3iBgiBFKg1foE/PSl93bBd5T9LDXNPo8UlNN6W0qwD8O5OaA==} + dependencies: + '@types/yargs-parser': 20.2.0 + + /@typescript-eslint/eslint-plugin@5.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.48.0)(typescript@4.7.4): + resolution: {integrity: sha512-kgTsISt9pM53yRFQmLZ4npj99yGl3x3Pl7z4eA66OuTzAGC4bQB5H5fuLwPnqTKU3yyrrg4MIhjF17UYnL4c0w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/parser': 5.62.0(eslint@8.48.0)(typescript@4.7.4) + '@typescript-eslint/scope-manager': 5.29.0 + '@typescript-eslint/type-utils': 5.29.0(eslint@8.48.0)(typescript@4.7.4) + '@typescript-eslint/utils': 5.29.0(eslint@8.48.0)(typescript@4.7.4) + debug: 4.3.4(supports-color@5.5.0) + eslint: 8.48.0 + functional-red-black-tree: 1.0.1 + ignore: 5.2.0 + regexpp: 3.2.0 + semver: 7.3.7 + tsutils: 3.21.0(typescript@4.7.4) + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@5.62.0(eslint@8.48.0)(typescript@4.7.4): + resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.7.4) + debug: 4.3.4(supports-color@5.5.0) + eslint: 8.48.0 + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@5.29.0: + resolution: {integrity: sha512-etbXUT0FygFi2ihcxDZjz21LtC+Eps9V2xVx09zFoN44RRHPrkMflidGMI+2dUs821zR1tDS6Oc9IXxIjOUZwA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.29.0 + '@typescript-eslint/visitor-keys': 5.29.0 + dev: true + + /@typescript-eslint/scope-manager@5.62.0: + resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + dev: true + + /@typescript-eslint/type-utils@5.29.0(eslint@8.48.0)(typescript@4.7.4): + resolution: {integrity: sha512-JK6bAaaiJozbox3K220VRfCzLa9n0ib/J+FHIwnaV3Enw/TO267qe0pM1b1QrrEuy6xun374XEAsRlA86JJnyg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/utils': 5.29.0(eslint@8.48.0)(typescript@4.7.4) + debug: 4.3.4(supports-color@5.5.0) + eslint: 8.48.0 + tsutils: 3.21.0(typescript@4.7.4) + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@5.29.0: + resolution: {integrity: sha512-X99VbqvAXOMdVyfFmksMy3u8p8yoRGITgU1joBJPzeYa0rhdf5ok9S56/itRoUSh99fiDoMtarSIJXo7H/SnOg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@typescript-eslint/types@5.62.0: + resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@typescript-eslint/typescript-estree@5.29.0(typescript@4.7.4): + resolution: {integrity: sha512-mQvSUJ/JjGBdvo+1LwC+GY2XmSYjK1nAaVw2emp/E61wEVYEyibRHCqm1I1vEKbXCpUKuW4G7u9ZCaZhJbLoNQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.29.0 + '@typescript-eslint/visitor-keys': 5.29.0 + debug: 4.3.4(supports-color@5.5.0) + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + tsutils: 3.21.0(typescript@4.7.4) + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/typescript-estree@5.62.0(typescript@4.7.4): + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.3.4(supports-color@5.5.0) + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + tsutils: 3.21.0(typescript@4.7.4) + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@5.29.0(eslint@8.48.0)(typescript@4.7.4): + resolution: {integrity: sha512-3Eos6uP1nyLOBayc/VUdKZikV90HahXE5Dx9L5YlSd/7ylQPXhLk1BYb29SDgnBnTp+jmSZUU0QxUiyHgW4p7A==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@types/json-schema': 7.0.11 + '@typescript-eslint/scope-manager': 5.29.0 + '@typescript-eslint/types': 5.29.0 + '@typescript-eslint/typescript-estree': 5.29.0(typescript@4.7.4) + eslint: 8.48.0 + eslint-scope: 5.1.1 + eslint-utils: 3.0.0(eslint@8.48.0) + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@5.29.0: + resolution: {integrity: sha512-Hpb/mCWsjILvikMQoZIE3voc9wtQcS0A9FUw3h8bhr9UxBdtI/tw1ZDZUOXHXLOVMedKCH5NxyzATwnU78bWCQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.29.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@typescript-eslint/visitor-keys@5.62.0: + resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.62.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@vitejs/plugin-react-swc@3.0.0(vite@4.0.5): + resolution: {integrity: sha512-vYlodz/mjYRbxMGbHzDgR8aPR+z8n7K/enWkyBGH096xrL2DIPCuTvQVRYPTXGyy6wO7OFiMxZ3r4nKQD1sH0A==} + peerDependencies: + vite: ^4.0.0 + dependencies: + '@swc/core': 1.3.38 + vite: 4.0.5(@types/node@16.11.7)(sass@1.66.1) + dev: true + + /abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + dev: true + + /ace-builds@1.7.1: + resolution: {integrity: sha512-1mcbP5kXvr729sJ9dA/8tul0pjuvKbma0LF/ZMRwPEwjoNWNpe/x0OXpaPJo36aRpZCjRZMl5zsME3hAKTiaNw==} + dev: false + + /acorn-globals@6.0.0: + resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==} + dependencies: + acorn: 7.4.1 + acorn-walk: 7.2.0 + dev: true + + /acorn-jsx@5.3.2(acorn@8.10.0): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.10.0 + dev: true + + /acorn-walk@7.2.0: + resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn-walk@8.2.0: + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + engines: {node: '>=0.4.0'} + + /acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /acorn@8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /acorn@8.7.1: + resolution: {integrity: sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==} + engines: {node: '>=0.4.0'} + hasBin: true + + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + dev: true + + /ajv-formats@2.1.1(ajv@8.8.2): + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + dependencies: + ajv: 8.8.2 + dev: false + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ajv@8.8.2: + resolution: {integrity: sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==} + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + dev: false + + /ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + + /ansi-escapes@6.0.0: + resolution: {integrity: sha512-IG23inYII3dWlU2EyiAiGj6Bwal5GzsgPMwjYGvc1HPE2dgbj4ZB5ToWBKSquKw74nB3TIuOwaI6/jSULzfgrw==} + engines: {node: '>=14.16'} + dependencies: + type-fest: 3.6.1 + dev: false + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: false + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + /anymatch@3.1.2: + resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + dev: false + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /aria-hidden@1.2.1(@types/react@18.2.21)(react@18.2.0): + resolution: {integrity: sha512-PN344VAf9j1EAi+jyVHOJ8XidQdPVssGco39eNcsGdM4wcsILtxrKLkbuiMfLWYROK1FjRQasMWCBttrhjnr6A==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0 + react: ^16.9.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.21 + react: 18.2.0 + tslib: 2.5.0 + dev: false + + /aria-query@5.0.0: + resolution: {integrity: sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==} + engines: {node: '>=6.0'} + dev: true + + /aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + dependencies: + deep-equal: 2.2.2 + + /aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + dependencies: + dequal: 2.0.3 + dev: true + + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.2 + is-array-buffer: 3.0.2 + + /array-includes@3.1.5: + resolution: {integrity: sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + get-intrinsic: 1.2.1 + is-string: 1.0.7 + dev: true + + /array-includes@3.1.6: + resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + get-intrinsic: 1.2.1 + is-string: 1.0.7 + dev: true + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + + /array.prototype.flat@1.3.0: + resolution: {integrity: sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + es-shim-unscopables: 1.0.0 + dev: true + + /array.prototype.flat@1.3.1: + resolution: {integrity: sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + es-shim-unscopables: 1.0.0 + dev: true + + /array.prototype.flatmap@1.3.0: + resolution: {integrity: sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + es-shim-unscopables: 1.0.0 + dev: true + + /array.prototype.flatmap@1.3.1: + resolution: {integrity: sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + es-shim-unscopables: 1.0.0 + dev: true + + /arraybuffer.prototype.slice@1.0.1: + resolution: {integrity: sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.2 + define-properties: 1.2.0 + get-intrinsic: 1.2.1 + is-array-buffer: 3.0.2 + is-shared-array-buffer: 1.0.2 + dev: true + + /ast-types-flow@0.0.7: + resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==} + dev: true + + /async@3.2.4: + resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} + dev: true + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: true + + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + + /axe-core@4.7.2: + resolution: {integrity: sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==} + engines: {node: '>=4'} + dev: true + + /axios@0.27.2: + resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} + dependencies: + follow-redirects: 1.15.2 + form-data: 4.0.0 + transitivePeerDependencies: + - debug + dev: true + + /axobject-query@3.2.1: + resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} + dependencies: + dequal: 2.0.3 + dev: true + + /babel-jest@29.6.4(@babel/core@7.18.9): + resolution: {integrity: sha512-meLj23UlSLddj6PC+YTOFRgDAtjnZom8w/ACsrx0gtPtv5cJZk0A5Unk5bV4wixD7XaPCN1fQvpww8czkZURmw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.18.9 + '@jest/transform': 29.6.4 + '@types/babel__core': 7.1.19 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.18.9) + chalk: 4.1.2 + graceful-fs: 4.2.10 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + dependencies: + '@babel/helper-plugin-utils': 7.18.6 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.0 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/template': 7.18.6 + '@babel/types': 7.18.9 + '@types/babel__core': 7.1.19 + '@types/babel__traverse': 7.17.1 + dev: false + + /babel-plugin-styled-components@1.13.2(styled-components@5.3.1): + resolution: {integrity: sha512-Vb1R3d4g+MUfPQPVDMCGjm3cDocJEUTR7Xq7QS95JWWeksN1wdFRYpD2kulDgI3Huuaf1CZd+NK4KQmqUFh5dA==} + peerDependencies: + styled-components: '>= 2' + dependencies: + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-module-imports': 7.18.6 + babel-plugin-syntax-jsx: 6.18.0 + lodash: 4.17.21 + styled-components: 5.3.1(react-dom@18.1.0)(react-is@18.2.0)(react@18.2.0) + + /babel-plugin-syntax-jsx@6.18.0: + resolution: {integrity: sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==} + + /babel-preset-current-node-syntax@1.0.1(@babel/core@7.18.9): + resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.18.9 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.18.9) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.18.9) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.18.9) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.18.9) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.18.9) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.18.9) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.18.9) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.18.9) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.18.9) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.18.9) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.18.9) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.18.9) + dev: false + + /babel-preset-jest@29.6.3(@babel/core@7.18.9): + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.18.9 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.18.9) + dev: false + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + + /bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.0 + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + + /browser-process-hrtime@1.0.0: + resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} + dev: true + + /browserslist@4.20.4: + resolution: {integrity: sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001352 + electron-to-chromium: 1.4.151 + escalade: 3.1.1 + node-releases: 2.0.5 + picocolors: 1.0.0 + dev: false + + /bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + dependencies: + node-int64: 0.4.0 + dev: false + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: false + + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.1 + + /call-me-maybe@1.0.1: + resolution: {integrity: sha512-wCyFsDQkKPwwF8BDwOiWNx/9K45L/hvggQiDbve+viMNMQnWhrlYIuBk09offfwCRtCO9P6XwUttufzU11WCVw==} + dev: false + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + dev: false + + /camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + dev: false + + /camelize@1.0.0: + resolution: {integrity: sha512-W2lPwkBkMZwFlPCXhIlYgxu+7gC/NUlCtdK652DAJ1JdgV0sTrvuPFshNPrFa1TY2JOkLhgdeEBplB4ezEa+xg==} + + /caniuse-lite@1.0.30001352: + resolution: {integrity: sha512-GUgH8w6YergqPQDGWhJGt8GDRnY0L/iJVQcU3eJ46GYf52R8tk0Wxp0PymuFVZboJYXGiCqwozAYZNRjVj6IcA==} + dev: false + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + /chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + /chalk@5.2.0: + resolution: {integrity: sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: false + + /char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + dev: false + + /char-regex@2.0.1: + resolution: {integrity: sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==} + engines: {node: '>=12.20'} + dev: false + + /chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + dev: true + + /chokidar@3.5.2: + resolution: {integrity: sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.2 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + + /ci-info@3.3.1: + resolution: {integrity: sha512-SXgeMX9VwDe7iFFaEWkA5AstuER9YKqy4EhHqr4DVqkwmD9rpVimkMKWHdjn30Ja45txyjhSn63lVX69eVCckg==} + + /cjs-module-lexer@1.2.2: + resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} + dev: false + + /classnames@2.3.1: + resolution: {integrity: sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==} + dev: false + + /cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + dependencies: + restore-cursor: 3.1.0 + dev: true + + /cli-spinners@2.6.1: + resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==} + engines: {node: '>=6'} + dev: true + + /cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + dev: true + + /cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + /clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + requiresBuild: true + dev: true + + /co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + dev: false + + /code-block-writer@11.0.1: + resolution: {integrity: sha512-0ch9DeCY8v/BWA9n1/Qu1ALG3lpesel4PYL2eNlGLgvGl+J7k74i+dSXSF3wLvF5SYII8/GUT/Ic+fycBR/DUQ==} + dev: true + + /collect-v8-coverage@1.0.1: + resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==} + dev: false + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: true + + /commander@6.2.1: + resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} + engines: {node: '>= 6'} + dev: true + + /commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + dev: true + + /compare-versions@4.1.4: + resolution: {integrity: sha512-FemMreK9xNyL8gQevsdRMrvO4lFCkQP7qbuktn1q8ndcNk1+0mz7lgE7b/sNvbhVgY4w6tMN1FDp6aADjqw2rw==} + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + /concurrently@6.5.1: + resolution: {integrity: sha512-FlSwNpGjWQfRwPLXvJ/OgysbBxPkWpiVjy1042b0U7on7S7qwwMIILRj7WTN1mTgqa582bG6NFuScOoh6Zgdag==} + engines: {node: '>=10.0.0'} + hasBin: true + dependencies: + chalk: 4.1.2 + date-fns: 2.27.0 + lodash: 4.17.21 + rxjs: 6.6.7 + spawn-command: 0.0.2-1 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 16.2.0 + dev: true + + /confusing-browser-globals@1.0.11: + resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} + dev: true + + /consola@2.15.3: + resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} + dev: true + + /console.table@0.10.0: + resolution: {integrity: sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g==} + engines: {node: '> 0.10'} + dependencies: + easy-table: 1.1.0 + dev: true + + /convert-source-map@1.7.0: + resolution: {integrity: sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==} + dependencies: + safe-buffer: 5.1.2 + dev: false + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: false + + /core-js@3.14.0: + resolution: {integrity: sha512-3s+ed8er9ahK+zJpp9ZtuVcDoFzHNiZsPbNAAE4KXgrRHbjSqqNN6xGSXq6bq7TZIbKj4NLrLb6bJ5i+vSVjHA==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + requiresBuild: true + dev: false + + /cosmiconfig@7.0.1: + resolution: {integrity: sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==} + engines: {node: '>=10'} + dependencies: + '@types/parse-json': 4.0.0 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + dev: true + + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + /css-color-keywords@1.0.0: + resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} + engines: {node: '>=4'} + + /css-to-react-native@3.0.0: + resolution: {integrity: sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==} + dependencies: + camelize: 1.0.0 + css-color-keywords: 1.0.0 + postcss-value-parser: 4.1.0 + + /css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + dev: true + + /cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + dev: true + + /cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + dev: true + + /cssstyle@2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + dependencies: + cssom: 0.3.8 + dev: true + + /csstype@3.0.8: + resolution: {integrity: sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==} + dev: true + + /csstype@3.1.2: + resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} + + /damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + dev: true + + /data-urls@3.0.2: + resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} + engines: {node: '>=12'} + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + dev: true + + /date-fns@2.27.0: + resolution: {integrity: sha512-sj+J0Mo2p2X1e306MHq282WS4/A8Pz/95GIFcsPNMPMZVI3EUrAdSv90al1k+p74WGLCruMXk23bfEDZa71X9Q==} + engines: {node: '>=0.11'} + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: true + + /debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: true + + /debug@4.3.4(supports-color@5.5.0): + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + supports-color: 5.5.0 + + /decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dev: true + + /dedent@1.5.1: + resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + dev: false + + /deep-equal@2.2.2: + resolution: {integrity: sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.2 + es-get-iterator: 1.1.3 + get-intrinsic: 1.2.1 + is-arguments: 1.1.1 + is-array-buffer: 3.0.2 + is-date-object: 1.0.5 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + isarray: 2.0.5 + object-is: 1.1.5 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.0 + side-channel: 1.0.4 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.1 + which-typed-array: 1.1.11 + + /deep-is@0.1.3: + resolution: {integrity: sha512-GtxAN4HvBachZzm4OnWqc45ESpUCMwkYcsjnsPs23FwJbsO+k4t0k9bQCgOmzIlpHO28+WPK/KRbRk0DDHuuDw==} + dev: true + + /deepmerge@4.2.2: + resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} + engines: {node: '>=0.10.0'} + dev: false + + /defaults@1.0.3: + resolution: {integrity: sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA==} + requiresBuild: true + dependencies: + clone: 1.0.4 + dev: true + + /define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + dev: true + + /define-properties@1.2.0: + resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} + engines: {node: '>= 0.4'} + dependencies: + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: true + + /dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + dev: true + + /detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + dev: false + + /diff-match-patch@1.0.5: + resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} + dev: false + + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /dom-accessibility-api@0.5.10: + resolution: {integrity: sha512-Xu9mD0UjrJisTmv7lmVSDMagQcU9R5hwAbxsaAE/35XPnPLJobbuREfV/rraiSaEj/UOvgrzQs66zyTWTlyd+g==} + + /domexception@4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + dependencies: + webidl-conversions: 7.0.0 + dev: true + + /dotenv@16.0.1: + resolution: {integrity: sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==} + engines: {node: '>=12'} + dev: true + + /easy-table@1.1.0: + resolution: {integrity: sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA==} + optionalDependencies: + wcwidth: 1.0.1 + dev: true + + /ejs@3.1.8: + resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + jake: 10.8.5 + dev: true + + /electron-to-chromium@1.4.151: + resolution: {integrity: sha512-XaG2LpZi9fdiWYOqJh0dJy4SlVywCvpgYXhzOlZTp4JqSKqxn5URqOjbm9OMYB3aInA2GuHQiem1QUOc1yT0Pw==} + dev: false + + /emittery@0.10.2: + resolution: {integrity: sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==} + engines: {node: '>=12'} + dev: false + + /emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + dev: false + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + + /enhanced-resolve@5.10.0: + resolution: {integrity: sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.10 + tapable: 2.2.1 + dev: true + + /entities@4.4.0: + resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==} + engines: {node: '>=0.12'} + dev: true + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + + /es-abstract@1.22.1: + resolution: {integrity: sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.1 + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.5 + get-intrinsic: 1.2.1 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.12.3 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.0 + safe-array-concat: 1.0.0 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.7 + string.prototype.trimend: 1.0.6 + string.prototype.trimstart: 1.0.6 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.11 + dev: true + + /es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + is-arguments: 1.1.1 + is-map: 2.0.2 + is-set: 2.0.2 + is-string: 1.0.7 + isarray: 2.0.5 + stop-iteration-iterator: 1.0.0 + + /es-set-tostringtag@2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + has-tostringtag: 1.0.0 + dev: true + + /es-shim-unscopables@1.0.0: + resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + dependencies: + has: 1.0.3 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + + /esbuild@0.16.4: + resolution: {integrity: sha512-qQrPMQpPTWf8jHugLWHoGqZjApyx3OEm76dlTXobHwh/EBbavbRdjXdYi/GWr43GyN0sfpap14GPkb05NH3ROA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.16.4 + '@esbuild/android-arm64': 0.16.4 + '@esbuild/android-x64': 0.16.4 + '@esbuild/darwin-arm64': 0.16.4 + '@esbuild/darwin-x64': 0.16.4 + '@esbuild/freebsd-arm64': 0.16.4 + '@esbuild/freebsd-x64': 0.16.4 + '@esbuild/linux-arm': 0.16.4 + '@esbuild/linux-arm64': 0.16.4 + '@esbuild/linux-ia32': 0.16.4 + '@esbuild/linux-loong64': 0.16.4 + '@esbuild/linux-mips64el': 0.16.4 + '@esbuild/linux-ppc64': 0.16.4 + '@esbuild/linux-riscv64': 0.16.4 + '@esbuild/linux-s390x': 0.16.4 + '@esbuild/linux-x64': 0.16.4 + '@esbuild/netbsd-x64': 0.16.4 + '@esbuild/openbsd-x64': 0.16.4 + '@esbuild/sunos-x64': 0.16.4 + '@esbuild/win32-arm64': 0.16.4 + '@esbuild/win32-ia32': 0.16.4 + '@esbuild/win32-x64': 0.16.4 + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + /escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /escodegen@2.0.0: + resolution: {integrity: sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==} + engines: {node: '>=6.0'} + hasBin: true + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionator: 0.8.3 + optionalDependencies: + source-map: 0.6.1 + dev: true + + /eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.26.0)(eslint@8.48.0): + resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} + engines: {node: ^10.12.0 || >=12.0.0} + peerDependencies: + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.2 + dependencies: + confusing-browser-globals: 1.0.11 + eslint: 8.48.0 + eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.2.7)(eslint@8.48.0) + object.assign: 4.1.4 + object.entries: 1.1.7 + semver: 6.3.1 + dev: true + + /eslint-config-airbnb-typescript@17.0.0(@typescript-eslint/eslint-plugin@5.29.0)(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.26.0)(eslint@8.48.0): + resolution: {integrity: sha512-elNiuzD0kPAPTXjFWg+lE24nMdHMtuxgYoD30OyMD6yrW1AhFZPAg27VX7d3tzOErw+dgJTNWfRSDqEcXb4V0g==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^5.13.0 + '@typescript-eslint/parser': ^5.0.0 + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.3 + dependencies: + '@typescript-eslint/eslint-plugin': 5.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.48.0)(typescript@4.7.4) + '@typescript-eslint/parser': 5.62.0(eslint@8.48.0)(typescript@4.7.4) + eslint: 8.48.0 + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.26.0)(eslint@8.48.0) + eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.2.7)(eslint@8.48.0) + dev: true + + /eslint-config-airbnb@19.0.4(eslint-plugin-import@2.26.0)(eslint-plugin-jsx-a11y@6.7.1)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.30.1)(eslint@8.48.0): + resolution: {integrity: sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==} + engines: {node: ^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.3 + eslint-plugin-jsx-a11y: ^6.5.1 + eslint-plugin-react: ^7.28.0 + eslint-plugin-react-hooks: ^4.3.0 + dependencies: + eslint: 8.48.0 + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.26.0)(eslint@8.48.0) + eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.2.7)(eslint@8.48.0) + eslint-plugin-jsx-a11y: 6.7.1(eslint@8.48.0) + eslint-plugin-react: 7.30.1(eslint@8.48.0) + eslint-plugin-react-hooks: 4.6.0(eslint@8.48.0) + object.assign: 4.1.2 + object.entries: 1.1.5 + dev: true + + /eslint-config-prettier@9.0.0(eslint@8.48.0): + resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.48.0 + dev: true + + /eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + dependencies: + debug: 3.2.7 + is-core-module: 2.13.0 + resolve: 1.22.4 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-import-resolver-typescript@3.2.7(eslint-plugin-import@2.26.0)(eslint@8.48.0): + resolution: {integrity: sha512-WvcsRy3aPmwVsuS/XVliAJWpIdTlaFXXZPZk3TCbvvF8RtaAkjAhcLL5bl5VEoTmE+XnTHjIbWMzNZcOQpK/DA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + dependencies: + debug: 4.3.4(supports-color@5.5.0) + enhanced-resolve: 5.10.0 + eslint: 8.48.0 + eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.2.7)(eslint@8.48.0) + get-tsconfig: 4.2.0 + globby: 13.1.2 + is-core-module: 2.9.0 + is-glob: 4.0.3 + synckit: 0.8.1 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-module-utils@2.7.3(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.2.7): + resolution: {integrity: sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 5.62.0(eslint@8.48.0)(typescript@4.7.4) + debug: 3.2.7 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.2.7(eslint-plugin-import@2.26.0)(eslint@8.48.0) + find-up: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-import@2.26.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.2.7)(eslint@8.48.0): + resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 5.62.0(eslint@8.48.0)(typescript@4.7.4) + array-includes: 3.1.5 + array.prototype.flat: 1.3.0 + debug: 2.6.9 + doctrine: 2.1.0 + eslint: 8.48.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.7.3(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.2.7) + has: 1.0.3 + is-core-module: 2.9.0 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.values: 1.1.5 + resolve: 1.22.0 + tsconfig-paths: 3.14.1 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-plugin-jest-dom@4.0.3(eslint@8.48.0): + resolution: {integrity: sha512-9j+n8uj0+V0tmsoS7bYC7fLhQmIvjRqRYEcbDSi+TKPsTThLLXCyj5swMSSf/hTleeMktACnn+HFqXBr5gbcbA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6', yarn: '>=1'} + peerDependencies: + eslint: ^6.8.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@babel/runtime': 7.17.9 + '@testing-library/dom': 8.13.0 + eslint: 8.48.0 + requireindex: 1.2.0 + dev: true + + /eslint-plugin-jsx-a11y@6.7.1(eslint@8.48.0): + resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + dependencies: + '@babel/runtime': 7.22.11 + aria-query: 5.3.0 + array-includes: 3.1.6 + array.prototype.flatmap: 1.3.1 + ast-types-flow: 0.0.7 + axe-core: 4.7.2 + axobject-query: 3.2.1 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 8.48.0 + has: 1.0.3 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.5 + minimatch: 3.1.2 + object.entries: 1.1.7 + object.fromentries: 2.0.7 + semver: 6.3.1 + dev: true + + /eslint-plugin-prettier@4.2.1(eslint-config-prettier@9.0.0)(eslint@8.48.0)(prettier@2.8.4): + resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + eslint: '>=7.28.0' + eslint-config-prettier: '*' + prettier: '>=2.0.0' + peerDependenciesMeta: + eslint-config-prettier: + optional: true + dependencies: + eslint: 8.48.0 + eslint-config-prettier: 9.0.0(eslint@8.48.0) + prettier: 2.8.4 + prettier-linter-helpers: 1.0.0 + dev: true + + /eslint-plugin-react-hooks@4.6.0(eslint@8.48.0): + resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + dependencies: + eslint: 8.48.0 + dev: true + + /eslint-plugin-react@7.30.1(eslint@8.48.0): + resolution: {integrity: sha512-NbEvI9jtqO46yJA3wcRF9Mo0lF9T/jhdHqhCHXiXtD+Zcb98812wvokjWpU7Q4QH5edo6dmqrukxVvWWXHlsUg==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + dependencies: + array-includes: 3.1.5 + array.prototype.flatmap: 1.3.0 + doctrine: 2.1.0 + eslint: 8.48.0 + estraverse: 5.3.0 + jsx-ast-utils: 3.3.0 + minimatch: 3.1.2 + object.entries: 1.1.5 + object.fromentries: 2.0.5 + object.hasown: 1.1.1 + object.values: 1.1.5 + prop-types: 15.8.1 + resolve: 2.0.0-next.3 + semver: 6.3.1 + string.prototype.matchall: 4.0.7 + dev: true + + /eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: true + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-utils@3.0.0(eslint@8.48.0): + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' + dependencies: + eslint: 8.48.0 + eslint-visitor-keys: 2.1.0 + dev: true + + /eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + dev: true + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint@8.48.0: + resolution: {integrity: sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.48.0) + '@eslint-community/regexpp': 4.8.0 + '@eslint/eslintrc': 2.1.2 + '@eslint/js': 8.48.0 + '@humanwhocodes/config-array': 0.11.11 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4(supports-color@5.5.0) + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.21.0 + graphemer: 1.4.0 + ignore: 5.2.0 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.10.0 + acorn-jsx: 5.3.2(acorn@8.10.0) + eslint-visitor-keys: 3.4.3 + dev: true + + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: false + + /exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + dev: false + + /expect@29.6.4: + resolution: {integrity: sha512-F2W2UyQ8XYyftHT57dtfg8Ue3X5qLgm2sSug0ivvLRH/VKNRL/pDxg/TH7zVzbQB0tu80clNFy6LU7OS/VSEKA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.6.4 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.6.4 + jest-message-util: 29.6.3 + jest-util: 29.6.3 + + /external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + dev: true + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + /fast-diff@1.2.0: + resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==} + dev: true + + /fast-glob@3.2.11: + resolution: {integrity: sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + dev: true + + /fastq@1.11.0: + resolution: {integrity: sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==} + dependencies: + reusify: 1.0.4 + dev: true + + /fb-watchman@2.0.1: + resolution: {integrity: sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==} + dependencies: + bser: 2.1.1 + dev: false + + /fetch-mock@9.11.0: + resolution: {integrity: sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==} + engines: {node: '>=4.0.0'} + peerDependencies: + node-fetch: '*' + peerDependenciesMeta: + node-fetch: + optional: true + dependencies: + '@babel/core': 7.18.2 + '@babel/runtime': 7.17.9 + core-js: 3.14.0 + debug: 4.3.4(supports-color@5.5.0) + glob-to-regexp: 0.4.1 + is-subset: 0.1.1 + lodash.isequal: 4.5.0 + path-to-regexp: 2.4.0 + querystring: 0.2.1 + whatwg-url: 6.5.0 + transitivePeerDependencies: + - supports-color + dev: false + + /figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + dependencies: + escape-string-regexp: 1.0.5 + dev: true + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.0.4 + dev: true + + /filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + dependencies: + minimatch: 5.1.2 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + + /find-up@2.1.0: + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} + engines: {node: '>=4'} + dependencies: + locate-path: 2.0.0 + dev: true + + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + dev: false + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache@3.0.4: + resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.2.5 + rimraf: 3.0.2 + dev: true + + /flatted@3.2.5: + resolution: {integrity: sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==} + dev: true + + /follow-redirects@1.15.2: + resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: true + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: true + + /format-util@1.0.5: + resolution: {integrity: sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==} + dev: false + + /fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + dependencies: + graceful-fs: 4.2.10 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + optional: true + + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + + /function.prototype.name@1.1.5: + resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + functions-have-names: 1.2.3 + dev: true + + /functional-red-black-tree@1.0.1: + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + dev: true + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: false + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + /get-intrinsic@1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-proto: 1.0.1 + has-symbols: 1.0.3 + + /get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + dev: false + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: false + + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + dev: true + + /get-tsconfig@4.2.0: + resolution: {integrity: sha512-X8u8fREiYOE6S8hLbq99PeykTDoLVnxvF4DjWKJmz9xy2nNRdUcV8ZN9tniJFeKyTU3qnC9lL8n4Chd6LmVKHg==} + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + dev: false + + /glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /glob@7.2.0: + resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + /glob@9.2.1: + resolution: {integrity: sha512-Pxxgq3W0HyA3XUvSXcFhRSs+43Jsx0ddxcFrbjxNGkL2Ak5BAUBxLqI5G6ADDeCHLfzzXFhe0b1yYcctGmytMA==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + fs.realpath: 1.0.0 + minimatch: 7.4.2 + minipass: 4.2.4 + path-scurry: 1.6.1 + dev: true + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + /globals@13.21.0: + resolution: {integrity: sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.0 + dev: true + + /globalyzer@0.1.0: + resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} + dev: true + + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.2.11 + ignore: 5.2.0 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /globby@13.1.2: + resolution: {integrity: sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + dir-glob: 3.0.1 + fast-glob: 3.2.11 + ignore: 5.2.0 + merge2: 1.4.1 + slash: 4.0.0 + dev: true + + /globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + /goober@2.1.10(csstype@3.1.2): + resolution: {integrity: sha512-7PpuQMH10jaTWm33sQgBQvz45pHR8N4l3Cu3WMGEWmHShAcTuuP7I+5/DwKo39fwti5A80WAjvqgz6SSlgWmGA==} + peerDependencies: + csstype: ^3.0.10 + dependencies: + csstype: 3.1.2 + dev: false + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.1 + + /graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + /has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.1 + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + + /has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + + /hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + dependencies: + react-is: 16.13.1 + + /html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + dependencies: + whatwg-encoding: 2.0.0 + dev: true + + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: false + + /http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.3.4(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + dev: true + + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + dev: true + + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + dev: false + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: true + + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: true + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true + + /ignore@5.2.0: + resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==} + engines: {node: '>= 4'} + dev: true + + /immer@9.0.12: + resolution: {integrity: sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==} + dev: false + + /immutable@4.0.0: + resolution: {integrity: sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==} + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /import-local@3.1.0: + resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} + engines: {node: '>=8'} + hasBin: true + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + dev: false + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + /indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /inquirer@8.2.5: + resolution: {integrity: sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==} + engines: {node: '>=12.0.0'} + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 7.0.0 + dev: true + + /internal-slot@1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + side-channel: 1.0.4 + + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + /is-bigint@1.0.2: + resolution: {integrity: sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==} + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + + /is-boolean-object@1.1.1: + resolution: {integrity: sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + /is-core-module@2.13.0: + resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} + dependencies: + has: 1.0.3 + + /is-core-module@2.9.0: + resolution: {integrity: sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==} + dependencies: + has: 1.0.3 + dev: true + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + + /is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + /is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + dev: false + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + + /is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + dev: true + + /is-map@2.0.2: + resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} + + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object@1.0.5: + resolution: {integrity: sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==} + engines: {node: '>= 0.4'} + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: true + + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + + /is-set@2.0.2: + resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + dev: false + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + + /is-subset@0.1.1: + resolution: {integrity: sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==} + dev: false + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + + /is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.11 + + /is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + dev: true + + /is-weakmap@2.0.1: + resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + dev: true + + /is-weakset@2.0.2: + resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + + /is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + dependencies: + is-docker: 2.2.1 + dev: true + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + /istanbul-lib-coverage@3.2.0: + resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} + engines: {node: '>=8'} + dev: false + + /istanbul-lib-instrument@5.2.0: + resolution: {integrity: sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A==} + engines: {node: '>=8'} + dependencies: + '@babel/core': 7.18.9 + '@babel/parser': 7.18.9 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: false + + /istanbul-lib-instrument@6.0.0: + resolution: {integrity: sha512-x58orMzEVfzPUKqlbLd1hXCnySCxKdDKa6Rjg97CwuLLRI4g3FHTdnExu1OqffVFay6zeMW+T6/DowFLndWnIw==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.18.9 + '@babel/parser': 7.18.9 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: false + + /istanbul-lib-report@3.0.0: + resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==} + engines: {node: '>=8'} + dependencies: + istanbul-lib-coverage: 3.2.0 + make-dir: 3.1.0 + supports-color: 7.2.0 + dev: false + + /istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + dependencies: + debug: 4.3.4(supports-color@5.5.0) + istanbul-lib-coverage: 3.2.0 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + dev: false + + /istanbul-reports@3.1.4: + resolution: {integrity: sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.0 + dev: false + + /iterare@1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + dev: true + + /jake@10.8.5: + resolution: {integrity: sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + async: 3.2.4 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + dev: true + + /jest-changed-files@29.6.3: + resolution: {integrity: sha512-G5wDnElqLa4/c66ma5PG9eRjE342lIbF6SUnTJi26C3J28Fv2TVY2rOyKB9YGbSA5ogwevgmxc4j4aVjrEK6Yg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + execa: 5.1.1 + jest-util: 29.6.3 + p-limit: 3.1.0 + dev: false + + /jest-circus@29.6.4: + resolution: {integrity: sha512-YXNrRyntVUgDfZbjXWBMPslX1mQ8MrSG0oM/Y06j9EYubODIyHWP8hMUbjbZ19M3M+zamqEur7O80HODwACoJw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.6.4 + '@jest/expect': 29.6.4 + '@jest/test-result': 29.6.4 + '@jest/types': 29.6.3 + '@types/node': 16.11.7 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.5.1 + is-generator-fn: 2.1.0 + jest-each: 29.6.3 + jest-matcher-utils: 29.6.4 + jest-message-util: 29.6.3 + jest-runtime: 29.6.4 + jest-snapshot: 29.6.4 + jest-util: 29.6.3 + p-limit: 3.1.0 + pretty-format: 29.6.3 + pure-rand: 6.0.0 + slash: 3.0.0 + stack-utils: 2.0.5 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: false + + /jest-cli@29.6.4(@types/node@16.11.7)(ts-node@10.9.1): + resolution: {integrity: sha512-+uMCQ7oizMmh8ZwRfZzKIEszFY9ksjjEQnTEMTaL7fYiL3Kw4XhqT9bYh+A4DQKUb67hZn2KbtEnDuHvcgK4pQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.6.4(ts-node@10.9.1) + '@jest/test-result': 29.6.4 + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.10 + import-local: 3.1.0 + jest-config: 29.6.4(@types/node@16.11.7)(ts-node@10.9.1) + jest-util: 29.6.3 + jest-validate: 29.6.3 + prompts: 2.4.2 + yargs: 17.5.1 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: false + + /jest-config@29.6.4(@types/node@16.11.7)(ts-node@10.9.1): + resolution: {integrity: sha512-JWohr3i9m2cVpBumQFv2akMEnFEPVOh+9L2xIBJhJ0zOaci2ZXuKJj0tgMKQCBZAKA09H049IR4HVS/43Qb19A==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.18.9 + '@jest/test-sequencer': 29.6.4 + '@jest/types': 29.6.3 + '@types/node': 16.11.7 + babel-jest: 29.6.4(@babel/core@7.18.9) + chalk: 4.1.2 + ci-info: 3.3.1 + deepmerge: 4.2.2 + glob: 7.2.0 + graceful-fs: 4.2.10 + jest-circus: 29.6.4 + jest-environment-node: 29.6.4 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.6.4 + jest-runner: 29.6.4 + jest-util: 29.6.3 + jest-validate: 29.6.3 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.6.3 + slash: 3.0.0 + strip-json-comments: 3.1.1 + ts-node: 10.9.1(@swc/core@1.3.38)(@types/node@16.11.7)(typescript@4.7.4) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: false + + /jest-diff@29.6.4: + resolution: {integrity: sha512-9F48UxR9e4XOEZvoUXEHSWY4qC4zERJaOfrbBg9JpbJOO43R1vN76REt/aMGZoY6GD5g84nnJiBIVlscegefpw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.6.3 + + /jest-docblock@29.6.3: + resolution: {integrity: sha512-2+H+GOTQBEm2+qFSQ7Ma+BvyV+waiIFxmZF5LdpBsAEjWX8QYjSCa4FrkIYtbfXUJJJnFCYrOtt6TZ+IAiTjBQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + detect-newline: 3.1.0 + dev: false + + /jest-each@29.6.3: + resolution: {integrity: sha512-KoXfJ42k8cqbkfshW7sSHcdfnv5agDdHCPA87ZBdmHP+zJstTJc0ttQaJ/x7zK6noAL76hOuTIJ6ZkQRS5dcyg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.6.3 + pretty-format: 29.6.3 + dev: false + + /jest-environment-jsdom@29.6.4: + resolution: {integrity: sha512-K6wfgUJ16DoMs02JYFid9lOsqfpoVtyJxpRlnTxUHzvZWBnnh2VNGRB9EC1Cro96TQdq5TtSjb3qUjNaJP9IyA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + '@jest/environment': 29.6.4 + '@jest/fake-timers': 29.6.4 + '@jest/types': 29.6.3 + '@types/jsdom': 20.0.0 + '@types/node': 16.11.7 + jest-mock: 29.6.3 + jest-util: 29.6.3 + jsdom: 20.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /jest-environment-node@29.6.4: + resolution: {integrity: sha512-i7SbpH2dEIFGNmxGCpSc2w9cA4qVD+wfvg2ZnfQ7XVrKL0NA5uDVBIiGH8SR4F0dKEv/0qI5r+aDomDf04DpEQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.6.4 + '@jest/fake-timers': 29.6.4 + '@jest/types': 29.6.3 + '@types/node': 16.11.7 + jest-mock: 29.6.3 + jest-util: 29.6.3 + dev: false + + /jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + /jest-haste-map@29.6.4: + resolution: {integrity: sha512-12Ad+VNTDHxKf7k+M65sviyynRoZYuL1/GTuhEVb8RYsNSNln71nANRb/faSyWvx0j+gHcivChXHIoMJrGYjog==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.5 + '@types/node': 16.11.7 + anymatch: 3.1.2 + fb-watchman: 2.0.1 + graceful-fs: 4.2.10 + jest-regex-util: 29.6.3 + jest-util: 29.6.3 + jest-worker: 29.6.4 + micromatch: 4.0.5 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.2 + dev: false + + /jest-leak-detector@29.6.3: + resolution: {integrity: sha512-0kfbESIHXYdhAdpLsW7xdwmYhLf1BRu4AA118/OxFm0Ho1b2RcTmO4oF6aAMaxpxdxnJ3zve2rgwzNBD4Zbm7Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.6.3 + dev: false + + /jest-matcher-utils@29.6.4: + resolution: {integrity: sha512-KSzwyzGvK4HcfnserYqJHYi7sZVqdREJ9DMPAKVbS98JsIAvumihaNUbjrWw0St7p9IY7A9UskCW5MYlGmBQFQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.6.4 + jest-get-type: 29.6.3 + pretty-format: 29.6.3 + + /jest-message-util@29.6.3: + resolution: {integrity: sha512-FtzaEEHzjDpQp51HX4UMkPZjy46ati4T5pEMyM6Ik48ztu4T9LQplZ6OsimHx7EuM9dfEh5HJa6D3trEftu3dA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.18.6 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.10 + micromatch: 4.0.5 + pretty-format: 29.6.3 + slash: 3.0.0 + stack-utils: 2.0.5 + + /jest-mock@29.6.3: + resolution: {integrity: sha512-Z7Gs/mOyTSR4yPsaZ72a/MtuK6RnC3JYqWONe48oLaoEcYwEDxqvbXz85G4SJrm2Z5Ar9zp6MiHF4AlFlRM4Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 16.11.7 + jest-util: 29.6.3 + + /jest-pnp-resolver@1.2.2(jest-resolve@29.6.4): + resolution: {integrity: sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + dependencies: + jest-resolve: 29.6.4 + dev: false + + /jest-regex-util@29.0.0: + resolution: {integrity: sha512-BV7VW7Sy0fInHWN93MMPtlClweYv2qrSCwfeFWmpribGZtQPWNvRSq9XOVgOEjU1iBGRKXUZil0o2AH7Iy9Lug==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: false + + /jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: false + + /jest-resolve-dependencies@29.6.4: + resolution: {integrity: sha512-7+6eAmr1ZBF3vOAJVsfLj1QdqeXG+WYhidfLHBRZqGN24MFRIiKG20ItpLw2qRAsW/D2ZUUmCNf6irUr/v6KHA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.6.4 + transitivePeerDependencies: + - supports-color + dev: false + + /jest-resolve@29.6.4: + resolution: {integrity: sha512-fPRq+0vcxsuGlG0O3gyoqGTAxasagOxEuyoxHeyxaZbc9QNek0AmJWSkhjlMG+mTsj+8knc/mWb3fXlRNVih7Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.10 + jest-haste-map: 29.6.4 + jest-pnp-resolver: 1.2.2(jest-resolve@29.6.4) + jest-util: 29.6.3 + jest-validate: 29.6.3 + resolve: 1.22.4 + resolve.exports: 2.0.1 + slash: 3.0.0 + dev: false + + /jest-runner@29.6.4: + resolution: {integrity: sha512-SDaLrMmtVlQYDuG0iSPYLycG8P9jLI+fRm8AF/xPKhYDB2g6xDWjXBrR5M8gEWsK6KVFlebpZ4QsrxdyIX1Jaw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.6.4 + '@jest/environment': 29.6.4 + '@jest/test-result': 29.6.4 + '@jest/transform': 29.6.4 + '@jest/types': 29.6.3 + '@types/node': 16.11.7 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.10 + jest-docblock: 29.6.3 + jest-environment-node: 29.6.4 + jest-haste-map: 29.6.4 + jest-leak-detector: 29.6.3 + jest-message-util: 29.6.3 + jest-resolve: 29.6.4 + jest-runtime: 29.6.4 + jest-util: 29.6.3 + jest-watcher: 29.6.4 + jest-worker: 29.6.4 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + dev: false + + /jest-runtime@29.6.4: + resolution: {integrity: sha512-s/QxMBLvmwLdchKEjcLfwzP7h+jsHvNEtxGP5P+Fl1FMaJX2jMiIqe4rJw4tFprzCwuSvVUo9bn0uj4gNRXsbA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.6.4 + '@jest/fake-timers': 29.6.4 + '@jest/globals': 29.6.4 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.6.4 + '@jest/transform': 29.6.4 + '@jest/types': 29.6.3 + '@types/node': 16.11.7 + chalk: 4.1.2 + cjs-module-lexer: 1.2.2 + collect-v8-coverage: 1.0.1 + glob: 7.2.0 + graceful-fs: 4.2.10 + jest-haste-map: 29.6.4 + jest-message-util: 29.6.3 + jest-mock: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.6.4 + jest-snapshot: 29.6.4 + jest-util: 29.6.3 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /jest-snapshot@29.6.4: + resolution: {integrity: sha512-VC1N8ED7+4uboUKGIDsbvNAZb6LakgIPgAF4RSpF13dN6YaMokfRqO+BaqK4zIh6X3JffgwbzuGqDEjHm/MrvA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.18.9 + '@babel/generator': 7.18.9 + '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.18.9) + '@babel/plugin-syntax-typescript': 7.17.12(@babel/core@7.18.9) + '@babel/types': 7.18.9 + '@jest/expect-utils': 29.6.4 + '@jest/transform': 29.6.4 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.18.9) + chalk: 4.1.2 + expect: 29.6.4 + graceful-fs: 4.2.10 + jest-diff: 29.6.4 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.6.4 + jest-message-util: 29.6.3 + jest-util: 29.6.3 + natural-compare: 1.4.0 + pretty-format: 29.6.3 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: false + + /jest-sonar-reporter@2.0.0: + resolution: {integrity: sha512-ZervDCgEX5gdUbdtWsjdipLN3bKJwpxbvhkYNXTAYvAckCihobSLr9OT/IuyNIRT1EZMDDwR6DroWtrq+IL64w==} + engines: {node: '>=8.0.0'} + dependencies: + xml: 1.0.1 + dev: true + + /jest-styled-components@7.1.1(styled-components@5.3.1): + resolution: {integrity: sha512-OUq31R5CivBF8oy81dnegNQrRW13TugMol/Dz6ZnFfEyo03exLASod7YGwyHGuayYlKmCstPtz0RQ1+NrAbIIA==} + engines: {node: '>= 12'} + peerDependencies: + styled-components: '>= 5' + dependencies: + '@adobe/css-tools': 4.2.0 + styled-components: 5.3.1(react-dom@18.1.0)(react-is@18.2.0)(react@18.2.0) + dev: true + + /jest-util@29.6.3: + resolution: {integrity: sha512-QUjna/xSy4B32fzcKTSz1w7YYzgiHrjjJjevdRf61HYk998R5vVMMNmrHESYZVDS5DSWs+1srPLPKxXPkeSDOA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 16.11.7 + chalk: 4.1.2 + ci-info: 3.3.1 + graceful-fs: 4.2.10 + picomatch: 2.3.1 + + /jest-validate@29.6.3: + resolution: {integrity: sha512-e7KWZcAIX+2W1o3cHfnqpGajdCs1jSM3DkXjGeLSNmCazv1EeI1ggTeK5wdZhF+7N+g44JI2Od3veojoaumlfg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.6.3 + dev: false + + /jest-watch-typeahead@2.2.2(jest@29.6.4): + resolution: {integrity: sha512-+QgOFW4o5Xlgd6jGS5X37i08tuuXNW8X0CV9WNFi+3n8ExCIP+E1melYhvYLjv5fE6D0yyzk74vsSO8I6GqtvQ==} + engines: {node: ^14.17.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + jest: ^27.0.0 || ^28.0.0 || ^29.0.0 + dependencies: + ansi-escapes: 6.0.0 + chalk: 5.2.0 + jest: 29.6.4(@types/node@16.11.7)(ts-node@10.9.1) + jest-regex-util: 29.0.0 + jest-watcher: 29.0.3 + slash: 5.0.0 + string-length: 5.0.1 + strip-ansi: 7.0.1 + dev: false + + /jest-watcher@29.0.3: + resolution: {integrity: sha512-tQX9lU91A+9tyUQKUMp0Ns8xAcdhC9fo73eqA3LFxP2bSgiF49TNcc+vf3qgGYYK9qRjFpXW9+4RgF/mbxyOOw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.6.4 + '@jest/types': 29.6.3 + '@types/node': 16.11.7 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.10.2 + jest-util: 29.6.3 + string-length: 4.0.2 + dev: false + + /jest-watcher@29.6.4: + resolution: {integrity: sha512-oqUWvx6+On04ShsT00Ir9T4/FvBeEh2M9PTubgITPxDa739p4hoQweWPRGyYeaojgT0xTpZKF0Y/rSY1UgMxvQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.6.4 + '@jest/types': 29.6.3 + '@types/node': 16.11.7 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.6.3 + string-length: 4.0.2 + dev: false + + /jest-worker@29.6.4: + resolution: {integrity: sha512-6dpvFV4WjcWbDVGgHTWo/aupl8/LbBx2NSKfiwqf79xC/yeJjKHT1+StcKy/2KTmW16hE68ccKVOtXf+WZGz7Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 16.11.7 + jest-util: 29.6.3 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: false + + /jest@29.6.4(@types/node@16.11.7)(ts-node@10.9.1): + resolution: {integrity: sha512-tEFhVQFF/bzoYV1YuGyzLPZ6vlPrdfvDmmAxudA1dLEuiztqg2Rkx20vkKY32xiDROcD2KXlgZ7Cu8RPeEHRKw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.6.4(ts-node@10.9.1) + '@jest/types': 29.6.3 + import-local: 3.1.0 + jest-cli: 29.6.4(@types/node@16.11.7)(ts-node@10.9.1) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: false + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + /js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + dev: false + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /jsdom@20.0.0: + resolution: {integrity: sha512-x4a6CKCgx00uCmP+QakBDFXwjAJ69IkkIWHmtmjd3wvXPcdOS44hfX2vqkOQrVrq8l9DhNNADZRXaCEWvgXtVA==} + engines: {node: '>=14'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + abab: 2.0.6 + acorn: 8.10.0 + acorn-globals: 6.0.0 + cssom: 0.5.0 + cssstyle: 2.3.0 + data-urls: 3.0.2 + decimal.js: 10.4.3 + domexception: 4.0.0 + escodegen: 2.0.0 + form-data: 4.0.0 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.0 + parse5: 7.1.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.3 + w3c-hr-time: 1.0.2 + w3c-xmlserializer: 3.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + ws: 8.8.0 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + /json-schema-faker@0.5.3: + resolution: {integrity: sha512-BeIrR0+YSrTbAR9dOMnjbFl1MvHyXnq+Wpdw1FpWZDHWKLzK229hZ5huyPcmzFUfVq1ODwf40WdGVoE266UBUg==} + hasBin: true + dependencies: + json-schema-ref-parser: 6.1.0 + jsonpath-plus: 7.2.0 + dev: false + + /json-schema-ref-parser@6.1.0: + resolution: {integrity: sha512-pXe9H1m6IgIpXmE5JSb8epilNTGsmTb2iPohAXpOdhqGFbQjNeHHsZxU+C8w6T81GZxSPFLeUoqDJmzxx5IGuw==} + deprecated: Please switch to @apidevtools/json-schema-ref-parser + dependencies: + call-me-maybe: 1.0.1 + js-yaml: 3.14.1 + ono: 4.0.11 + dev: false + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + dev: false + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + dependencies: + minimist: 1.2.6 + dev: true + + /json5@2.2.1: + resolution: {integrity: sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==} + engines: {node: '>=6'} + hasBin: true + + /jsonc-parser@3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + dev: true + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.0 + optionalDependencies: + graceful-fs: 4.2.10 + dev: true + + /jsonpath-plus@7.2.0: + resolution: {integrity: sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==} + engines: {node: '>=12.0.0'} + dev: false + + /jsx-ast-utils@3.3.0: + resolution: {integrity: sha512-XzO9luP6L0xkxwhIJMTJQpZo/eeN60K08jHdexfD569AGxeNug6UketeHXEhROoM8aR7EcUoOQmIhcJQjcuq8Q==} + engines: {node: '>=4.0'} + dependencies: + array-includes: 3.1.6 + object.assign: 4.1.4 + dev: true + + /jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + dependencies: + array-includes: 3.1.6 + array.prototype.flat: 1.3.1 + object.assign: 4.1.4 + object.values: 1.1.7 + dev: true + + /kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + dev: false + + /language-subtag-registry@0.3.21: + resolution: {integrity: sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==} + dev: true + + /language-tags@1.0.5: + resolution: {integrity: sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==} + dependencies: + language-subtag-registry: 0.3.21 + dev: true + + /leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + dev: false + + /levn@0.3.0: + resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.1.2 + type-check: 0.3.2 + dev: true + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + /locate-path@2.0.0: + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} + engines: {node: '>=4'} + dependencies: + p-locate: 2.0.0 + path-exists: 3.0.0 + dev: true + + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + dev: false + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + dev: false + + /lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + dev: false + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + dev: false + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + /log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + dev: true + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + + /lossless-json@2.0.11: + resolution: {integrity: sha512-BP0vn+NGYvzDielvBZaFain/wgeJ1hTvURCqtKvhr1SCPePdaaTanmmcplrHfEJSJOUql7hk4FHwToNJjWRY3g==} + dev: false + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + + /lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + dev: true + + /lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + /make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + dependencies: + semver: 6.3.1 + dev: false + + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + /makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + dependencies: + tmpl: 1.0.5 + dev: false + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: false + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: true + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: true + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + /min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + dev: true + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + + /minimatch@5.1.2: + resolution: {integrity: sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimatch@7.4.2: + resolution: {integrity: sha512-xy4q7wou3vUoC9k1xGTXc+awNdGaGVHtFUaey8tiX4H1QRc04DZ/rmDFwNm2EBsuYEhAZ6SgMmYf3InGY6OauA==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimist@1.2.6: + resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==} + dev: true + + /minipass@4.2.4: + resolution: {integrity: sha512-lwycX3cBMTvcejsHITUgYj6Gy6A7Nh4Q6h9NP4sTHY1ccJlC7yKzDmiShEHsJ16Jf1nKGDEaiHxiltsJEvk0nQ==} + engines: {node: '>=8'} + dev: true + + /mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + dev: true + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: true + + /mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + dev: true + + /nanoid@3.3.4: + resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + /node-fetch@2.6.7: + resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: true + + /node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + dev: false + + /node-releases@2.0.5: + resolution: {integrity: sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q==} + dev: false + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 + dev: false + + /nwsapi@2.2.0: + resolution: {integrity: sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==} + dev: true + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + + /object-is@1.1.5: + resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + /object.assign@4.1.2: + resolution: {integrity: sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + has-symbols: 1.0.3 + object-keys: 1.1.1 + + /object.entries@1.1.5: + resolution: {integrity: sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: true + + /object.entries@1.1.7: + resolution: {integrity: sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: true + + /object.fromentries@2.0.5: + resolution: {integrity: sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: true + + /object.fromentries@2.0.7: + resolution: {integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: true + + /object.hasown@1.1.1: + resolution: {integrity: sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==} + dependencies: + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: true + + /object.values@1.1.5: + resolution: {integrity: sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: true + + /object.values@1.1.7: + resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + + /ono@4.0.11: + resolution: {integrity: sha512-jQ31cORBFE6td25deYeD80wxKBMj+zBmHTrVxnc6CKhx8gho6ipmWM5zj/oeoqioZ99yqBls9Z/9Nss7J26G2g==} + dependencies: + format-util: 1.0.5 + dev: false + + /open@8.4.0: + resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==} + engines: {node: '>=12'} + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + dev: true + + /optionator@0.8.3: + resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.3 + fast-levenshtein: 2.0.6 + levn: 0.3.0 + prelude-ls: 1.1.2 + type-check: 0.3.2 + word-wrap: 1.2.5 + dev: true + + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.3 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.6.1 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + dev: true + + /os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + dev: true + + /p-limit@1.3.0: + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} + engines: {node: '>=4'} + dependencies: + p-try: 1.0.0 + dev: true + + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: false + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + + /p-locate@2.0.0: + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} + engines: {node: '>=4'} + dependencies: + p-limit: 1.3.0 + dev: true + + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + dev: false + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /p-try@1.0.0: + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} + engines: {node: '>=4'} + dev: true + + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: false + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.18.6 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + /parse-ms@2.1.0: + resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} + engines: {node: '>=6'} + dev: false + + /parse5@7.1.1: + resolution: {integrity: sha512-kwpuwzB+px5WUg9pyK0IcK/shltJN5/OVhQagxhCQNtT9Y9QRZqNY2e1cmbu/paRh5LMnz/oVTVLBpjFmMZhSg==} + dependencies: + entities: 4.4.0 + dev: true + + /path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + dev: true + + /path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + /path-scurry@1.6.1: + resolution: {integrity: sha512-OW+5s+7cw6253Q4E+8qQ/u1fVvcJQCJo/VFD8pje+dbJCF1n5ZRMV2AEHbGp+5Q7jxQIYJxkHopnj6nzdGeZLA==} + engines: {node: '>=14'} + dependencies: + lru-cache: 7.18.3 + minipass: 4.2.4 + dev: true + + /path-to-regexp@2.4.0: + resolution: {integrity: sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==} + dev: false + + /path-to-regexp@3.2.0: + resolution: {integrity: sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==} + dev: true + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + /pirates@4.0.5: + resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} + engines: {node: '>= 6'} + dev: false + + /pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + dev: false + + /postcss-value-parser@4.1.0: + resolution: {integrity: sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==} + + /postcss@8.4.20: + resolution: {integrity: sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.4 + picocolors: 1.0.0 + source-map-js: 1.0.2 + + /prelude-ls@1.1.2: + resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} + engines: {node: '>= 0.8.0'} + dev: true + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + dependencies: + fast-diff: 1.2.0 + dev: true + + /prettier@2.8.4: + resolution: {integrity: sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==} + engines: {node: '>=10.13.0'} + hasBin: true + dev: true + + /pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + /pretty-format@29.6.3: + resolution: {integrity: sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + + /pretty-ms@7.0.1: + resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} + engines: {node: '>=10'} + dependencies: + parse-ms: 2.1.0 + dev: false + + /prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + dev: false + + /prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + /property-expr@2.0.5: + resolution: {integrity: sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==} + dev: false + + /psl@1.8.0: + resolution: {integrity: sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==} + dev: true + + /punycode@2.1.1: + resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} + engines: {node: '>=6'} + + /pure-rand@6.0.0: + resolution: {integrity: sha512-rLSBxJjP+4DQOgcJAx6RZHT2he2pkhQdSnofG5VWyVl6GRq/K02ISOuOLcsMOrtKDIJb8JN2zm3FFzWNbezdPw==} + dev: false + + /querystring@0.2.1: + resolution: {integrity: sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==} + engines: {node: '>=0.4.x'} + deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. + dev: false + + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /react-ace@10.1.0(react-dom@18.1.0)(react@18.2.0): + resolution: {integrity: sha512-VkvUjZNhdYTuKOKQpMIZi7uzZZVgzCjM7cLYu6F64V0mejY8a2XTyPUIMszC6A4trbeMIHbK5fYFcT/wkP/8VA==} + peerDependencies: + react: ^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + ace-builds: 1.7.1 + diff-match-patch: 1.0.5 + lodash.get: 4.4.2 + lodash.isequal: 4.5.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.1.0(react@18.2.0) + dev: false + + /react-datepicker@4.10.0(react-dom@18.1.0)(react@18.2.0): + resolution: {integrity: sha512-6IfBCZyWj54ZZGLmEZJ9c4Yph0s9MVfEGDC2evOvf9AmVz+RRcfP2Czqad88Ff9wREbcbqa4dk7IFYeXF1d3Ag==} + peerDependencies: + react: ^16.9.0 || ^17 || ^18 + react-dom: ^16.9.0 || ^17 || ^18 + dependencies: + '@popperjs/core': 2.9.2 + classnames: 2.3.1 + date-fns: 2.27.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.1.0(react@18.2.0) + react-onclickoutside: 6.12.2(react-dom@18.1.0)(react@18.2.0) + react-popper: 2.3.0(@popperjs/core@2.9.2)(react-dom@18.1.0)(react@18.2.0) + dev: false + + /react-dom@18.1.0(react@18.2.0): + resolution: {integrity: sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==} + peerDependencies: + react: ^18.1.0 + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.22.0 + + /react-error-boundary@3.1.4(react@18.2.0): + resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==} + engines: {node: '>=10', npm: '>=6'} + peerDependencies: + react: '>=16.13.1' + dependencies: + '@babel/runtime': 7.17.9 + react: 18.2.0 + dev: false + + /react-fast-compare@3.2.0: + resolution: {integrity: sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==} + + /react-hook-form@7.43.1(react@18.2.0): + resolution: {integrity: sha512-+s3+s8LLytRMriwwuSqeLStVjRXFGxgjjx2jED7Z+wz1J/88vpxieRQGvJVvzrzVxshZ0BRuocFERb779m2kNg==} + engines: {node: '>=12.22.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + dependencies: + react: 18.2.0 + dev: false + + /react-hot-toast@2.4.1(csstype@3.1.2)(react-dom@18.1.0)(react@18.2.0): + resolution: {integrity: sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + dependencies: + goober: 2.1.10(csstype@3.1.2) + react: 18.2.0 + react-dom: 18.1.0(react@18.2.0) + transitivePeerDependencies: + - csstype + dev: false + + /react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + /react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + /react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + + /react-multi-select-component@4.3.3(react-dom@18.1.0)(react@18.2.0): + resolution: {integrity: sha512-V8cDJC3M7F27PWv1baV8FpJReHa/SbpJGL80CmXwnlMkDK2KMlQSRDmDzBnmCjcbROIgoztdW+gYBpqo9BIF4g==} + peerDependencies: + react: ^16 || ^17 || ^18 + react-dom: ^16 || ^17 || ^18 + dependencies: + react: 18.2.0 + react-dom: 18.1.0(react@18.2.0) + dev: false + + /react-onclickoutside@6.12.2(react-dom@18.1.0)(react@18.2.0): + resolution: {integrity: sha512-NMXGa223OnsrGVp5dJHkuKxQ4czdLmXSp5jSV9OqiCky9LOpPATn3vLldc+q5fK3gKbEHvr7J1u0yhBh/xYkpA==} + peerDependencies: + react: ^15.5.x || ^16.x || ^17.x || ^18.x + react-dom: ^15.5.x || ^16.x || ^17.x || ^18.x + dependencies: + react: 18.2.0 + react-dom: 18.1.0(react@18.2.0) + dev: false + + /react-popper@2.2.5(@popperjs/core@2.9.2)(react@18.2.0): + resolution: {integrity: sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw==} + peerDependencies: + '@popperjs/core': ^2.0.0 + react: ^16.8.0 || ^17 + dependencies: + '@popperjs/core': 2.9.2 + react: 18.2.0 + react-fast-compare: 3.2.0 + warning: 4.0.3 + dev: true + + /react-popper@2.3.0(@popperjs/core@2.9.2)(react-dom@18.1.0)(react@18.2.0): + resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==} + peerDependencies: + '@popperjs/core': ^2.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + dependencies: + '@popperjs/core': 2.9.2 + react: 18.2.0 + react-dom: 18.1.0(react@18.2.0) + react-fast-compare: 3.2.0 + warning: 4.0.3 + dev: false + + /react-redux@8.0.2(@types/react-dom@18.0.5)(@types/react@18.2.21)(react-dom@18.1.0)(react@18.2.0)(redux@4.2.0): + resolution: {integrity: sha512-nBwiscMw3NoP59NFCXFf02f8xdo+vSHT/uZ1ldDwF7XaTpzm+Phk97VT4urYBl5TYAPNVaFm12UHAEyzkpNzRA==} + peerDependencies: + '@types/react': ^16.8 || ^17.0 || ^18.0 + '@types/react-dom': ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + react-native: '>=0.59' + redux: ^4 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + react-dom: + optional: true + react-native: + optional: true + redux: + optional: true + dependencies: + '@babel/runtime': 7.17.9 + '@types/hoist-non-react-statics': 3.3.1 + '@types/react': 18.2.21 + '@types/react-dom': 18.0.5 + '@types/use-sync-external-store': 0.0.3 + hoist-non-react-statics: 3.3.2 + react: 18.2.0 + react-dom: 18.1.0(react@18.2.0) + react-is: 18.2.0 + redux: 4.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false + + /react-router-dom@6.15.0(react-dom@18.1.0)(react@18.2.0): + resolution: {integrity: sha512-aR42t0fs7brintwBGAv2+mGlCtgtFQeOzK0BM1/OiqEzRejOZtpMZepvgkscpMUnKb8YO84G7s3LsHnnDNonbQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + dependencies: + '@remix-run/router': 1.8.0 + react: 18.2.0 + react-dom: 18.1.0(react@18.2.0) + react-router: 6.15.0(react@18.2.0) + dev: false + + /react-router@6.15.0(react@18.2.0): + resolution: {integrity: sha512-NIytlzvzLwJkCQj2HLefmeakxxWHWAP+02EGqWEZy+DgfHHKQMUoBBjUQLOtFInBMhWtb3hiUy6MfFgwLjXhqg==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + dependencies: + '@remix-run/router': 1.8.0 + react: 18.2.0 + dev: false + + /react-transition-state@1.1.5(react-dom@18.1.0)(react@18.2.0): + resolution: {integrity: sha512-ITY2mZqc2dWG2eitJkYNdcSFW8aKeOlkL2A/vowRrLL8GH3J6Re/SpD/BLvQzrVOTqjsP0b5S9N10vgNNzwMUQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + react: 18.2.0 + react-dom: 18.1.0(react@18.2.0) + dev: false + + /react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + + /readable-stream@3.6.0: + resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + + /redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + dev: true + + /redux-thunk@2.4.1(redux@4.2.0): + resolution: {integrity: sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==} + peerDependencies: + redux: ^4 + dependencies: + redux: 4.2.0 + dev: false + + /redux@4.2.0: + resolution: {integrity: sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==} + dependencies: + '@babel/runtime': 7.17.9 + dev: false + + /reflect-metadata@0.1.13: + resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} + dev: true + + /regenerator-runtime@0.13.7: + resolution: {integrity: sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==} + + /regenerator-runtime@0.14.0: + resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} + + /regexp.prototype.flags@1.5.0: + resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + functions-have-names: 1.2.3 + + /regexpp@3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + dev: true + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + /require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: false + + /requireindex@1.2.0: + resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==} + engines: {node: '>=0.10.5'} + dev: true + + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: true + + /reselect@4.1.5: + resolution: {integrity: sha512-uVdlz8J7OO+ASpBYoz1Zypgx0KasCY20H+N8JD13oUMtPvSHQuscrHop4KbXrbsBcdB9Ds7lVK7eRkBIfO43vQ==} + dev: false + + /resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 + dev: false + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: false + + /resolve.exports@2.0.1: + resolution: {integrity: sha512-OEJWVeimw8mgQuj3HfkNl4KqRevH7lzeQNaWRPfx0PPse7Jk6ozcsG4FKVgtzDsC1KUF+YlTHh17NcgHOPykLw==} + engines: {node: '>=10'} + dev: false + + /resolve@1.22.0: + resolution: {integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==} + hasBin: true + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /resolve@1.22.1: + resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} + hasBin: true + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + /resolve@1.22.4: + resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==} + hasBin: true + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + /resolve@2.0.0-next.3: + resolution: {integrity: sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==} + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + dev: true + + /restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.0 + dev: true + + /rimraf@4.3.1: + resolution: {integrity: sha512-GfHJHBzFQra23IxDzIdBqhOWfbtdgS1/dCHrDy+yvhpoJY5TdwdT28oWaHWfRpKFDLd3GZnGTx6Mlt4+anbsxQ==} + engines: {node: '>=14'} + hasBin: true + dependencies: + glob: 9.2.1 + dev: true + + /rollup@3.7.3: + resolution: {integrity: sha512-7e68MQbAWCX6mI4/0lG1WHd+NdNAlVamg0Zkd+8LZ/oXojligdGnCNyHlzXqXCZObyjs5FRc3AH0b17iJESGIQ==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + + /run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /rxjs@6.6.7: + resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} + engines: {npm: '>=2.0.0'} + dependencies: + tslib: 1.13.0 + dev: true + + /rxjs@7.8.0: + resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==} + dependencies: + tslib: 2.5.0 + dev: true + + /safe-array-concat@1.0.0: + resolution: {integrity: sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: true + + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: false + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-regex: 1.1.4 + dev: true + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: true + + /sass@1.66.1: + resolution: {integrity: sha512-50c+zTsZOJVgFfTgwwEzkjA3/QACgdNsKueWPyAR0mRINIvLAStVQBbPg14iuqEQ74NPDbXzJARJ/O4SI1zftA==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + chokidar: 3.5.2 + immutable: 4.0.0 + source-map-js: 1.0.2 + + /saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: true + + /scheduler@0.22.0: + resolution: {integrity: sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==} + dependencies: + loose-envify: 1.4.0 + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + /semver@7.3.7: + resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + + /shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + object-inspect: 1.12.3 + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: false + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + /slash@4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} + dev: true + + /slash@5.0.0: + resolution: {integrity: sha512-n6KkmvKS0623igEVj3FF0OZs1gYYJ0o0Hj939yc1fyxl2xt+xYpLnzJB6xBSqOfV9ZFLEWodBBN/heZJahuIJQ==} + engines: {node: '>=14.16'} + dev: false + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + + /source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: false + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + /spawn-command@0.0.2-1: + resolution: {integrity: sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==} + dev: true + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: false + + /stack-utils@2.0.5: + resolution: {integrity: sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==} + engines: {node: '>=10'} + dependencies: + escape-string-regexp: 2.0.0 + + /stop-iteration-iterator@1.0.0: + resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} + engines: {node: '>= 0.4'} + dependencies: + internal-slot: 1.0.5 + + /string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + dev: false + + /string-length@5.0.1: + resolution: {integrity: sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==} + engines: {node: '>=12.20'} + dependencies: + char-regex: 2.0.1 + strip-ansi: 7.0.1 + dev: false + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + /string.prototype.matchall@4.0.7: + resolution: {integrity: sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + regexp.prototype.flags: 1.5.0 + side-channel: 1.0.4 + dev: true + + /string.prototype.trim@1.2.7: + resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: true + + /string.prototype.trimend@1.0.6: + resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: true + + /string.prototype.trimstart@1.0.6: + resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: true + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + + /strip-ansi@7.0.1: + resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: false + + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + + /strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + dev: false + + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + dev: false + + /strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + dependencies: + min-indent: 1.0.1 + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + /styled-components@5.3.1(react-dom@18.1.0)(react-is@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-JThv2JRzyH0NOIURrk9iskdxMSAAtCfj/b2Sf1WJaCUsloQkblepy1jaCLX/bYE+mhYo3unmwVSI9I5d9ncSiQ==} + engines: {node: '>=10'} + peerDependencies: + react: '>= 16.8.0' + react-dom: '>= 16.8.0' + react-is: '>= 16.8.0' + dependencies: + '@babel/helper-module-imports': 7.16.7 + '@babel/traverse': 7.18.2(supports-color@5.5.0) + '@emotion/is-prop-valid': 0.8.8 + '@emotion/stylis': 0.8.5 + '@emotion/unitless': 0.7.5 + babel-plugin-styled-components: 1.13.2(styled-components@5.3.1) + css-to-react-native: 3.0.0 + hoist-non-react-statics: 3.3.2 + react: 18.2.0 + react-dom: 18.1.0(react@18.2.0) + react-is: 18.2.0 + shallowequal: 1.1.0 + supports-color: 5.5.0 + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + /symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: true + + /synckit@0.8.1: + resolution: {integrity: sha512-rJEeygO5PNmcZICmrgnbOd2usi5zWE1ESc0Gn5tTmJlongoU8zCTwMFQtar2UgMSiR68vK9afPQ+uVs2lURSIA==} + engines: {node: ^14.18.0 || >=16.0.0} + dependencies: + '@pkgr/utils': 2.3.0 + tslib: 2.5.0 + dev: true + + /tabbable@6.1.1: + resolution: {integrity: sha512-4kl5w+nCB44EVRdO0g/UGoOp3vlwgycUVtkk/7DPyeLZUCuNFFKCFG6/t/DgHLrUPHjrZg6s5tNm+56Q2B0xyg==} + dev: false + + /tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + dev: true + + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.0 + minimatch: 3.1.2 + dev: false + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + dev: true + + /tiny-case@1.0.3: + resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} + dev: false + + /tiny-glob@0.2.9: + resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} + dependencies: + globalyzer: 0.1.0 + globrex: 0.1.2 + dev: true + + /tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + dependencies: + os-tmpdir: 1.0.2 + dev: true + + /tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + dev: false + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + + /toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + dev: false + + /tough-cookie@4.1.3: + resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} + engines: {node: '>=6'} + dependencies: + psl: 1.8.0 + punycode: 2.1.1 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: true + + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: true + + /tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + dependencies: + punycode: 2.1.1 + dev: false + + /tr46@3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + dependencies: + punycode: 2.1.1 + dev: true + + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: true + + /true-myth@4.1.1: + resolution: {integrity: sha512-rqy30BSpxPznbbTcAcci90oZ1YR4DqvKcNXNerG5gQBU2v4jk0cygheiul5J6ExIMrgDVuanv/MkGfqZbKrNNg==} + engines: {node: 10.* || >= 12.*} + dev: true + + /ts-morph@13.0.3: + resolution: {integrity: sha512-pSOfUMx8Ld/WUreoSzvMFQG5i9uEiWIsBYjpU9+TTASOeUa89j5HykomeqVULm1oqWtBdleI3KEFRLrlA3zGIw==} + dependencies: + '@ts-morph/common': 0.12.3 + code-block-writer: 11.0.1 + dev: true + + /ts-node@10.9.1(@swc/core@1.3.38)(@types/node@16.11.7)(typescript@4.7.4): + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@swc/core': 1.3.38 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.3 + '@types/node': 16.11.7 + acorn: 8.7.1 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.7.4 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + /ts-prune@0.10.3: + resolution: {integrity: sha512-iS47YTbdIcvN8Nh/1BFyziyUqmjXz7GVzWu02RaZXqb+e/3Qe1B7IQ4860krOeCGUeJmterAlaM2FRH0Ue0hjw==} + hasBin: true + dependencies: + commander: 6.2.1 + cosmiconfig: 7.0.1 + json5: 2.2.1 + lodash: 4.17.21 + true-myth: 4.1.1 + ts-morph: 13.0.3 + dev: true + + /tsconfck@2.1.2(typescript@4.7.4): + resolution: {integrity: sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==} + engines: {node: ^14.13.1 || ^16 || >=18} + hasBin: true + peerDependencies: + typescript: ^4.3.5 || ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + dependencies: + typescript: 4.7.4 + dev: false + + /tsconfig-paths@3.14.1: + resolution: {integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==} + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.6 + strip-bom: 3.0.0 + dev: true + + /tslib@1.13.0: + resolution: {integrity: sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==} + dev: true + + /tslib@2.0.3: + resolution: {integrity: sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==} + dev: true + + /tslib@2.5.0: + resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} + + /tsutils@3.21.0(typescript@4.7.4): + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.13.0 + typescript: 4.7.4 + dev: true + + /type-check@0.3.2: + resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.1.2 + dev: true + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + /type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + dev: false + + /type-fest@3.6.1: + resolution: {integrity: sha512-htXWckxlT6U4+ilVgweNliPqlsVSSucbxVexRYllyMVJDtf5rTjv6kF/s+qAd4QSL1BZcnJPEJavYBPQiWuZDA==} + engines: {node: '>=14.16'} + dev: false + + /typed-array-buffer@1.0.0: + resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-length@1.0.0: + resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-offset@1.0.0: + resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + is-typed-array: 1.1.12 + dev: true + + /typescript@4.7.4: + resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==} + engines: {node: '>=4.2.0'} + hasBin: true + + /uid@2.0.1: + resolution: {integrity: sha512-PF+1AnZgycpAIEmNtjxGBVmKbZAQguaa4pBUq6KNaGEcpzZ2klCNZLM34tsjp76maN00TttiiUf6zkIBpJQm2A==} + engines: {node: '>=8'} + dependencies: + '@lukeed/csprng': 1.1.0 + dev: true + + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + + /universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: true + + /universalify@2.0.0: + resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + engines: {node: '>= 10.0.0'} + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.1.1 + + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: true + + /use-debounce@9.0.4(react@18.2.0): + resolution: {integrity: sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==} + engines: {node: '>= 10.0.0'} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + dev: false + + /use-sync-external-store@1.2.0(react@18.2.0): + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + /v8-to-istanbul@9.0.1: + resolution: {integrity: sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.19 + '@types/istanbul-lib-coverage': 2.0.3 + convert-source-map: 1.7.0 + dev: false + + /vite-plugin-ejs@1.6.4: + resolution: {integrity: sha512-23p1RS4PiA0veXY5/gHZ60pl3pPvd8NEqdBsDgxNK8nM1rjFFDcVb0paNmuipzCgNP/Y0f/Id22M7Il4kvZ2jA==} + dependencies: + ejs: 3.1.8 + dev: true + + /vite-tsconfig-paths@4.2.1(typescript@4.7.4)(vite@4.0.5): + resolution: {integrity: sha512-GNUI6ZgPqT3oervkvzU+qtys83+75N/OuDaQl7HmOqFTb0pjZsuARrRipsyJhJ3enqV8beI1xhGbToR4o78nSQ==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + dependencies: + debug: 4.3.4(supports-color@5.5.0) + globrex: 0.1.2 + tsconfck: 2.1.2(typescript@4.7.4) + vite: 4.0.5(@types/node@16.11.7)(sass@1.66.1) + transitivePeerDependencies: + - supports-color + - typescript + dev: false + + /vite@4.0.5(@types/node@16.11.7)(sass@1.66.1): + resolution: {integrity: sha512-7m87RC+caiAxG+8j3jObveRLqaWA/neAdCat6JAZwMkSWqFHOvg8MYe5fAQxVBRAuKAQ1S6XDh3CBQuLNbY33w==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 16.11.7 + esbuild: 0.16.4 + postcss: 8.4.20 + resolve: 1.22.1 + rollup: 3.7.3 + sass: 1.66.1 + optionalDependencies: + fsevents: 2.3.2 + + /w3c-hr-time@1.0.2: + resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} + deprecated: Use your platform's native performance.now() and performance.timeOrigin. + dependencies: + browser-process-hrtime: 1.0.0 + dev: true + + /w3c-xmlserializer@3.0.0: + resolution: {integrity: sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==} + engines: {node: '>=12'} + dependencies: + xml-name-validator: 4.0.0 + dev: true + + /walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + dependencies: + makeerror: 1.0.12 + dev: false + + /warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + dependencies: + loose-envify: 1.4.0 + + /wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + dependencies: + defaults: 1.0.3 + dev: true + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: true + + /webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + dev: false + + /webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: true + + /whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + dependencies: + iconv-lite: 0.6.3 + dev: true + + /whatwg-fetch@3.6.2: + resolution: {integrity: sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==} + dev: false + + /whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + dev: true + + /whatwg-url@11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} + dependencies: + tr46: 3.0.0 + webidl-conversions: 7.0.0 + dev: true + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: true + + /whatwg-url@6.5.0: + resolution: {integrity: sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==} + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + dev: false + + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.2 + is-boolean-object: 1.1.1 + is-number-object: 1.0.5 + is-string: 1.0.7 + is-symbol: 1.0.4 + + /which-collection@1.0.1: + resolution: {integrity: sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==} + dependencies: + is-map: 2.0.2 + is-set: 2.0.2 + is-weakmap: 2.0.1 + is-weakset: 2.0.2 + + /which-typed-array@1.1.11: + resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + + /word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + /write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + dev: false + + /ws@8.8.0: + resolution: {integrity: sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + + /xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + dev: true + + /xml@1.0.1: + resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} + dev: true + + /xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: true + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + /yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + dev: true + + /yargs-parser@20.2.7: + resolution: {integrity: sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==} + engines: {node: '>=10'} + dev: true + + /yargs-parser@21.0.1: + resolution: {integrity: sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==} + engines: {node: '>=12'} + dev: false + + /yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.7 + dev: true + + /yargs@17.5.1: + resolution: {integrity: sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==} + engines: {node: '>=12'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.0.1 + dev: false + + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + /yup@1.0.2: + resolution: {integrity: sha512-Lpi8nITFKjWtCoK3yQP8MUk78LJmHWqbFd0OOMXTar+yjejlQ4OIIoZgnTW1bnEUKDw6dZBcy3/IdXnt2KDUow==} + dependencies: + property-expr: 2.0.5 + tiny-case: 1.0.3 + toposort: 2.0.2 + type-fest: 2.19.0 + dev: false + + /zustand@4.1.1(react@18.2.0): + resolution: {integrity: sha512-h4F3WMqsZgvvaE0n3lThx4MM81Ls9xebjvrABNzf5+jb3/03YjNTSgZXeyrvXDArMeV9untvWXRw1tY+ntPYbA==} + engines: {node: '>=12.7.0'} + peerDependencies: + immer: '>=9.0' + react: '>=16.8' + peerDependenciesMeta: + immer: + optional: true + react: + optional: true + dependencies: + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false diff --git a/kafka-ui-react-app/public/favicon.ico b/kafka-ui-react-app/public/favicon/favicon.ico similarity index 100% rename from kafka-ui-react-app/public/favicon.ico rename to kafka-ui-react-app/public/favicon/favicon.ico diff --git a/kafka-ui-react-app/public/fonts/Inter-Medium.ttf b/kafka-ui-react-app/public/fonts/Inter-Medium.ttf new file mode 100644 index 00000000000..9a3396fc436 Binary files /dev/null and b/kafka-ui-react-app/public/fonts/Inter-Medium.ttf differ diff --git a/kafka-ui-react-app/public/fonts/Inter-Regular.ttf b/kafka-ui-react-app/public/fonts/Inter-Regular.ttf new file mode 100644 index 00000000000..2c164bb2df5 Binary files /dev/null and b/kafka-ui-react-app/public/fonts/Inter-Regular.ttf differ diff --git a/kafka-ui-react-app/public/fonts/RobotoMono-Medium.ttf b/kafka-ui-react-app/public/fonts/RobotoMono-Medium.ttf new file mode 100644 index 00000000000..395fabe1e9a Binary files /dev/null and b/kafka-ui-react-app/public/fonts/RobotoMono-Medium.ttf differ diff --git a/kafka-ui-react-app/public/fonts/RobotoMono-Regular.ttf b/kafka-ui-react-app/public/fonts/RobotoMono-Regular.ttf new file mode 100644 index 00000000000..2ab8f34a5bb Binary files /dev/null and b/kafka-ui-react-app/public/fonts/RobotoMono-Regular.ttf differ diff --git a/kafka-ui-react-app/public/index.html b/kafka-ui-react-app/public/index.html deleted file mode 100644 index a49f8a1b7ce..00000000000 --- a/kafka-ui-react-app/public/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - UI for Apache Kafka - - - - -
- - diff --git a/kafka-ui-react-app/public/manifest.json b/kafka-ui-react-app/public/manifest.json index 1f6e4871af1..fd521d3d839 100644 --- a/kafka-ui-react-app/public/manifest.json +++ b/kafka-ui-react-app/public/manifest.json @@ -11,9 +11,5 @@ "type": "image/png", "sizes": "512x512" } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" -} \ No newline at end of file + ] +} diff --git a/kafka-ui-react-app/public/robots.txt b/kafka-ui-react-app/public/robots.txt index 01b0f9a1073..1f53798bb4f 100644 --- a/kafka-ui-react-app/public/robots.txt +++ b/kafka-ui-react-app/public/robots.txt @@ -1,2 +1,2 @@ -# https://www.robotstxt.org/robotstxt.html User-agent: * +Disallow: / diff --git a/kafka-ui-react-app/sonar-project.properties b/kafka-ui-react-app/sonar-project.properties index cea009d27a0..d19822fabc6 100644 --- a/kafka-ui-react-app/sonar-project.properties +++ b/kafka-ui-react-app/sonar-project.properties @@ -2,7 +2,7 @@ sonar.projectKey=com.provectus:kafka-ui_frontend sonar.organization=provectus sonar.sources=. -sonar.exclusions=**/__tests__/**,**/__test__/**,src/serviceWorker.ts,src/setupTests.ts,src/setupProxy.js,**/fixtures.ts,src/lib/testHelpers.tsx,src/index.tsx +sonar.exclusions=**/__tests__/**,**/__test__/**,src/serviceWorker.ts,src/setupTests.ts,src/setupProxy.js,**/fixtures.ts,src/lib/fixtures/**,src/lib/testHelpers.tsx,src/index.tsx,vite.config.ts,config/** sonar.typescript.lcov.reportPaths=./coverage/lcov.info sonar.testExecutionReportPaths=./test-report.xml diff --git a/kafka-ui-react-app/src/components/ACLPage/ACLPage.tsx b/kafka-ui-react-app/src/components/ACLPage/ACLPage.tsx new file mode 100644 index 00000000000..616198716d2 --- /dev/null +++ b/kafka-ui-react-app/src/components/ACLPage/ACLPage.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { Routes, Route } from 'react-router-dom'; +import ACList from 'components/ACLPage/List/List'; + +const ACLPage = () => { + return ( + + } /> + + ); +}; + +export default ACLPage; diff --git a/kafka-ui-react-app/src/components/ACLPage/List/List.styled.ts b/kafka-ui-react-app/src/components/ACLPage/List/List.styled.ts new file mode 100644 index 00000000000..287214e1e62 --- /dev/null +++ b/kafka-ui-react-app/src/components/ACLPage/List/List.styled.ts @@ -0,0 +1,44 @@ +import styled from 'styled-components'; + +export const EnumCell = styled.div` + text-transform: capitalize; +`; + +export const DeleteCell = styled.div` + svg { + cursor: pointer; + } +`; + +export const Chip = styled.div<{ + chipType?: 'default' | 'success' | 'danger' | 'secondary' | string; +}>` + width: fit-content; + text-transform: capitalize; + padding: 2px 8px; + font-size: 12px; + line-height: 16px; + border-radius: 16px; + color: ${({ theme }) => theme.tag.color}; + background-color: ${({ theme, chipType }) => { + switch (chipType) { + case 'success': + return theme.tag.backgroundColor.green; + case 'danger': + return theme.tag.backgroundColor.red; + case 'secondary': + return theme.tag.backgroundColor.secondary; + default: + return theme.tag.backgroundColor.gray; + } + }}; +`; + +export const PatternCell = styled.div` + display: flex; + align-items: center; + + ${Chip} { + margin-left: 4px; + } +`; diff --git a/kafka-ui-react-app/src/components/ACLPage/List/List.tsx b/kafka-ui-react-app/src/components/ACLPage/List/List.tsx new file mode 100644 index 00000000000..499f255c307 --- /dev/null +++ b/kafka-ui-react-app/src/components/ACLPage/List/List.tsx @@ -0,0 +1,153 @@ +import React from 'react'; +import { ColumnDef } from '@tanstack/react-table'; +import { useTheme } from 'styled-components'; +import PageHeading from 'components/common/PageHeading/PageHeading'; +import Table from 'components/common/NewTable'; +import DeleteIcon from 'components/common/Icons/DeleteIcon'; +import { useConfirm } from 'lib/hooks/useConfirm'; +import useAppParams from 'lib/hooks/useAppParams'; +import { useAcls, useDeleteAcl } from 'lib/hooks/api/acl'; +import { ClusterName } from 'redux/interfaces'; +import { + KafkaAcl, + KafkaAclNamePatternType, + KafkaAclPermissionEnum, +} from 'generated-sources'; + +import * as S from './List.styled'; + +const ACList: React.FC = () => { + const { clusterName } = useAppParams<{ clusterName: ClusterName }>(); + const theme = useTheme(); + const { data: aclList } = useAcls(clusterName); + const { deleteResource } = useDeleteAcl(clusterName); + const modal = useConfirm(true); + + const [rowId, setRowId] = React.useState(''); + + const onDeleteClick = (acl: KafkaAcl | null) => { + if (acl) { + modal('Are you sure want to delete this ACL record?', () => + deleteResource(acl) + ); + } + }; + + const columns = React.useMemo[]>( + () => [ + { + header: 'Principal', + accessorKey: 'principal', + size: 257, + }, + { + header: 'Resource', + accessorKey: 'resourceType', + // eslint-disable-next-line react/no-unstable-nested-components + cell: ({ getValue }) => ( + {getValue().toLowerCase()} + ), + size: 145, + }, + { + header: 'Pattern', + accessorKey: 'resourceName', + // eslint-disable-next-line react/no-unstable-nested-components + cell: ({ getValue, row }) => { + let chipType; + if ( + row.original.namePatternType === KafkaAclNamePatternType.PREFIXED + ) { + chipType = 'default'; + } + + if ( + row.original.namePatternType === KafkaAclNamePatternType.LITERAL + ) { + chipType = 'secondary'; + } + return ( + + {getValue()} + {chipType ? ( + + {row.original.namePatternType.toLowerCase()} + + ) : null} + + ); + }, + size: 257, + }, + { + header: 'Host', + accessorKey: 'host', + size: 257, + }, + { + header: 'Operation', + accessorKey: 'operation', + // eslint-disable-next-line react/no-unstable-nested-components + cell: ({ getValue }) => ( + {getValue().toLowerCase()} + ), + size: 121, + }, + { + header: 'Permission', + accessorKey: 'permission', + // eslint-disable-next-line react/no-unstable-nested-components + cell: ({ getValue }) => ( + () === KafkaAclPermissionEnum.ALLOW + ? 'success' + : 'danger' + } + > + {getValue().toLowerCase()} + + ), + size: 111, + }, + { + id: 'delete', + // eslint-disable-next-line react/no-unstable-nested-components + cell: ({ row }) => { + return ( + onDeleteClick(row.original)}> + + + ); + }, + size: 76, + }, + ], + [rowId] + ); + + const onRowHover = (value: unknown) => { + if (value && typeof value === 'object' && 'id' in value) { + setRowId(value.id as string); + } + }; + + return ( + <> + + setRowId('')} + /> + + ); +}; + +export default ACList; diff --git a/kafka-ui-react-app/src/components/ACLPage/List/__test__/List.spec.tsx b/kafka-ui-react-app/src/components/ACLPage/List/__test__/List.spec.tsx new file mode 100644 index 00000000000..0c39681bbd4 --- /dev/null +++ b/kafka-ui-react-app/src/components/ACLPage/List/__test__/List.spec.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { render, WithRoute } from 'lib/testHelpers'; +import { screen } from '@testing-library/dom'; +import userEvent from '@testing-library/user-event'; +import { clusterACLPath } from 'lib/paths'; +import ACList from 'components/ACLPage/List/List'; +import { useAcls, useDeleteAcl } from 'lib/hooks/api/acl'; +import { aclPayload } from 'lib/fixtures/acls'; + +jest.mock('lib/hooks/api/acl', () => ({ + useAcls: jest.fn(), + useDeleteAcl: jest.fn(), +})); + +describe('ACLList Component', () => { + const clusterName = 'local'; + const renderComponent = () => + render( + + + , + { + initialEntries: [clusterACLPath(clusterName)], + } + ); + + describe('ACLList', () => { + describe('when the acls are loaded', () => { + beforeEach(() => { + (useAcls as jest.Mock).mockImplementation(() => ({ + data: aclPayload, + })); + (useDeleteAcl as jest.Mock).mockImplementation(() => ({ + deleteResource: jest.fn(), + })); + }); + + it('renders ACLList with records', async () => { + renderComponent(); + expect(screen.getByRole('table')).toBeInTheDocument(); + expect(screen.getAllByRole('row').length).toEqual(4); + }); + + it('shows delete icon on hover', async () => { + const { container } = renderComponent(); + const [trElement] = screen.getAllByRole('row'); + await userEvent.hover(trElement); + const deleteElement = container.querySelector('svg'); + expect(deleteElement).not.toHaveStyle({ + fill: 'transparent', + }); + }); + }); + + describe('when it has no acls', () => { + beforeEach(() => { + (useAcls as jest.Mock).mockImplementation(() => ({ + data: [], + })); + (useDeleteAcl as jest.Mock).mockImplementation(() => ({ + deleteResource: jest.fn(), + })); + }); + + it('renders empty ACLList with message', async () => { + renderComponent(); + expect(screen.getByRole('table')).toBeInTheDocument(); + expect( + screen.getByRole('row', { name: 'No ACL items found' }) + ).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/Alerts/Alert.tsx b/kafka-ui-react-app/src/components/Alerts/Alert.tsx deleted file mode 100644 index bca0d3b2f89..00000000000 --- a/kafka-ui-react-app/src/components/Alerts/Alert.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import CloseIcon from 'components/common/Icons/CloseIcon'; -import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper'; -import { Alert as AlertType } from 'redux/interfaces'; - -import * as S from './Alert.styled'; - -export interface AlertProps { - title: AlertType['title']; - type: AlertType['type']; - message: AlertType['message']; - onDissmiss(): void; -} - -const Alert: React.FC = ({ title, type, message, onDissmiss }) => ( - -
- {title} - -
- - - - -
-); - -export default Alert; diff --git a/kafka-ui-react-app/src/components/Alerts/Alerts.tsx b/kafka-ui-react-app/src/components/Alerts/Alerts.tsx deleted file mode 100644 index 11249dceaea..00000000000 --- a/kafka-ui-react-app/src/components/Alerts/Alerts.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { alertDissmissed, selectAll } from 'redux/reducers/alerts/alertsSlice'; -import { useAppSelector, useAppDispatch } from 'lib/hooks/redux'; -import Alert from 'components/Alerts/Alert'; - -const Alerts: React.FC = () => { - const alerts = useAppSelector(selectAll); - const dispatch = useAppDispatch(); - const dismiss = (id: string) => () => { - dispatch(alertDissmissed(id)); - }; - - return ( - <> - {alerts.map(({ id, type, title, message }) => ( - - ))} - - ); -}; - -export default Alerts; diff --git a/kafka-ui-react-app/src/components/Alerts/__tests__/Alerts.spec.tsx b/kafka-ui-react-app/src/components/Alerts/__tests__/Alerts.spec.tsx deleted file mode 100644 index ee9fd0bacf2..00000000000 --- a/kafka-ui-react-app/src/components/Alerts/__tests__/Alerts.spec.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import { ServerResponse } from 'redux/interfaces'; -import { act, screen } from '@testing-library/react'; -import Alerts from 'components/Alerts/Alerts'; -import { render } from 'lib/testHelpers'; -import { store } from 'redux/store'; -import { UnknownAsyncThunkRejectedWithValueAction } from '@reduxjs/toolkit/dist/matchers'; -import userEvent from '@testing-library/user-event'; - -const payload: ServerResponse = { - status: 422, - statusText: 'Unprocessable Entity', - message: 'Unprocessable Entity', - url: 'https://test.com/clusters', -}; -const action: UnknownAsyncThunkRejectedWithValueAction = { - type: 'any/action/rejected', - payload, - meta: { - arg: 'test', - requestId: 'test-request-id', - requestStatus: 'rejected', - aborted: false, - condition: false, - rejectedWithValue: true, - }, - error: { message: 'Rejected' }, -}; - -describe('Alerts', () => { - it('renders alerts', async () => { - store.dispatch(action); - - await act(() => { - render(, { store }); - }); - - expect(screen.getAllByRole('alert').length).toEqual(1); - - const dissmissAlertButtons = screen.getAllByRole('button'); - expect(dissmissAlertButtons.length).toEqual(1); - - const dissmissButton = dissmissAlertButtons[0]; - - userEvent.click(dissmissButton); - - expect(screen.queryAllByRole('alert').length).toEqual(0); - }); -}); diff --git a/kafka-ui-react-app/src/components/App.styled.ts b/kafka-ui-react-app/src/components/App.styled.ts index cb01bd94fc0..418e5d8a124 100644 --- a/kafka-ui-react-app/src/components/App.styled.ts +++ b/kafka-ui-react-app/src/components/App.styled.ts @@ -1,5 +1,4 @@ -import styled, { css } from 'styled-components'; -import { Link } from 'react-router-dom'; +import styled from 'styled-components'; export const Layout = styled.div` min-width: 1200px; @@ -8,199 +7,3 @@ export const Layout = styled.div` min-width: initial; } `; - -export const Container = styled.main( - ({ theme }) => css` - margin-top: ${theme.layout.navBarHeight}; - margin-left: ${theme.layout.navBarWidth}; - position: relative; - z-index: 20; - - @media screen and (max-width: 1023px) { - margin-left: initial; - } - ` -); - -export const Sidebar = styled.div<{ $visible: boolean }>( - ({ theme, $visible }) => css` - width: ${theme.layout.navBarWidth}; - display: flex; - flex-direction: column; - border-right: 1px solid ${theme.layout.stuffBorderColor}; - position: fixed; - top: ${theme.layout.navBarHeight}; - left: 0; - bottom: 0; - padding: 8px 16px; - overflow-y: scroll; - transition: width 0.25s, opacity 0.25s, transform 0.25s, - -webkit-transform 0.25s; - background: ${theme.menu.backgroundColor.normal}; - @media screen and (max-width: 1023px) { - ${$visible && - `transform: translate3d(${theme.layout.navBarWidth}, 0, 0)`}; - left: -${theme.layout.navBarWidth}; - z-index: 100; - } - - &::-webkit-scrollbar { - width: 8px; - } - - &::-webkit-scrollbar-track { - background-color: ${theme.scrollbar.trackColor.normal}; - } - - &::-webkit-scrollbar-thumb { - width: 8px; - background-color: ${theme.scrollbar.thumbColor.normal}; - border-radius: 4px; - } - - &:hover::-webkit-scrollbar-thumb { - background: ${theme.scrollbar.thumbColor.active}; - } - - &:hover::-webkit-scrollbar-track { - background-color: ${theme.scrollbar.trackColor.active}; - } - ` -); - -export const Overlay = styled.div<{ $visible: boolean }>( - ({ theme, $visible }) => css` - height: calc(100vh - ${theme.layout.navBarHeight}); - z-index: 99; - visibility: 'hidden'; - opacity: 0; - -webkit-transition: all 0.5s ease; - transition: all 0.5s ease; - left: 0; - position: absolute; - top: 0; - ${$visible && - css` - @media screen and (max-width: 1023px) { - bottom: 0; - right: 0; - visibility: 'visible'; - opacity: 1; - background-color: ${theme.layout.overlay.backgroundColor}; - } - `} - ` -); - -export const Navbar = styled.nav( - ({ theme }) => css` - border-bottom: 1px solid ${theme.layout.stuffBorderColor}; - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 30; - background-color: ${theme.menu.backgroundColor.normal}; - min-height: 3.25rem; - ` -); - -export const NavbarBrand = styled.div` - display: flex; - flex-shrink: 0; - align-items: stretch; - min-height: 3.25rem; -`; - -export const NavbarItem = styled.div` - display: flex; - position: relative; - flex-grow: 0; - flex-shrink: 0; - align-items: center; - line-height: 1.5; - padding: 0.5rem 0.75rem; -`; - -export const NavbarBurger = styled.div( - ({ theme }) => css` - display: block; - position: relative; - cursor: pointer; - height: 3.25rem; - width: 3.25rem; - margin: 0; - padding: 0; - - &:hover { - background-color: ${theme.menu.backgroundColor.hover}; - } - - @media screen and (min-width: 1024px) { - display: none; - } - ` -); - -export const Span = styled.span( - ({ theme }) => css` - display: block; - position: absolute; - background: ${theme.menu.color.active}; - height: 1px; - left: calc(50% - 8px); - transform-origin: center; - transition-duration: 86ms; - transition-property: background-color, opacity, transform, -webkit-transform; - transition-timing-function: ease-out; - width: 16px; - - &:first-child { - top: calc(50% - 6px); - } - &:nth-child(2) { - top: calc(50% - 1px); - } - &:nth-child(3) { - top: calc(50% + 4px); - } - ` -); - -export const Hyperlink = styled(Link)( - ({ theme }) => css` - position: relative; - - display: flex; - flex-grow: 0; - flex-shrink: 0; - align-items: center; - gap: 8px; - - margin: 0; - padding: 0.5rem 0.75rem; - - font-family: Inter, sans-serif; - font-style: normal; - font-weight: bold; - font-size: 12px; - line-height: 16px; - color: ${theme.menu.color.active}; - text-decoration: none; - word-break: break-word; - cursor: pointer; - ` -); - -export const AlertsContainer = styled.div` - max-width: 40%; - width: 500px; - position: fixed; - bottom: 15px; - right: 15px; - z-index: 1000; - - @media screen and (max-width: 1023px) { - max-width: initial; - } -`; diff --git a/kafka-ui-react-app/src/components/App.tsx b/kafka-ui-react-app/src/components/App.tsx index 5f4824f125d..43abc60bd1a 100644 --- a/kafka-ui-react-app/src/components/App.tsx +++ b/kafka-ui-react-app/src/components/App.tsx @@ -1,109 +1,99 @@ -import React from 'react'; -import { Routes, Route, useLocation } from 'react-router-dom'; -import { GIT_TAG, GIT_COMMIT } from 'lib/constants'; -import { clusterPath, getNonExactPath } from 'lib/paths'; -import Nav from 'components/Nav/Nav'; +import React, { Suspense, useContext } from 'react'; +import { Routes, Route, Navigate } from 'react-router-dom'; +import { + accessErrorPage, + clusterPath, + errorPage, + getNonExactPath, + clusterNewConfigPath, +} from 'lib/paths'; import PageLoader from 'components/common/PageLoader/PageLoader'; import Dashboard from 'components/Dashboard/Dashboard'; -import ClusterPage from 'components/Cluster/Cluster'; -import Version from 'components/Version/Version'; -import Alerts from 'components/Alerts/Alerts'; +import ClusterPage from 'components/ClusterPage/ClusterPage'; import { ThemeProvider } from 'styled-components'; -import theme from 'theme/theme'; -import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; -import { - fetchClusters, - getClusterList, - getAreClustersFulfilled, -} from 'redux/reducers/clusters/clustersSlice'; +import { theme, darkTheme } from 'theme/theme'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { showServerError } from 'lib/errorHandling'; +import { Toaster } from 'react-hot-toast'; +import GlobalCSS from 'components/globalCss'; +import * as S from 'components/App.styled'; +import ClusterConfigForm from 'widgets/ClusterConfigForm'; +import { ThemeModeContext } from 'components/contexts/ThemeModeContext'; -import * as S from './App.styled'; -import Logo from './common/Logo/Logo'; +import ConfirmationModal from './common/ConfirmationModal/ConfirmationModal'; +import { ConfirmContextProvider } from './contexts/ConfirmContext'; +import { GlobalSettingsProvider } from './contexts/GlobalSettingsContext'; +import ErrorPage from './ErrorPage/ErrorPage'; +import { UserInfoRolesAccessProvider } from './contexts/UserInfoRolesAccessContext'; +import PageContainer from './PageContainer/PageContainer'; +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + suspense: true, + networkMode: 'offlineFirst', + onError(error) { + showServerError(error as Response); + }, + }, + mutations: { + onError(error) { + showServerError(error as Response); + }, + }, + }, +}); const App: React.FC = () => { - const dispatch = useAppDispatch(); - const areClustersFulfilled = useAppSelector(getAreClustersFulfilled); - const clusters = useAppSelector(getClusterList); - const [isSidebarVisible, setIsSidebarVisible] = React.useState(false); - - const onBurgerClick = () => setIsSidebarVisible(!isSidebarVisible); - const closeSidebar = () => setIsSidebarVisible(false); - - const location = useLocation(); - - React.useEffect(() => { - closeSidebar(); - }, [closeSidebar, location]); - - React.useEffect(() => { - dispatch(fetchClusters()); - }, [dispatch]); + const { isDarkMode } = useContext(ThemeModeContext); return ( - - - - - - - - - - - - - UI for Apache Kafka - - - - {GIT_TAG && } - - - - - - -
+ ); +}; + +export default BrokerLogdir; diff --git a/kafka-ui-react-app/src/components/Brokers/Broker/BrokerLogdir/__test__/BrokerLogdir.spec.tsx b/kafka-ui-react-app/src/components/Brokers/Broker/BrokerLogdir/__test__/BrokerLogdir.spec.tsx new file mode 100644 index 00000000000..b8ae5bcd581 --- /dev/null +++ b/kafka-ui-react-app/src/components/Brokers/Broker/BrokerLogdir/__test__/BrokerLogdir.spec.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { render, WithRoute } from 'lib/testHelpers'; +import { screen } from '@testing-library/dom'; +import { clusterBrokerPath } from 'lib/paths'; +import { brokerLogDirsPayload } from 'lib/fixtures/brokers'; +import { useBrokerLogDirs } from 'lib/hooks/api/brokers'; +import { BrokerLogdirs } from 'generated-sources'; +import BrokerLogdir from 'components/Brokers/Broker/BrokerLogdir/BrokerLogdir'; + +jest.mock('lib/hooks/api/brokers', () => ({ + useBrokerLogDirs: jest.fn(), +})); + +const clusterName = 'local'; +const brokerId = 1; + +describe('BrokerLogdir Component', () => { + const renderComponent = async (payload?: BrokerLogdirs[]) => { + (useBrokerLogDirs as jest.Mock).mockImplementation(() => ({ + data: payload, + })); + await render( + + + , + { + initialEntries: [clusterBrokerPath(clusterName, brokerId)], + } + ); + }; + + it('shows warning when server returns undefined logDirs response', async () => { + await renderComponent(); + expect( + screen.getByRole('row', { name: 'Log dir data not available' }) + ).toBeInTheDocument(); + }); + + it('shows warning when server returns empty logDirs response', async () => { + await renderComponent([]); + expect( + screen.getByRole('row', { name: 'Log dir data not available' }) + ).toBeInTheDocument(); + }); + + it('shows brokers', async () => { + await renderComponent(brokerLogDirsPayload); + expect( + screen.queryByRole('row', { name: 'Log dir data not available' }) + ).not.toBeInTheDocument(); + + expect( + screen.getByRole('row', { + name: '/opt/kafka/data-0/logs NONE 3 4', + }) + ).toBeInTheDocument(); + expect( + screen.getByRole('row', { + name: '/opt/kafka/data-1/logs NONE 0 0', + }) + ).toBeInTheDocument(); + }); +}); diff --git a/kafka-ui-react-app/src/components/Brokers/Broker/BrokerMetrics/BrokerMetrics.tsx b/kafka-ui-react-app/src/components/Brokers/Broker/BrokerMetrics/BrokerMetrics.tsx new file mode 100644 index 00000000000..40fe42fe711 --- /dev/null +++ b/kafka-ui-react-app/src/components/Brokers/Broker/BrokerMetrics/BrokerMetrics.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import useAppParams from 'lib/hooks/useAppParams'; +import { ClusterBrokerParam } from 'lib/paths'; +import { useBrokerMetrics } from 'lib/hooks/api/brokers'; +import { SchemaType } from 'generated-sources'; +import EditorViewer from 'components/common/EditorViewer/EditorViewer'; +import { getEditorText } from 'components/Brokers/utils/getEditorText'; + +const BrokerMetrics: React.FC = () => { + const { clusterName, brokerId } = useAppParams(); + const { data: metrics } = useBrokerMetrics(clusterName, Number(brokerId)); + + return ( + + ); +}; + +export default BrokerMetrics; diff --git a/kafka-ui-react-app/src/components/Brokers/Broker/BrokerMetrics/__test__/BrokerMetrics.spec.tsx b/kafka-ui-react-app/src/components/Brokers/Broker/BrokerMetrics/__test__/BrokerMetrics.spec.tsx new file mode 100644 index 00000000000..4000ec7c76e --- /dev/null +++ b/kafka-ui-react-app/src/components/Brokers/Broker/BrokerMetrics/__test__/BrokerMetrics.spec.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { render, WithRoute } from 'lib/testHelpers'; +import { screen } from '@testing-library/dom'; +import { clusterBrokerMetricsPath } from 'lib/paths'; +import BrokerMetrics from 'components/Brokers/Broker/BrokerMetrics/BrokerMetrics'; +import { useBrokerMetrics } from 'lib/hooks/api/brokers'; + +jest.mock('lib/hooks/api/brokers', () => ({ + useBrokerMetrics: jest.fn(), +})); + +const clusterName = 'local'; +const brokerId = 1; + +describe('BrokerMetrics Component', () => { + it("shows warning when server doesn't return metrics response", async () => { + (useBrokerMetrics as jest.Mock).mockImplementation(() => ({ + data: {}, + })); + + render( + + + , + { + initialEntries: [clusterBrokerMetricsPath(clusterName, brokerId)], + } + ); + expect(screen.getAllByRole('textbox').length).toEqual(1); + }); +}); diff --git a/kafka-ui-react-app/src/components/Brokers/Broker/Configs/Configs.styled.ts b/kafka-ui-react-app/src/components/Brokers/Broker/Configs/Configs.styled.ts new file mode 100644 index 00000000000..cced53373e8 --- /dev/null +++ b/kafka-ui-react-app/src/components/Brokers/Broker/Configs/Configs.styled.ts @@ -0,0 +1,36 @@ +import styled from 'styled-components'; + +export const ValueWrapper = styled.div` + display: flex; + justify-content: space-between; + button { + margin: 0 10px; + } +`; + +export const Value = styled.span` + line-height: 24px; + margin-right: 10px; + text-overflow: ellipsis; + max-width: 400px; + overflow: hidden; + white-space: nowrap; +`; + +export const ButtonsWrapper = styled.div` + display: flex; +`; +export const SearchWrapper = styled.div` + margin: 10px; + width: 21%; +`; + +export const Source = styled.div` + display: flex; + align-content: center; + svg { + margin-left: 10px; + vertical-align: middle; + cursor: pointer; + } +`; diff --git a/kafka-ui-react-app/src/components/Brokers/Broker/Configs/Configs.tsx b/kafka-ui-react-app/src/components/Brokers/Broker/Configs/Configs.tsx new file mode 100644 index 00000000000..5b05b7cc5be --- /dev/null +++ b/kafka-ui-react-app/src/components/Brokers/Broker/Configs/Configs.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { CellContext, ColumnDef } from '@tanstack/react-table'; +import { ClusterBrokerParam } from 'lib/paths'; +import useAppParams from 'lib/hooks/useAppParams'; +import { + useBrokerConfig, + useUpdateBrokerConfigByName, +} from 'lib/hooks/api/brokers'; +import Table from 'components/common/NewTable'; +import { BrokerConfig, ConfigSource } from 'generated-sources'; +import Search from 'components/common/Search/Search'; +import Tooltip from 'components/common/Tooltip/Tooltip'; +import InfoIcon from 'components/common/Icons/InfoIcon'; + +import InputCell from './InputCell'; +import * as S from './Configs.styled'; + +const tooltipContent = `DYNAMIC_TOPIC_CONFIG = dynamic topic config that is configured for a specific topic +DYNAMIC_BROKER_LOGGER_CONFIG = dynamic broker logger config that is configured for a specific broker +DYNAMIC_BROKER_CONFIG = dynamic broker config that is configured for a specific broker +DYNAMIC_DEFAULT_BROKER_CONFIG = dynamic broker config that is configured as default for all brokers in the cluster +STATIC_BROKER_CONFIG = static broker config provided as broker properties at start up (e.g. server.properties file) +DEFAULT_CONFIG = built-in default configuration for configs that have a default value +UNKNOWN = source unknown e.g. in the ConfigEntry used for alter requests where source is not set`; + +const Configs: React.FC = () => { + const [keyword, setKeyword] = React.useState(''); + const { clusterName, brokerId } = useAppParams(); + const { data = [] } = useBrokerConfig(clusterName, Number(brokerId)); + const stateMutation = useUpdateBrokerConfigByName( + clusterName, + Number(brokerId) + ); + + const getData = () => { + return data + .filter((item) => { + const nameMatch = item.name + .toLocaleLowerCase() + .includes(keyword.toLocaleLowerCase()); + return nameMatch + ? true + : item.value && + item.value + .toLocaleLowerCase() + .includes(keyword.toLocaleLowerCase()); // try to match the keyword on any of the item.value elements when nameMatch fails but item.value exists + }) + .sort((a, b) => { + if (a.source === b.source) return 0; + return a.source === ConfigSource.DYNAMIC_BROKER_CONFIG ? -1 : 1; + }); + }; + + const dataSource = React.useMemo(() => getData(), [data, keyword]); + + const renderCell = (props: CellContext) => ( + { + stateMutation.mutateAsync({ + name, + brokerConfigItem: { + value, + }, + }); + }} + /> + ); + + const columns = React.useMemo[]>( + () => [ + { header: 'Key', accessorKey: 'name' }, + { + header: 'Value', + accessorKey: 'value', + cell: renderCell, + }, + { + // eslint-disable-next-line react/no-unstable-nested-components + header: () => { + return ( + + Source + } + content={tooltipContent} + placement="top-end" + /> + + ); + }, + accessorKey: 'source', + }, + ], + [] + ); + + return ( + <> + + + +
+ + ); +}; + +export default Configs; diff --git a/kafka-ui-react-app/src/components/Brokers/Broker/Configs/InputCell.tsx b/kafka-ui-react-app/src/components/Brokers/Broker/Configs/InputCell.tsx new file mode 100644 index 00000000000..bf54c45c617 --- /dev/null +++ b/kafka-ui-react-app/src/components/Brokers/Broker/Configs/InputCell.tsx @@ -0,0 +1,91 @@ +import React, { useEffect } from 'react'; +import { CellContext } from '@tanstack/react-table'; +import CheckmarkIcon from 'components/common/Icons/CheckmarkIcon'; +import EditIcon from 'components/common/Icons/EditIcon'; +import CancelIcon from 'components/common/Icons/CancelIcon'; +import { useConfirm } from 'lib/hooks/useConfirm'; +import { Action, BrokerConfig, ResourceType } from 'generated-sources'; +import { Button } from 'components/common/Button/Button'; +import Input from 'components/common/Input/Input'; +import { ActionButton } from 'components/common/ActionComponent'; + +import * as S from './Configs.styled'; + +interface InputCellProps extends CellContext { + onUpdate: (name: string, value?: string) => void; +} + +const InputCell: React.FC = ({ row, getValue, onUpdate }) => { + const initialValue = `${getValue()}`; + const [isEdit, setIsEdit] = React.useState(false); + const [value, setValue] = React.useState(initialValue); + + const confirm = useConfirm(); + + const onSave = () => { + if (value !== initialValue) { + confirm('Are you sure you want to change the value?', async () => { + onUpdate(row?.original?.name, value); + }); + } + setIsEdit(false); + }; + + useEffect(() => { + setValue(initialValue); + }, [initialValue]); + + return isEdit ? ( + + setValue(target?.value)} + /> + + + + + + ) : ( + + {initialValue} + setIsEdit(true)} + permission={{ + resource: ResourceType.CLUSTERCONFIG, + action: Action.EDIT, + }} + > + Edit + + + ); +}; + +export default InputCell; diff --git a/kafka-ui-react-app/src/components/Brokers/Broker/Configs/__test__/Configs.spec.tsx b/kafka-ui-react-app/src/components/Brokers/Broker/Configs/__test__/Configs.spec.tsx new file mode 100644 index 00000000000..d82065eb321 --- /dev/null +++ b/kafka-ui-react-app/src/components/Brokers/Broker/Configs/__test__/Configs.spec.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { screen } from '@testing-library/dom'; +import { render, WithRoute } from 'lib/testHelpers'; +import { clusterBrokerConfigsPath } from 'lib/paths'; +import { useBrokerConfig } from 'lib/hooks/api/brokers'; +import { brokerConfigPayload } from 'lib/fixtures/brokers'; +import Configs from 'components/Brokers/Broker/Configs/Configs'; +import userEvent from '@testing-library/user-event'; + +const clusterName = 'Cluster_Name'; +const brokerId = 'Broker_Id'; + +jest.mock('lib/hooks/api/brokers', () => ({ + useBrokerConfig: jest.fn(), + useUpdateBrokerConfigByName: jest.fn(), +})); + +describe('Configs', () => { + const renderComponent = () => { + const path = clusterBrokerConfigsPath(clusterName, brokerId); + return render( + + + , + { initialEntries: [path] } + ); + }; + + beforeEach(() => { + (useBrokerConfig as jest.Mock).mockImplementation(() => ({ + data: brokerConfigPayload, + })); + renderComponent(); + }); + + it('renders configs table', async () => { + expect(screen.getByRole('table')).toBeInTheDocument(); + expect(screen.getAllByRole('row').length).toEqual( + brokerConfigPayload.length + 1 + ); + }); + + it('updates textbox value', async () => { + await userEvent.click(screen.getAllByLabelText('editAction')[0]); + + const textbox = screen.getByLabelText('inputValue'); + expect(textbox).toBeInTheDocument(); + expect(textbox).toHaveValue('producer'); + + await userEvent.type(textbox, 'new value'); + + expect( + screen.getByRole('button', { name: 'confirmAction' }) + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'cancelAction' }) + ).toBeInTheDocument(); + + await userEvent.click( + screen.getByRole('button', { name: 'confirmAction' }) + ); + + expect( + screen.getByText('Are you sure you want to change the value?') + ).toBeInTheDocument(); + }); +}); diff --git a/kafka-ui-react-app/src/components/Brokers/Broker/__test__/Broker.spec.tsx b/kafka-ui-react-app/src/components/Brokers/Broker/__test__/Broker.spec.tsx new file mode 100644 index 00000000000..40a5deeebfe --- /dev/null +++ b/kafka-ui-react-app/src/components/Brokers/Broker/__test__/Broker.spec.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { render, WithRoute } from 'lib/testHelpers'; +import { screen } from '@testing-library/dom'; +import { + clusterBrokerMetricsPath, + clusterBrokerPath, + getNonExactPath, +} from 'lib/paths'; +import Broker from 'components/Brokers/Broker/Broker'; +import { useBrokers } from 'lib/hooks/api/brokers'; +import { useClusterStats } from 'lib/hooks/api/clusters'; +import { brokersPayload } from 'lib/fixtures/brokers'; +import { clusterStatsPayload } from 'lib/fixtures/clusters'; + +const clusterName = 'local'; +const brokerId = 200; +const activeClassName = 'is-active'; +const brokerLogdir = { + pageText: 'brokerLogdir', + navigationName: 'Log directories', +}; +const brokerMetrics = { + pageText: 'brokerMetrics', + navigationName: 'Metrics', +}; + +jest.mock('components/Brokers/Broker/BrokerLogdir/BrokerLogdir', () => () => ( +
{brokerLogdir.pageText}
+)); +jest.mock('components/Brokers/Broker/BrokerMetrics/BrokerMetrics', () => () => ( +
{brokerMetrics.pageText}
+)); +jest.mock('lib/hooks/api/brokers', () => ({ + useBrokers: jest.fn(), +})); +jest.mock('lib/hooks/api/clusters', () => ({ + useClusterStats: jest.fn(), +})); + +describe('Broker Component', () => { + beforeEach(() => { + (useBrokers as jest.Mock).mockImplementation(() => ({ + data: brokersPayload, + })); + (useClusterStats as jest.Mock).mockImplementation(() => ({ + data: clusterStatsPayload, + })); + }); + const renderComponent = (path = clusterBrokerPath(clusterName, brokerId)) => + render( + + + , + { + initialEntries: [path], + } + ); + + it('shows broker found', async () => { + await renderComponent(); + const brokerInfo = brokersPayload.find((broker) => broker.id === brokerId); + const brokerDiskUsage = clusterStatsPayload.diskUsage.find( + (disk) => disk.brokerId === brokerId + ); + + expect( + screen.getByText(brokerDiskUsage?.segmentCount || '') + ).toBeInTheDocument(); + expect(screen.getByText('11.77 MB')).toBeInTheDocument(); + + expect(screen.getByText('Segment Count')).toBeInTheDocument(); + expect( + screen.getByText(brokerDiskUsage?.segmentCount || '') + ).toBeInTheDocument(); + + expect(screen.getByText('Port')).toBeInTheDocument(); + expect(screen.getByText(brokerInfo?.port || '')).toBeInTheDocument(); + + expect(screen.getByText('Host')).toBeInTheDocument(); + expect(screen.getByText(brokerInfo?.host || '')).toBeInTheDocument(); + }); + + it('renders Broker Logdir', async () => { + await renderComponent(); + + const logdirLink = screen.getByRole('link', { + name: brokerLogdir.navigationName, + }); + expect(logdirLink).toBeInTheDocument(); + expect(logdirLink).toHaveClass(activeClassName); + + expect(screen.getByText(brokerLogdir.pageText)).toBeInTheDocument(); + }); + + it('renders Broker Metrics', async () => { + await renderComponent(clusterBrokerMetricsPath(clusterName, brokerId)); + + const metricsLink = screen.getByRole('link', { + name: brokerMetrics.navigationName, + }); + expect(metricsLink).toBeInTheDocument(); + expect(metricsLink).toHaveClass(activeClassName); + + expect(screen.getByText(brokerMetrics.pageText)).toBeInTheDocument(); + }); +}); diff --git a/kafka-ui-react-app/src/components/Brokers/Brokers.tsx b/kafka-ui-react-app/src/components/Brokers/Brokers.tsx index 80ced9baed5..742ac937ca8 100644 --- a/kafka-ui-react-app/src/components/Brokers/Brokers.tsx +++ b/kafka-ui-react-app/src/components/Brokers/Brokers.tsx @@ -1,146 +1,22 @@ import React from 'react'; -import useInterval from 'lib/hooks/useInterval'; -import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted'; -import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; -import { Table } from 'components/common/table/Table/Table.styled'; -import PageHeading from 'components/common/PageHeading/PageHeading'; -import * as Metrics from 'components/common/Metrics'; -import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; -import { ClusterNameRoute } from 'lib/paths'; -import { - fetchBrokers, - fetchClusterStats, - selectStats, -} from 'redux/reducers/brokers/brokersSlice'; -import useAppParams from 'lib/hooks/useAppParams'; +import { Route, Routes } from 'react-router-dom'; +import { getNonExactPath, RouteParams } from 'lib/paths'; +import BrokersList from 'components/Brokers/BrokersList/BrokersList'; +import Broker from 'components/Brokers/Broker/Broker'; +import SuspenseQueryComponent from 'components/common/SuspenseQueryComponent/SuspenseQueryComponent'; -const Brokers: React.FC = () => { - const dispatch = useAppDispatch(); - const { clusterName } = useAppParams(); - const { - brokerCount, - activeControllers, - onlinePartitionCount, - offlinePartitionCount, - inSyncReplicasCount, - outOfSyncReplicasCount, - underReplicatedPartitionCount, - diskUsage, - version, - items, - } = useAppSelector(selectStats); - - const replicas = (inSyncReplicasCount ?? 0) + (outOfSyncReplicasCount ?? 0); - const areAllInSync = inSyncReplicasCount && replicas === inSyncReplicasCount; - const partitionIsOffline = offlinePartitionCount && offlinePartitionCount > 0; - React.useEffect(() => { - dispatch(fetchClusterStats(clusterName)); - dispatch(fetchBrokers(clusterName)); - }, [clusterName, dispatch]); - - useInterval(() => { - fetchClusterStats(clusterName); - fetchBrokers(clusterName); - }, 5000); - - return ( - <> - - - - - {brokerCount} - - - {activeControllers} - - {version} - - - - {partitionIsOffline ? ( - {onlinePartitionCount} - ) : ( - onlinePartitionCount - )} - - {' '} - of {(onlinePartitionCount || 0) + (offlinePartitionCount || 0)} - - - - {!underReplicatedPartitionCount ? ( - - {underReplicatedPartitionCount} - - ) : ( - {underReplicatedPartitionCount} - )} - - - {areAllInSync ? ( - replicas - ) : ( - {inSyncReplicasCount} - )} - of {replicas} - - - {outOfSyncReplicasCount} - - - -
- - - - - - - - - - - {(!diskUsage || diskUsage.length === 0) && ( - - - - )} - - {diskUsage && - diskUsage.length !== 0 && - diskUsage.map(({ brokerId, segmentSize, segmentCount }) => { - const brokerItem = items?.find((item) => item.id === brokerId); - - return ( - - - - - - - - ); - })} - -
Disk usage data not available
{brokerId} - - {segmentCount}{brokerItem?.port}{brokerItem?.host}
- - ); -}; +const Brokers: React.FC = () => ( + + } /> + + + + } + /> + +); export default Brokers; diff --git a/kafka-ui-react-app/src/components/Brokers/BrokersList/BrokersList.styled.ts b/kafka-ui-react-app/src/components/Brokers/BrokersList/BrokersList.styled.ts new file mode 100644 index 00000000000..964e64368dc --- /dev/null +++ b/kafka-ui-react-app/src/components/Brokers/BrokersList/BrokersList.styled.ts @@ -0,0 +1,16 @@ +import styled from 'styled-components'; + +export const RowCell = styled.div` + display: flex; + width: 100%; + align-items: center; + + svg { + width: 20px; + padding-left: 6px; + } +`; + +export const DangerText = styled.span` + color: ${({ theme }) => theme.circularAlert.color.error}; +`; diff --git a/kafka-ui-react-app/src/components/Brokers/BrokersList/BrokersList.tsx b/kafka-ui-react-app/src/components/Brokers/BrokersList/BrokersList.tsx new file mode 100644 index 00000000000..ede570c655b --- /dev/null +++ b/kafka-ui-react-app/src/components/Brokers/BrokersList/BrokersList.tsx @@ -0,0 +1,255 @@ +import React from 'react'; +import { ClusterName } from 'redux/interfaces'; +import { useNavigate } from 'react-router-dom'; +import PageHeading from 'components/common/PageHeading/PageHeading'; +import * as Metrics from 'components/common/Metrics'; +import useAppParams from 'lib/hooks/useAppParams'; +import { useBrokers } from 'lib/hooks/api/brokers'; +import { useClusterStats } from 'lib/hooks/api/clusters'; +import Table, { LinkCell, SizeCell } from 'components/common/NewTable'; +import CheckMarkRoundIcon from 'components/common/Icons/CheckMarkRoundIcon'; +import { ColumnDef } from '@tanstack/react-table'; +import { clusterBrokerPath } from 'lib/paths'; +import Tooltip from 'components/common/Tooltip/Tooltip'; +import ColoredCell from 'components/common/NewTable/ColoredCell'; + +import SkewHeader from './SkewHeader/SkewHeader'; +import * as S from './BrokersList.styled'; + +const NA = 'N/A'; + +const BrokersList: React.FC = () => { + const navigate = useNavigate(); + const { clusterName } = useAppParams<{ clusterName: ClusterName }>(); + const { data: clusterStats = {} } = useClusterStats(clusterName); + const { data: brokers } = useBrokers(clusterName); + + const { + brokerCount, + activeControllers, + onlinePartitionCount, + offlinePartitionCount, + inSyncReplicasCount, + outOfSyncReplicasCount, + underReplicatedPartitionCount, + diskUsage, + version, + } = clusterStats; + + const rows = React.useMemo(() => { + let brokersResource; + if (!diskUsage || !diskUsage?.length) { + brokersResource = + brokers?.map((broker) => { + return { + brokerId: broker.id, + segmentSize: NA, + segmentCount: NA, + }; + }) || []; + } else { + brokersResource = diskUsage; + } + + return brokersResource.map(({ brokerId, segmentSize, segmentCount }) => { + const broker = brokers?.find(({ id }) => id === brokerId); + return { + brokerId, + size: segmentSize || NA, + count: segmentCount || NA, + port: broker?.port, + host: broker?.host, + partitionsLeader: broker?.partitionsLeader, + partitionsSkew: broker?.partitionsSkew, + leadersSkew: broker?.leadersSkew, + inSyncPartitions: broker?.inSyncPartitions, + }; + }); + }, [diskUsage, brokers]); + + const columns = React.useMemo[]>( + () => [ + { + header: 'Broker ID', + accessorKey: 'brokerId', + // eslint-disable-next-line react/no-unstable-nested-components + cell: ({ getValue }) => ( + + ()}`} + to={encodeURIComponent(`${getValue()}`)} + /> + {getValue() === activeControllers && ( + } + content="Active Controller" + placement="right" + /> + )} + + ), + }, + { + header: 'Disk usage', + accessorKey: 'size', + // eslint-disable-next-line react/no-unstable-nested-components + cell: ({ getValue, table, cell, column, renderValue, row }) => + getValue() === NA ? ( + NA + ) : ( + + ), + }, + { + // eslint-disable-next-line react/no-unstable-nested-components + header: () => , + accessorKey: 'partitionsSkew', + // eslint-disable-next-line react/no-unstable-nested-components + cell: ({ getValue }) => { + const value = getValue(); + return ( + = 10 && value < 20} + attention={value >= 20} + /> + ); + }, + }, + { header: 'Leaders', accessorKey: 'partitionsLeader' }, + { + header: 'Leader skew', + accessorKey: 'leadersSkew', + // eslint-disable-next-line react/no-unstable-nested-components + cell: ({ getValue }) => { + const value = getValue(); + return ( + = 10 && value < 20} + attention={value >= 20} + /> + ); + }, + }, + { + header: 'Online partitions', + accessorKey: 'inSyncPartitions', + // eslint-disable-next-line react/no-unstable-nested-components + cell: ({ getValue, row }) => { + const value = getValue(); + return ( + + ); + }, + }, + { header: 'Port', accessorKey: 'port' }, + { + header: 'Host', + accessorKey: 'host', + }, + ], + [] + ); + + const replicas = (inSyncReplicasCount ?? 0) + (outOfSyncReplicasCount ?? 0); + const areAllInSync = inSyncReplicasCount && replicas === inSyncReplicasCount; + const partitionIsOffline = offlinePartitionCount && offlinePartitionCount > 0; + + const isActiveControllerUnKnown = typeof activeControllers === 'undefined'; + + return ( + <> + + + + + {brokerCount} + + + {isActiveControllerUnKnown ? ( + No Active Controller + ) : ( + activeControllers + )} + + {version} + + + + {partitionIsOffline ? ( + {onlinePartitionCount} + ) : ( + onlinePartitionCount + )} + + {` of ${ + (onlinePartitionCount || 0) + (offlinePartitionCount || 0) + } + `} + + + + {!underReplicatedPartitionCount ? ( + + {underReplicatedPartitionCount} + + ) : ( + {underReplicatedPartitionCount} + )} + + + {areAllInSync ? ( + replicas + ) : ( + {inSyncReplicasCount} + )} + of {replicas} + + + {outOfSyncReplicasCount} + + + + + navigate(clusterBrokerPath(clusterName, brokerId)) + } + emptyMessage="No clusters are online" + /> + + ); +}; + +export default BrokersList; diff --git a/kafka-ui-react-app/src/components/Brokers/BrokersList/SkewHeader/SkewHeader.styled.ts b/kafka-ui-react-app/src/components/Brokers/BrokersList/SkewHeader/SkewHeader.styled.ts new file mode 100644 index 00000000000..eea2fa3cd98 --- /dev/null +++ b/kafka-ui-react-app/src/components/Brokers/BrokersList/SkewHeader/SkewHeader.styled.ts @@ -0,0 +1,11 @@ +import styled from 'styled-components'; +import { MessageTooltip } from 'components/common/Tooltip/Tooltip.styled'; + +export const CellWrapper = styled.div` + display: flex; + gap: 10px; + + ${MessageTooltip} { + max-height: unset; + } +`; diff --git a/kafka-ui-react-app/src/components/Brokers/BrokersList/SkewHeader/SkewHeader.tsx b/kafka-ui-react-app/src/components/Brokers/BrokersList/SkewHeader/SkewHeader.tsx new file mode 100644 index 00000000000..978d1768dd7 --- /dev/null +++ b/kafka-ui-react-app/src/components/Brokers/BrokersList/SkewHeader/SkewHeader.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import Tooltip from 'components/common/Tooltip/Tooltip'; +import InfoIcon from 'components/common/Icons/InfoIcon'; + +import * as S from './SkewHeader.styled'; + +const SkewHeader: React.FC = () => ( + + Partitions skew + } + content="The divergence from the average brokers' value" + /> + +); + +export default SkewHeader; diff --git a/kafka-ui-react-app/src/components/Brokers/BrokersList/__test__/BrokersList.spec.tsx b/kafka-ui-react-app/src/components/Brokers/BrokersList/__test__/BrokersList.spec.tsx new file mode 100644 index 00000000000..3e88569a39e --- /dev/null +++ b/kafka-ui-react-app/src/components/Brokers/BrokersList/__test__/BrokersList.spec.tsx @@ -0,0 +1,203 @@ +import React from 'react'; +import { render, WithRoute } from 'lib/testHelpers'; +import { screen, waitFor } from '@testing-library/dom'; +import { clusterBrokerPath, clusterBrokersPath } from 'lib/paths'; +import BrokersList from 'components/Brokers/BrokersList/BrokersList'; +import userEvent from '@testing-library/user-event'; +import { useBrokers } from 'lib/hooks/api/brokers'; +import { useClusterStats } from 'lib/hooks/api/clusters'; +import { brokersPayload } from 'lib/fixtures/brokers'; +import { clusterStatsPayload } from 'lib/fixtures/clusters'; + +const mockedUsedNavigate = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockedUsedNavigate, +})); + +jest.mock('lib/hooks/api/brokers', () => ({ + useBrokers: jest.fn(), +})); +jest.mock('lib/hooks/api/clusters', () => ({ + useClusterStats: jest.fn(), +})); + +describe('BrokersList Component', () => { + const clusterName = 'local'; + + const testInSyncReplicasCount = 798; + const testOutOfSyncReplicasCount = 1; + + const renderComponent = () => + render( + + + , + { + initialEntries: [clusterBrokersPath(clusterName)], + } + ); + + describe('BrokersList', () => { + describe('when the brokers are loaded', () => { + beforeEach(() => { + (useBrokers as jest.Mock).mockImplementation(() => ({ + data: brokersPayload, + })); + (useClusterStats as jest.Mock).mockImplementation(() => ({ + data: clusterStatsPayload, + })); + }); + it('renders', async () => { + renderComponent(); + expect(screen.getByRole('table')).toBeInTheDocument(); + expect(screen.getAllByRole('row').length).toEqual(3); + }); + it('opens broker when row clicked', async () => { + renderComponent(); + await userEvent.click(screen.getByRole('cell', { name: '100' })); + + await waitFor(() => + expect(mockedUsedNavigate).toBeCalledWith( + clusterBrokerPath(clusterName, '100') + ) + ); + }); + it('shows warning when offlinePartitionCount > 0', async () => { + (useClusterStats as jest.Mock).mockImplementation(() => ({ + data: { + ...clusterStatsPayload, + offlinePartitionCount: 1345, + }, + })); + renderComponent(); + const onlineWidget = screen.getByText( + clusterStatsPayload.onlinePartitionCount + ); + expect(onlineWidget).toBeInTheDocument(); + expect(onlineWidget).toHaveStyle({ color: '#E51A1A' }); + }); + it('shows right count when offlinePartitionCount > 0', async () => { + (useClusterStats as jest.Mock).mockImplementation(() => ({ + data: { + ...clusterStatsPayload, + inSyncReplicasCount: testInSyncReplicasCount, + outOfSyncReplicasCount: testOutOfSyncReplicasCount, + }, + })); + renderComponent(); + const onlineWidgetDef = screen.getByText(testInSyncReplicasCount); + const onlineWidget = screen.getByText( + `of ${testInSyncReplicasCount + testOutOfSyncReplicasCount}` + ); + expect(onlineWidgetDef).toBeInTheDocument(); + expect(onlineWidget).toBeInTheDocument(); + }); + it('shows right count when inSyncReplicasCount: undefined && outOfSyncReplicasCount: 1', async () => { + (useClusterStats as jest.Mock).mockImplementation(() => ({ + data: { + ...clusterStatsPayload, + inSyncReplicasCount: undefined, + outOfSyncReplicasCount: testOutOfSyncReplicasCount, + }, + })); + renderComponent(); + const onlineWidget = screen.getByText( + `of ${testOutOfSyncReplicasCount}` + ); + expect(onlineWidget).toBeInTheDocument(); + }); + it(`shows right count when inSyncReplicasCount: ${testInSyncReplicasCount} outOfSyncReplicasCount: undefined`, async () => { + (useClusterStats as jest.Mock).mockImplementation(() => ({ + data: { + ...clusterStatsPayload, + inSyncReplicasCount: testInSyncReplicasCount, + outOfSyncReplicasCount: undefined, + }, + })); + renderComponent(); + const onlineWidgetDef = screen.getByText(testInSyncReplicasCount); + const onlineWidget = screen.getByText(`of ${testInSyncReplicasCount}`); + expect(onlineWidgetDef).toBeInTheDocument(); + expect(onlineWidget).toBeInTheDocument(); + }); + }); + + describe('BrokersList', () => { + describe('when the brokers are loaded', () => { + const testActiveControllers = 0; + beforeEach(() => { + (useBrokers as jest.Mock).mockImplementation(() => ({ + data: brokersPayload, + })); + (useClusterStats as jest.Mock).mockImplementation(() => ({ + data: clusterStatsPayload, + })); + }); + + it(`Indicates correct active cluster`, async () => { + renderComponent(); + await waitFor(() => + expect(screen.getByRole('tooltip')).toBeInTheDocument() + ); + }); + it(`Correct display even if there is no active cluster: ${testActiveControllers} `, async () => { + (useClusterStats as jest.Mock).mockImplementation(() => ({ + data: { + ...clusterStatsPayload, + activeControllers: testActiveControllers, + }, + })); + renderComponent(); + await waitFor(() => + expect(screen.queryByRole('tooltip')).not.toBeInTheDocument() + ); + }); + }); + }); + + describe('when diskUsage is empty', () => { + beforeEach(() => { + (useBrokers as jest.Mock).mockImplementation(() => ({ + data: brokersPayload, + })); + (useClusterStats as jest.Mock).mockImplementation(() => ({ + data: { ...clusterStatsPayload, diskUsage: undefined }, + })); + }); + + describe('when it has no brokers', () => { + beforeEach(() => { + (useBrokers as jest.Mock).mockImplementation(() => ({ + data: [], + })); + }); + + it('renders empty table', async () => { + renderComponent(); + expect(screen.getByRole('table')).toBeInTheDocument(); + expect( + screen.getByRole('row', { name: 'No clusters are online' }) + ).toBeInTheDocument(); + }); + }); + + it('renders list of all brokers', async () => { + renderComponent(); + expect(screen.getByRole('table')).toBeInTheDocument(); + expect(screen.getAllByRole('row').length).toEqual(3); + }); + it('opens broker when row clicked', async () => { + renderComponent(); + await userEvent.click(screen.getByRole('cell', { name: '100' })); + + await waitFor(() => + expect(mockedUsedNavigate).toBeCalledWith( + clusterBrokerPath(clusterName, '100') + ) + ); + }); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/Brokers/__test__/Brokers.spec.tsx b/kafka-ui-react-app/src/components/Brokers/__test__/Brokers.spec.tsx index 8e5d0157903..1b4bf761544 100644 --- a/kafka-ui-react-app/src/components/Brokers/__test__/Brokers.spec.tsx +++ b/kafka-ui-react-app/src/components/Brokers/__test__/Brokers.spec.tsx @@ -1,130 +1,32 @@ import React from 'react'; +import { render } from 'lib/testHelpers'; +import { screen } from '@testing-library/react'; +import { clusterBrokerPath } from 'lib/paths'; import Brokers from 'components/Brokers/Brokers'; -import { render, WithRoute } from 'lib/testHelpers'; -import { screen, waitFor } from '@testing-library/dom'; -import { clusterBrokersPath } from 'lib/paths'; -import fetchMock from 'fetch-mock'; -import { clusterStatsPayload } from 'redux/reducers/brokers/__test__/fixtures'; -import { act } from '@testing-library/react'; -describe('Brokers Component', () => { - afterEach(() => fetchMock.reset()); - - const clusterName = 'local'; - - const testInSyncReplicasCount = 798; - const testOutOfSyncReplicasCount = 1; - - const renderComponent = () => - render( - - - , - { - initialEntries: [clusterBrokersPath(clusterName)], - } - ); - - describe('Brokers', () => { - let fetchBrokersMock: fetchMock.FetchMockStatic; - const fetchStatsUrl = `/api/clusters/${clusterName}/stats`; - - beforeEach(() => { - fetchBrokersMock = fetchMock.getOnce( - `/api/clusters/${clusterName}/brokers`, - clusterStatsPayload - ); - }); - - it('renders', async () => { - const fetchStatsMock = fetchMock.getOnce( - fetchStatsUrl, - clusterStatsPayload - ); - await act(() => { - renderComponent(); - }); +const brokersList = 'brokersList'; +const broker = 'brokers'; - await waitFor(() => expect(fetchStatsMock.called()).toBeTruthy()); - await waitFor(() => expect(fetchBrokersMock.called()).toBeTruthy()); +jest.mock('components/Brokers/BrokersList/BrokersList', () => () => ( +
{brokersList}
+)); +jest.mock('components/Brokers/Broker/Broker', () => () =>
{broker}
); - expect(screen.getByRole('table')).toBeInTheDocument(); - const rows = screen.getAllByRole('row'); - expect(rows.length).toEqual(3); - }); - - it('shows warning when offlinePartitionCount > 0', async () => { - const fetchStatsMock = fetchMock.getOnce(fetchStatsUrl, { - ...clusterStatsPayload, - offlinePartitionCount: 1345, - }); - await act(() => { - renderComponent(); - }); - await waitFor(() => { - expect(fetchStatsMock.called()).toBeTruthy(); - }); - await waitFor(() => { - expect(fetchBrokersMock.called()).toBeTruthy(); - }); - const onlineWidget = screen.getByText( - clusterStatsPayload.onlinePartitionCount - ); - expect(onlineWidget).toBeInTheDocument(); - expect(onlineWidget).toHaveStyle({ color: '#E51A1A' }); +describe('Brokers Component', () => { + const clusterName = 'clusterName'; + const brokerId = '1'; + const renderComponent = (path?: string) => + render(, { + initialEntries: path ? [path] : undefined, }); - it('shows right count when offlinePartitionCount > 0', async () => { - const fetchStatsMock = fetchMock.getOnce(fetchStatsUrl, { - ...clusterStatsPayload, - inSyncReplicasCount: testInSyncReplicasCount, - outOfSyncReplicasCount: testOutOfSyncReplicasCount, - }); - await act(() => { - renderComponent(); - }); - await waitFor(() => { - expect(fetchStatsMock.called()).toBeTruthy(); - }); - const onlineWidgetDef = screen.getByText(testInSyncReplicasCount); - const onlineWidget = screen.getByText( - `of ${testInSyncReplicasCount + testOutOfSyncReplicasCount}` - ); - expect(onlineWidgetDef).toBeInTheDocument(); - expect(onlineWidget).toBeInTheDocument(); - }); - it('shows right count when inSyncReplicasCount: undefined outOfSyncReplicasCount: 1', async () => { - const fetchStatsMock = fetchMock.getOnce(fetchStatsUrl, { - ...clusterStatsPayload, - inSyncReplicasCount: undefined, - outOfSyncReplicasCount: testOutOfSyncReplicasCount, - }); - await act(() => { - renderComponent(); - }); - await waitFor(() => { - expect(fetchStatsMock.called()).toBeTruthy(); - }); + it('renders BrokersList', () => { + renderComponent(); + expect(screen.getByText(brokersList)).toBeInTheDocument(); + }); - const onlineWidget = screen.getByText(`of ${testOutOfSyncReplicasCount}`); - expect(onlineWidget).toBeInTheDocument(); - }); - it(`shows right count when inSyncReplicasCount: ${testInSyncReplicasCount} outOfSyncReplicasCount: undefined`, async () => { - const fetchStatsMock = fetchMock.getOnce(fetchStatsUrl, { - ...clusterStatsPayload, - inSyncReplicasCount: testInSyncReplicasCount, - outOfSyncReplicasCount: undefined, - }); - await act(() => { - renderComponent(); - }); - await waitFor(() => { - expect(fetchStatsMock.called()).toBeTruthy(); - }); - const onlineWidgetDef = screen.getByText(testInSyncReplicasCount); - const onlineWidget = screen.getByText(`of ${testInSyncReplicasCount}`); - expect(onlineWidgetDef).toBeInTheDocument(); - expect(onlineWidget).toBeInTheDocument(); - }); + it('renders Broker', () => { + renderComponent(clusterBrokerPath(clusterName, brokerId)); + expect(screen.getByText(broker)).toBeInTheDocument(); }); }); diff --git a/kafka-ui-react-app/src/components/Brokers/utils/__test__/fixtures.ts b/kafka-ui-react-app/src/components/Brokers/utils/__test__/fixtures.ts new file mode 100644 index 00000000000..310c9bc54a3 --- /dev/null +++ b/kafka-ui-react-app/src/components/Brokers/utils/__test__/fixtures.ts @@ -0,0 +1,25 @@ +import { BrokerMetrics } from 'generated-sources'; + +export const brokerMetricsPayload: BrokerMetrics = { + segmentSize: 23, + segmentCount: 23, + metrics: [ + { + name: 'TotalFetchRequestsPerSec', + labels: { + canonicalName: + 'kafka.server:name=TotalFetchRequestsPerSec,topic=_connect_status,type=BrokerTopicMetrics', + }, + value: 10, + }, + { + name: 'ZooKeeperRequestLatencyMs', + value: 11, + }, + { + name: 'RequestHandlerAvgIdlePercent', + }, + ], +}; +export const transformedBrokerMetricsPayload = + '{"segmentSize":23,"segmentCount":23,"metrics":[{"name":"TotalFetchRequestsPerSec","labels":{"canonicalName":"kafka.server:name=TotalFetchRequestsPerSec,topic=_connect_status,type=BrokerTopicMetrics"},"value":10},{"name":"ZooKeeperRequestLatencyMs","value":11},{"name":"RequestHandlerAvgIdlePercent"}]}'; diff --git a/kafka-ui-react-app/src/components/Brokers/utils/__test__/getEditorText.spec.tsx b/kafka-ui-react-app/src/components/Brokers/utils/__test__/getEditorText.spec.tsx new file mode 100644 index 00000000000..0ff11f8e20d --- /dev/null +++ b/kafka-ui-react-app/src/components/Brokers/utils/__test__/getEditorText.spec.tsx @@ -0,0 +1,17 @@ +import { getEditorText } from 'components/Brokers/utils/getEditorText'; + +import { + brokerMetricsPayload, + transformedBrokerMetricsPayload, +} from './fixtures'; + +describe('Get editor text', () => { + it('returns error message when broker metrics is not defined', () => { + expect(getEditorText(undefined)).toEqual('Metrics data not available'); + }); + it('returns transformed metrics text when broker logdirs metrics', () => { + expect(getEditorText(brokerMetricsPayload)).toEqual( + transformedBrokerMetricsPayload + ); + }); +}); diff --git a/kafka-ui-react-app/src/components/Brokers/utils/getEditorText.ts b/kafka-ui-react-app/src/components/Brokers/utils/getEditorText.ts new file mode 100644 index 00000000000..85c5d6fdb4f --- /dev/null +++ b/kafka-ui-react-app/src/components/Brokers/utils/getEditorText.ts @@ -0,0 +1,4 @@ +import { BrokerMetrics } from 'generated-sources'; + +export const getEditorText = (metrics: BrokerMetrics | undefined): string => + metrics ? JSON.stringify(metrics) : 'Metrics data not available'; diff --git a/kafka-ui-react-app/src/components/Cluster/Cluster.tsx b/kafka-ui-react-app/src/components/Cluster/Cluster.tsx deleted file mode 100644 index e60346ceae5..00000000000 --- a/kafka-ui-react-app/src/components/Cluster/Cluster.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import React from 'react'; -import { useSelector } from 'react-redux'; -import { Routes, Navigate, Route, Outlet } from 'react-router-dom'; -import useAppParams from 'lib/hooks/useAppParams'; -import { ClusterFeaturesEnum } from 'generated-sources'; -import { - getClustersFeatures, - getClustersReadonlyStatus, -} from 'redux/reducers/clusters/clustersSlice'; -import { - clusterBrokerRelativePath, - clusterConnectorsRelativePath, - clusterConnectsRelativePath, - clusterConsumerGroupsRelativePath, - clusterKsqlDbRelativePath, - ClusterNameRoute, - clusterSchemasRelativePath, - clusterTopicsRelativePath, - getNonExactPath, -} from 'lib/paths'; -import Topics from 'components/Topics/Topics'; -import Schemas from 'components/Schemas/Schemas'; -import Connect from 'components/Connect/Connect'; -import ClusterContext from 'components/contexts/ClusterContext'; -import Brokers from 'components/Brokers/Brokers'; -import ConsumersGroups from 'components/ConsumerGroups/ConsumerGroups'; -import KsqlDb from 'components/KsqlDb/KsqlDb'; -import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; -import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route'; -import { BreadcrumbProvider } from 'components/common/Breadcrumb/Breadcrumb.provider'; - -const Cluster: React.FC = () => { - const { clusterName } = useAppParams(); - const isReadOnly = useSelector(getClustersReadonlyStatus(clusterName)); - const features = useSelector(getClustersFeatures(clusterName)); - - const hasKafkaConnectConfigured = features.includes( - ClusterFeaturesEnum.KAFKA_CONNECT - ); - const hasSchemaRegistryConfigured = features.includes( - ClusterFeaturesEnum.SCHEMA_REGISTRY - ); - const isTopicDeletionAllowed = features.includes( - ClusterFeaturesEnum.TOPIC_DELETION - ); - const hasKsqlDbConfigured = features.includes(ClusterFeaturesEnum.KSQL_DB); - - const contextValue = React.useMemo( - () => ({ - isReadOnly, - hasKafkaConnectConfigured, - hasSchemaRegistryConfigured, - isTopicDeletionAllowed, - }), - [ - hasKafkaConnectConfigured, - hasSchemaRegistryConfigured, - isReadOnly, - isTopicDeletionAllowed, - ] - ); - - return ( - - - - - - - - } - /> - - - - } - /> - - - - } - /> - {hasSchemaRegistryConfigured && ( - - - - } - /> - )} - {hasKafkaConnectConfigured && ( - - - - } - /> - )} - {hasKafkaConnectConfigured && ( - - - - } - /> - )} - {hasKsqlDbConfigured && ( - - - - } - /> - )} - } - /> - - - - - ); -}; - -export default Cluster; diff --git a/kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx b/kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx deleted file mode 100644 index 2ef3a0de958..00000000000 --- a/kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import React from 'react'; -import { ClusterFeaturesEnum } from 'generated-sources'; -import { store } from 'redux/store'; -import { onlineClusterPayload } from 'redux/reducers/clusters/__test__/fixtures'; -import Cluster from 'components/Cluster/Cluster'; -import { fetchClusters } from 'redux/reducers/clusters/clustersSlice'; -import { screen } from '@testing-library/react'; -import { render, WithRoute } from 'lib/testHelpers'; -import { - clusterBrokersPath, - clusterConnectsPath, - clusterConsumerGroupsPath, - clusterKsqlDbPath, - clusterPath, - clusterSchemasPath, - clusterTopicsPath, -} from 'lib/paths'; - -const CLusterCompText = { - Topics: 'Topics', - Schemas: 'Schemas', - Connect: 'Connect', - Brokers: 'Brokers', - ConsumerGroups: 'ConsumerGroups', - KsqlDb: 'KsqlDb', -}; - -jest.mock('components/Topics/Topics', () => () => ( -
{CLusterCompText.Topics}
-)); -jest.mock('components/Schemas/Schemas', () => () => ( -
{CLusterCompText.Schemas}
-)); -jest.mock('components/Connect/Connect', () => () => ( -
{CLusterCompText.Connect}
-)); -jest.mock('components/Brokers/Brokers', () => () => ( -
{CLusterCompText.Brokers}
-)); -jest.mock('components/ConsumerGroups/ConsumerGroups', () => () => ( -
{CLusterCompText.ConsumerGroups}
-)); -jest.mock('components/KsqlDb/KsqlDb', () => () => ( -
{CLusterCompText.KsqlDb}
-)); - -describe('Cluster', () => { - const renderComponent = (pathname: string) => - render( - - - , - { initialEntries: [pathname], store } - ); - - it('renders Brokers', () => { - renderComponent(clusterBrokersPath('second')); - expect(screen.getByText(CLusterCompText.Brokers)).toBeInTheDocument(); - }); - it('renders Topics', () => { - renderComponent(clusterTopicsPath('second')); - expect(screen.getByText(CLusterCompText.Topics)).toBeInTheDocument(); - }); - it('renders ConsumerGroups', () => { - renderComponent(clusterConsumerGroupsPath('second')); - expect( - screen.getByText(CLusterCompText.ConsumerGroups) - ).toBeInTheDocument(); - }); - - describe('configured features', () => { - it('does not render Schemas if SCHEMA_REGISTRY is not configured', () => { - store.dispatch( - fetchClusters.fulfilled( - [ - { - ...onlineClusterPayload, - features: [], - }, - ], - '123' - ) - ); - renderComponent(clusterSchemasPath('second')); - expect( - screen.queryByText(CLusterCompText.Schemas) - ).not.toBeInTheDocument(); - }); - it('renders Schemas if SCHEMA_REGISTRY is configured', async () => { - store.dispatch( - fetchClusters.fulfilled( - [ - { - ...onlineClusterPayload, - features: [ClusterFeaturesEnum.SCHEMA_REGISTRY], - }, - ], - '123' - ) - ); - renderComponent(clusterSchemasPath(onlineClusterPayload.name)); - expect(screen.getByText(CLusterCompText.Schemas)).toBeInTheDocument(); - }); - it('renders Connect if KAFKA_CONNECT is configured', async () => { - store.dispatch( - fetchClusters.fulfilled( - [ - { - ...onlineClusterPayload, - features: [ClusterFeaturesEnum.KAFKA_CONNECT], - }, - ], - 'requestId' - ) - ); - renderComponent(clusterConnectsPath(onlineClusterPayload.name)); - expect(screen.getByText(CLusterCompText.Connect)).toBeInTheDocument(); - }); - it('renders KSQL if KSQL_DB is configured', async () => { - store.dispatch( - fetchClusters.fulfilled( - [ - { - ...onlineClusterPayload, - features: [ClusterFeaturesEnum.KSQL_DB], - }, - ], - 'requestId' - ) - ); - renderComponent(clusterKsqlDbPath(onlineClusterPayload.name)); - expect(screen.getByText(CLusterCompText.KsqlDb)).toBeInTheDocument(); - }); - }); -}); diff --git a/kafka-ui-react-app/src/components/ClusterPage/ClusterConfigPage.tsx b/kafka-ui-react-app/src/components/ClusterPage/ClusterConfigPage.tsx new file mode 100644 index 00000000000..b610114dd8c --- /dev/null +++ b/kafka-ui-react-app/src/components/ClusterPage/ClusterConfigPage.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { useAppConfig } from 'lib/hooks/api/appConfig'; +import useAppParams from 'lib/hooks/useAppParams'; +import { ClusterNameRoute } from 'lib/paths'; +import ClusterConfigForm from 'widgets/ClusterConfigForm'; +import { getInitialFormData } from 'widgets/ClusterConfigForm/utils/getInitialFormData'; + +const ClusterConfigPage: React.FC = () => { + const config = useAppConfig(); + const { clusterName } = useAppParams(); + + const currentClusterConfig = React.useMemo(() => { + if (config.isSuccess && !!config.data.properties?.kafka?.clusters) { + const current = config.data.properties?.kafka?.clusters?.find( + ({ name }) => name === clusterName + ); + if (current) { + return getInitialFormData(current); + } + } + return undefined; + }, [clusterName, config]); + + if (!currentClusterConfig) { + return null; + } + + const hasCustomConfig = Object.values(currentClusterConfig.customAuth).some( + (v) => !!v + ); + + return ( + + ); +}; + +export default ClusterConfigPage; diff --git a/kafka-ui-react-app/src/components/ClusterPage/ClusterPage.tsx b/kafka-ui-react-app/src/components/ClusterPage/ClusterPage.tsx new file mode 100644 index 00000000000..29d2015f612 --- /dev/null +++ b/kafka-ui-react-app/src/components/ClusterPage/ClusterPage.tsx @@ -0,0 +1,127 @@ +import React, { Suspense } from 'react'; +import { Routes, Navigate, Route, Outlet } from 'react-router-dom'; +import useAppParams from 'lib/hooks/useAppParams'; +import { ClusterFeaturesEnum } from 'generated-sources'; +import { + clusterBrokerRelativePath, + clusterConnectorsRelativePath, + clusterConnectsRelativePath, + clusterConsumerGroupsRelativePath, + clusterKsqlDbRelativePath, + ClusterNameRoute, + clusterSchemasRelativePath, + clusterTopicsRelativePath, + clusterConfigRelativePath, + getNonExactPath, + clusterAclRelativePath, +} from 'lib/paths'; +import ClusterContext from 'components/contexts/ClusterContext'; +import PageLoader from 'components/common/PageLoader/PageLoader'; +import { useClusters } from 'lib/hooks/api/clusters'; +import { GlobalSettingsContext } from 'components/contexts/GlobalSettingsContext'; + +const Brokers = React.lazy(() => import('components/Brokers/Brokers')); +const Topics = React.lazy(() => import('components/Topics/Topics')); +const Schemas = React.lazy(() => import('components/Schemas/Schemas')); +const Connect = React.lazy(() => import('components/Connect/Connect')); +const KsqlDb = React.lazy(() => import('components/KsqlDb/KsqlDb')); +const ClusterConfigPage = React.lazy( + () => import('components/ClusterPage/ClusterConfigPage') +); +const ConsumerGroups = React.lazy( + () => import('components/ConsumerGroups/ConsumerGroups') +); +const AclPage = React.lazy(() => import('components/ACLPage/ACLPage')); + +const ClusterPage: React.FC = () => { + const { clusterName } = useAppParams(); + const appInfo = React.useContext(GlobalSettingsContext); + + const { data } = useClusters(); + const contextValue = React.useMemo(() => { + const cluster = data?.find(({ name }) => name === clusterName); + const features = cluster?.features || []; + return { + isReadOnly: cluster?.readOnly || false, + hasKafkaConnectConfigured: features.includes( + ClusterFeaturesEnum.KAFKA_CONNECT + ), + hasSchemaRegistryConfigured: features.includes( + ClusterFeaturesEnum.SCHEMA_REGISTRY + ), + isTopicDeletionAllowed: features.includes( + ClusterFeaturesEnum.TOPIC_DELETION + ), + hasKsqlDbConfigured: features.includes(ClusterFeaturesEnum.KSQL_DB), + hasAclViewConfigured: + features.includes(ClusterFeaturesEnum.KAFKA_ACL_VIEW) || + features.includes(ClusterFeaturesEnum.KAFKA_ACL_EDIT), + }; + }, [clusterName, data]); + + return ( + }> + + }> + + } + /> + } + /> + } + /> + {contextValue.hasSchemaRegistryConfigured && ( + } + /> + )} + {contextValue.hasKafkaConnectConfigured && ( + } + /> + )} + {contextValue.hasKafkaConnectConfigured && ( + } + /> + )} + {contextValue.hasKsqlDbConfigured && ( + } + /> + )} + {contextValue.hasAclViewConfigured && ( + } + /> + )} + {appInfo.hasDynamicConfig && ( + } + /> + )} + } + /> + + + + + + ); +}; + +export default ClusterPage; diff --git a/kafka-ui-react-app/src/components/ClusterPage/__tests__/ClusterPage.spec.tsx b/kafka-ui-react-app/src/components/ClusterPage/__tests__/ClusterPage.spec.tsx new file mode 100644 index 00000000000..b66fd0a0b33 --- /dev/null +++ b/kafka-ui-react-app/src/components/ClusterPage/__tests__/ClusterPage.spec.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { Cluster, ClusterFeaturesEnum } from 'generated-sources'; +import ClusterPageComponent from 'components/ClusterPage/ClusterPage'; +import { screen, waitFor } from '@testing-library/react'; +import { render, WithRoute } from 'lib/testHelpers'; +import { + clusterBrokersPath, + clusterConnectorsPath, + clusterConnectsPath, + clusterConsumerGroupsPath, + clusterKsqlDbPath, + clusterPath, + clusterSchemasPath, + clusterTopicsPath, +} from 'lib/paths'; +import { useClusters } from 'lib/hooks/api/clusters'; +import { onlineClusterPayload } from 'lib/fixtures/clusters'; + +const CLusterCompText = { + Topics: 'Topics', + Schemas: 'Schemas', + Connect: 'Connect', + Brokers: 'Brokers', + ConsumerGroups: 'ConsumerGroups', + KsqlDb: 'KsqlDb', +}; + +jest.mock('components/Topics/Topics', () => () => ( +
{CLusterCompText.Topics}
+)); +jest.mock('components/Schemas/Schemas', () => () => ( +
{CLusterCompText.Schemas}
+)); +jest.mock('components/Connect/Connect', () => () => ( +
{CLusterCompText.Connect}
+)); +jest.mock('components/Brokers/Brokers', () => () => ( +
{CLusterCompText.Brokers}
+)); +jest.mock('components/ConsumerGroups/ConsumerGroups', () => () => ( +
{CLusterCompText.ConsumerGroups}
+)); +jest.mock('components/KsqlDb/KsqlDb', () => () => ( +
{CLusterCompText.KsqlDb}
+)); + +jest.mock('lib/hooks/api/clusters', () => ({ + useClusters: jest.fn(), +})); + +describe('ClusterPage', () => { + const renderComponent = async (pathname: string, payload: Cluster[] = []) => { + (useClusters as jest.Mock).mockImplementation(() => ({ + data: payload, + })); + await render( + + + , + { initialEntries: [pathname] } + ); + await waitFor(() => { + expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); + }); + }; + + it('renders Brokers', async () => { + await renderComponent(clusterBrokersPath('second')); + expect(screen.getByText(CLusterCompText.Brokers)).toBeInTheDocument(); + }); + it('renders Topics', async () => { + await renderComponent(clusterTopicsPath('second')); + expect(screen.getByText(CLusterCompText.Topics)).toBeInTheDocument(); + }); + it('renders ConsumerGroups', async () => { + await renderComponent(clusterConsumerGroupsPath('second')); + expect( + screen.getByText(CLusterCompText.ConsumerGroups) + ).toBeInTheDocument(); + }); + + describe('configured features', () => { + const itCorrectlyHandlesConfiguredSchema = ( + feature: ClusterFeaturesEnum, + text: string, + path: string + ) => { + it(`renders Schemas if ${feature} is configured`, async () => { + await renderComponent(path, [ + { + ...onlineClusterPayload, + features: [feature], + }, + ]); + expect(screen.getByText(text)).toBeInTheDocument(); + }); + + it(`does not render Schemas if ${feature} is not configured`, async () => { + await renderComponent(path, [ + { ...onlineClusterPayload, features: [] }, + ]); + expect(screen.queryByText(text)).not.toBeInTheDocument(); + }); + }; + + itCorrectlyHandlesConfiguredSchema( + ClusterFeaturesEnum.SCHEMA_REGISTRY, + CLusterCompText.Schemas, + clusterSchemasPath(onlineClusterPayload.name) + ); + itCorrectlyHandlesConfiguredSchema( + ClusterFeaturesEnum.KAFKA_CONNECT, + CLusterCompText.Connect, + clusterConnectsPath(onlineClusterPayload.name) + ); + itCorrectlyHandlesConfiguredSchema( + ClusterFeaturesEnum.KAFKA_CONNECT, + CLusterCompText.Connect, + clusterConnectorsPath(onlineClusterPayload.name) + ); + itCorrectlyHandlesConfiguredSchema( + ClusterFeaturesEnum.KSQL_DB, + CLusterCompText.KsqlDb, + clusterKsqlDbPath(onlineClusterPayload.name) + ); + }); +}); diff --git a/kafka-ui-react-app/src/components/Connect/Connect.tsx b/kafka-ui-react-app/src/components/Connect/Connect.tsx index f1bb7ef4066..095055cb615 100644 --- a/kafka-ui-react-app/src/components/Connect/Connect.tsx +++ b/kafka-ui-react-app/src/components/Connect/Connect.tsx @@ -2,62 +2,44 @@ import React from 'react'; import { Navigate, Routes, Route } from 'react-router-dom'; import { RouteParams, - clusterConnectConnectorEditRelativePath, clusterConnectConnectorRelativePath, clusterConnectConnectorsRelativePath, clusterConnectorNewRelativePath, getNonExactPath, + clusterConnectorsPath, } from 'lib/paths'; -import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route'; +import useAppParams from 'lib/hooks/useAppParams'; +import SuspenseQueryComponent from 'components/common/SuspenseQueryComponent/SuspenseQueryComponent'; -import ListContainer from './List/ListContainer'; -import NewContainer from './New/NewContainer'; -import DetailsContainer from './Details/DetailsContainer'; -import EditContainer from './Edit/EditContainer'; +import ListPage from './List/ListPage'; +import New from './New/New'; +import DetailsPage from './Details/DetailsPage'; -const Connect: React.FC = () => ( - - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - } - /> - } - /> - -); +const Connect: React.FC = () => { + const { clusterName } = useAppParams(); + + return ( + + } /> + } /> + + + + } + /> + } + /> + } + /> + + ); +}; export default Connect; diff --git a/kafka-ui-react-app/src/components/Connect/Details/Actions/Action.styled.ts b/kafka-ui-react-app/src/components/Connect/Details/Actions/Action.styled.ts new file mode 100644 index 00000000000..77165abd9ff --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/Details/Actions/Action.styled.ts @@ -0,0 +1,23 @@ +import styled from 'styled-components'; + +export const ConnectorActionsWrapperStyled = styled.div` + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +`; +export const ButtonLabel = styled.span` + margin-right: 11.5px; +`; +export const RestartButton = styled.div` + padding: 0 12px; + border: none; + border-radius: 4px; + display: flex; + -webkit-align-items: center; + background: ${({ theme }) => theme.button.primary.backgroundColor.normal}; + color: ${({ theme }) => theme.button.primary.color.normal}; + font-size: 14px; + font-weight: 500; + height: 32px; +`; diff --git a/kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx b/kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx index db26a27deee..cc3755e19b8 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx @@ -1,212 +1,151 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; +import { useIsMutating } from '@tanstack/react-query'; +import { + Action, + ConnectorAction, + ConnectorState, + ResourceType, +} from 'generated-sources'; import useAppParams from 'lib/hooks/useAppParams'; -import { ConnectorState, ConnectorAction } from 'generated-sources'; -import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces'; import { - clusterConnectConnectorEditPath, + useConnector, + useDeleteConnector, + useUpdateConnectorState, +} from 'lib/hooks/api/kafkaConnect'; +import { clusterConnectorsPath, RouterParamsClusterConnectConnector, } from 'lib/paths'; -import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; -import styled from 'styled-components'; -import { Button } from 'components/common/Button/Button'; - -const ConnectorActionsWrapperStyled = styled.div` - display: flex; - gap: 8px; -`; +import { useConfirm } from 'lib/hooks/useConfirm'; +import { Dropdown } from 'components/common/Dropdown'; +import { ActionDropdownItem } from 'components/common/ActionComponent'; +import ChevronDownIcon from 'components/common/Icons/ChevronDownIcon'; -export interface ActionsProps { - deleteConnector(payload: { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - }): Promise; - isConnectorDeleting: boolean; - connectorStatus?: ConnectorState; - restartConnector(payload: { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - }): void; - restartTasks(payload: { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - action: ConnectorAction; - }): void; - pauseConnector(payload: { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - }): void; - resumeConnector(payload: { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - }): void; - isConnectorActionRunning: boolean; -} - -const Actions: React.FC = ({ - deleteConnector, - isConnectorDeleting, - connectorStatus, - restartConnector, - restartTasks, - pauseConnector, - resumeConnector, - isConnectorActionRunning, -}) => { - const { clusterName, connectName, connectorName } = - useAppParams(); +import * as S from './Action.styled'; +const Actions: React.FC = () => { const navigate = useNavigate(); + const routerProps = useAppParams(); + const mutationsNumber = useIsMutating(); + const isMutating = mutationsNumber > 0; - const [ - isDeleteConnectorConfirmationVisible, - setIsDeleteConnectorConfirmationVisible, - ] = React.useState(false); - - const deleteConnectorHandler = async () => { - try { - await deleteConnector({ clusterName, connectName, connectorName }); - navigate(clusterConnectorsPath(clusterName)); - } catch { - // do not redirect - } - }; - - const restartConnectorHandler = () => { - restartConnector({ clusterName, connectName, connectorName }); - }; + const { data: connector } = useConnector(routerProps); + const confirm = useConfirm(); - const restartTasksHandler = (actionType: ConnectorAction) => { - restartTasks({ - clusterName, - connectName, - connectorName, - action: actionType, - }); - }; - - const pauseConnectorHandler = () => { - pauseConnector({ clusterName, connectName, connectorName }); - }; - - const resumeConnectorHandler = () => { - resumeConnector({ clusterName, connectName, connectorName }); - }; + const deleteConnectorMutation = useDeleteConnector(routerProps); + const deleteConnectorHandler = () => + confirm( + <> + Are you sure you want to remove {routerProps.connectorName}{' '} + connector? + , + async () => { + try { + await deleteConnectorMutation.mutateAsync(); + navigate(clusterConnectorsPath(routerProps.clusterName)); + } catch { + // do not redirect + } + } + ); + const stateMutation = useUpdateConnectorState(routerProps); + const restartConnectorHandler = () => + stateMutation.mutateAsync(ConnectorAction.RESTART); + const restartAllTasksHandler = () => + stateMutation.mutateAsync(ConnectorAction.RESTART_ALL_TASKS); + const restartFailedTasksHandler = () => + stateMutation.mutateAsync(ConnectorAction.RESTART_FAILED_TASKS); + const pauseConnectorHandler = () => + stateMutation.mutateAsync(ConnectorAction.PAUSE); + const resumeConnectorHandler = () => + stateMutation.mutateAsync(ConnectorAction.RESUME); return ( - - {connectorStatus === ConnectorState.RUNNING && ( - - )} - - {connectorStatus === ConnectorState.PAUSED && ( - - )} - - - - - - - - setIsDeleteConnectorConfirmationVisible(false)} - onConfirm={deleteConnectorHandler} - isConfirming={isConnectorDeleting} - > - Are you sure you want to remove {connectorName} connector? - - + {connector?.status.state === ConnectorState.PAUSED && ( + + Resume + + )} + + Restart Connector + + + Restart All Tasks + + + Restart Failed Tasks + + + + + Delete + + + ); }; diff --git a/kafka-ui-react-app/src/components/Connect/Details/Actions/ActionsContainer.ts b/kafka-ui-react-app/src/components/Connect/Details/Actions/ActionsContainer.ts deleted file mode 100644 index e0a78343ea3..00000000000 --- a/kafka-ui-react-app/src/components/Connect/Details/Actions/ActionsContainer.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { connect } from 'react-redux'; -import { RootState } from 'redux/interfaces'; -import { - deleteConnector, - restartConnector, - restartTasks, - pauseConnector, - resumeConnector, -} from 'redux/reducers/connect/connectSlice'; -import { - getIsConnectorDeleting, - getConnectorStatus, - getIsConnectorActionRunning, -} from 'redux/reducers/connect/selectors'; - -import Actions from './Actions'; - -const mapStateToProps = (state: RootState) => ({ - isConnectorDeleting: getIsConnectorDeleting(state), - connectorStatus: getConnectorStatus(state), - isConnectorActionRunning: getIsConnectorActionRunning(state), -}); - -const mapDispatchToProps = { - deleteConnector, - restartConnector, - restartTasks, - pauseConnector, - resumeConnector, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(Actions); diff --git a/kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx b/kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx index d11cabb40ac..9dce7507f59 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx @@ -1,16 +1,16 @@ import React from 'react'; import { render, WithRoute } from 'lib/testHelpers'; -import { clusterConnectConnectorPath, clusterConnectorsPath } from 'lib/paths'; -import ActionsContainer from 'components/Connect/Details/Actions/ActionsContainer'; -import Actions, { - ActionsProps, -} from 'components/Connect/Details/Actions/Actions'; -import { ConnectorState } from 'generated-sources'; -import { screen } from '@testing-library/react'; +import { clusterConnectConnectorPath } from 'lib/paths'; +import Actions from 'components/Connect/Details/Actions/Actions'; +import { ConnectorAction, ConnectorState } from 'generated-sources'; +import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import ConfirmationModal, { - ConfirmationModalProps, -} from 'components/common/ConfirmationModal/ConfirmationModal'; +import { + useConnector, + useUpdateConnectorState, +} from 'lib/hooks/api/kafkaConnect'; +import { connector } from 'lib/fixtures/kafkaConnect'; +import set from 'lodash/set'; const mockHistoryPush = jest.fn(); const deleteConnector = jest.fn(); @@ -21,19 +21,26 @@ jest.mock('react-router-dom', () => ({ useNavigate: () => mockHistoryPush, })); -jest.mock( - 'components/common/ConfirmationModal/ConfirmationModal', - () => 'mock-ConfirmationModal' -); +jest.mock('lib/hooks/api/kafkaConnect', () => ({ + useConnector: jest.fn(), + useDeleteConnector: jest.fn(), + useUpdateConnectorState: jest.fn(), +})); const expectActionButtonsExists = () => { expect(screen.getByText('Restart Connector')).toBeInTheDocument(); expect(screen.getByText('Restart All Tasks')).toBeInTheDocument(); expect(screen.getByText('Restart Failed Tasks')).toBeInTheDocument(); - expect(screen.getByText('Edit Config')).toBeInTheDocument(); expect(screen.getByText('Delete')).toBeInTheDocument(); }; - +const afterClickDropDownButton = async () => { + const dropDownButton = screen.getAllByRole('button'); + await userEvent.click(dropDownButton[1]); +}; +const afterClickRestartButton = async () => { + const dropDownButton = screen.getByText('Restart'); + await userEvent.click(dropDownButton); +}; describe('Actions', () => { afterEach(() => { mockHistoryPush.mockClear(); @@ -41,247 +48,152 @@ describe('Actions', () => { cancelMock.mockClear(); }); - const actionsContainer = (props: Partial = {}) => ( - - - - ); - - it('container renders view', () => { - const { container } = render(actionsContainer()); - expect(container).toBeInTheDocument(); - }); - describe('view', () => { - const pathname = clusterConnectConnectorPath(); - const clusterName = 'my-cluster'; - const connectName = 'my-connect'; - const connectorName = 'my-connector'; - - const confirmationModal = (props: Partial = {}) => ( - - - deleteConnector(clusterName, connectName, connectorName) - } - {...props} - > - - - - + const route = clusterConnectConnectorPath(); + const path = clusterConnectConnectorPath( + 'myCluster', + 'myConnect', + 'myConnector' ); - const component = (props: Partial = {}) => ( - - - - ); + const renderComponent = () => + render( + + + , + { initialEntries: [path] } + ); - it('renders buttons when paused', () => { - render(component({ connectorStatus: ConnectorState.PAUSED }), { - initialEntries: [ - clusterConnectConnectorPath(clusterName, connectName, connectorName), - ], - }); - expect(screen.getAllByRole('button').length).toEqual(6); + it('renders buttons when paused', async () => { + (useConnector as jest.Mock).mockImplementation(() => ({ + data: set({ ...connector }, 'status.state', ConnectorState.PAUSED), + })); + renderComponent(); + await afterClickRestartButton(); + expect(screen.getAllByRole('menuitem').length).toEqual(4); expect(screen.getByText('Resume')).toBeInTheDocument(); expect(screen.queryByText('Pause')).not.toBeInTheDocument(); - expectActionButtonsExists(); }); - it('renders buttons when failed', () => { - render(component({ connectorStatus: ConnectorState.FAILED }), { - initialEntries: [ - clusterConnectConnectorPath(clusterName, connectName, connectorName), - ], - }); - expect(screen.getAllByRole('button').length).toEqual(5); - + it('renders buttons when failed', async () => { + (useConnector as jest.Mock).mockImplementation(() => ({ + data: set({ ...connector }, 'status.state', ConnectorState.FAILED), + })); + renderComponent(); + await afterClickRestartButton(); + expect(screen.getAllByRole('menuitem').length).toEqual(3); expect(screen.queryByText('Resume')).not.toBeInTheDocument(); expect(screen.queryByText('Pause')).not.toBeInTheDocument(); - expectActionButtonsExists(); }); - it('renders buttons when unassigned', () => { - render(component({ connectorStatus: ConnectorState.UNASSIGNED }), { - initialEntries: [ - clusterConnectConnectorPath(clusterName, connectName, connectorName), - ], - }); - expect(screen.getAllByRole('button').length).toEqual(5); + it('renders buttons when unassigned', async () => { + (useConnector as jest.Mock).mockImplementation(() => ({ + data: set({ ...connector }, 'status.state', ConnectorState.UNASSIGNED), + })); + renderComponent(); + await afterClickRestartButton(); + expect(screen.getAllByRole('menuitem').length).toEqual(3); expect(screen.queryByText('Resume')).not.toBeInTheDocument(); expect(screen.queryByText('Pause')).not.toBeInTheDocument(); expectActionButtonsExists(); }); - it('renders buttons when running connector action', () => { - render(component({ connectorStatus: ConnectorState.RUNNING }), { - initialEntries: [ - clusterConnectConnectorPath(clusterName, connectName, connectorName), - ], - }); - expect(screen.getAllByRole('button').length).toEqual(6); + it('renders buttons when running connector action', async () => { + (useConnector as jest.Mock).mockImplementation(() => ({ + data: set({ ...connector }, 'status.state', ConnectorState.RUNNING), + })); + renderComponent(); + await afterClickRestartButton(); + expect(screen.getAllByRole('menuitem').length).toEqual(4); expect(screen.queryByText('Resume')).not.toBeInTheDocument(); expect(screen.getByText('Pause')).toBeInTheDocument(); - expectActionButtonsExists(); }); - it('opens confirmation modal when delete button clicked', () => { - render(component({ deleteConnector }), { - initialEntries: [ - clusterConnectConnectorPath(clusterName, connectName, connectorName), - ], + describe('mutations', () => { + beforeEach(() => { + (useConnector as jest.Mock).mockImplementation(() => ({ + data: set({ ...connector }, 'status.state', ConnectorState.RUNNING), + })); }); - userEvent.click(screen.getByRole('button', { name: 'Delete' })); - expect( - screen.getByText(/Are you sure you want to remove/i) - ).toHaveAttribute('isopen', 'true'); - }); - - it('closes when cancel button clicked', () => { - render(confirmationModal({ isOpen: true }), { - initialEntries: [ - clusterConnectConnectorPath(clusterName, connectName, connectorName), - ], + it('opens confirmation modal when delete button clicked', async () => { + renderComponent(); + await afterClickDropDownButton(); + await waitFor(async () => + userEvent.click(screen.getByRole('menuitem', { name: 'Delete' })) + ); + expect(screen.getByRole('dialog')).toBeInTheDocument(); }); - const cancelBtn = screen.getByRole('button', { name: 'Cancel' }); - userEvent.click(cancelBtn); - expect(cancelMock).toHaveBeenCalledTimes(1); - }); - it('calls deleteConnector when confirm button clicked', () => { - render(confirmationModal({ isOpen: true }), { - initialEntries: [ - clusterConnectConnectorPath(clusterName, connectName, connectorName), - ], + it('calls restartConnector when restart button clicked', async () => { + const restartConnector = jest.fn(); + (useUpdateConnectorState as jest.Mock).mockImplementation(() => ({ + mutateAsync: restartConnector, + })); + renderComponent(); + await afterClickRestartButton(); + await userEvent.click( + screen.getByRole('menuitem', { name: 'Restart Connector' }) + ); + expect(restartConnector).toHaveBeenCalledWith(ConnectorAction.RESTART); }); - const confirmBtn = screen.getByRole('button', { name: 'Confirm' }); - userEvent.click(confirmBtn); - expect(deleteConnector).toHaveBeenCalledTimes(1); - expect(deleteConnector).toHaveBeenCalledWith( - clusterName, - connectName, - connectorName - ); - }); - it('redirects after delete', async () => { - render(confirmationModal({ isOpen: true }), { - initialEntries: [ - clusterConnectConnectorPath(clusterName, connectName, connectorName), - ], + it('calls restartAllTasks', async () => { + const restartAllTasks = jest.fn(); + (useUpdateConnectorState as jest.Mock).mockImplementation(() => ({ + mutateAsync: restartAllTasks, + })); + renderComponent(); + await afterClickRestartButton(); + await userEvent.click( + screen.getByRole('menuitem', { name: 'Restart All Tasks' }) + ); + expect(restartAllTasks).toHaveBeenCalledWith( + ConnectorAction.RESTART_ALL_TASKS + ); }); - const confirmBtn = screen.getByRole('button', { name: 'Confirm' }); - userEvent.click(confirmBtn); - expect(mockHistoryPush).toHaveBeenCalledTimes(1); - expect(mockHistoryPush).toHaveBeenCalledWith( - clusterConnectorsPath(clusterName) - ); - }); - it('calls restartConnector when restart button clicked', () => { - const restartConnector = jest.fn(); - render(component({ restartConnector }), { - initialEntries: [ - clusterConnectConnectorPath(clusterName, connectName, connectorName), - ], + it('calls restartFailedTasks', async () => { + const restartFailedTasks = jest.fn(); + (useUpdateConnectorState as jest.Mock).mockImplementation(() => ({ + mutateAsync: restartFailedTasks, + })); + renderComponent(); + await afterClickRestartButton(); + await userEvent.click( + screen.getByRole('menuitem', { name: 'Restart Failed Tasks' }) + ); + expect(restartFailedTasks).toHaveBeenCalledWith( + ConnectorAction.RESTART_FAILED_TASKS + ); }); - userEvent.click( - screen.getByRole('button', { name: 'Restart Connector' }) - ); - expect(restartConnector).toHaveBeenCalledTimes(1); - expect(restartConnector).toHaveBeenCalledWith({ - clusterName, - connectName, - connectorName, - }); - }); - it('calls pauseConnector when pause button clicked', () => { - const pauseConnector = jest.fn(); - render( - component({ - connectorStatus: ConnectorState.RUNNING, - pauseConnector, - }), - { - initialEntries: [ - clusterConnectConnectorPath( - clusterName, - connectName, - connectorName - ), - ], - } - ); - userEvent.click(screen.getByRole('button', { name: 'Pause' })); - expect(pauseConnector).toHaveBeenCalledTimes(1); - expect(pauseConnector).toHaveBeenCalledWith({ - clusterName, - connectName, - connectorName, + it('calls pauseConnector when pause button clicked', async () => { + const pauseConnector = jest.fn(); + (useUpdateConnectorState as jest.Mock).mockImplementation(() => ({ + mutateAsync: pauseConnector, + })); + renderComponent(); + await afterClickRestartButton(); + await userEvent.click(screen.getByRole('menuitem', { name: 'Pause' })); + expect(pauseConnector).toHaveBeenCalledWith(ConnectorAction.PAUSE); }); - }); - it('calls resumeConnector when resume button clicked', () => { - const resumeConnector = jest.fn(); - render( - component({ - connectorStatus: ConnectorState.PAUSED, - resumeConnector, - }), - { - initialEntries: [ - clusterConnectConnectorPath( - clusterName, - connectName, - connectorName - ), - ], - } - ); - userEvent.click(screen.getByRole('button', { name: 'Resume' })); - expect(resumeConnector).toHaveBeenCalledTimes(1); - expect(resumeConnector).toHaveBeenCalledWith({ - clusterName, - connectName, - connectorName, + it('calls resumeConnector when resume button clicked', async () => { + const resumeConnector = jest.fn(); + (useConnector as jest.Mock).mockImplementation(() => ({ + data: set({ ...connector }, 'status.state', ConnectorState.PAUSED), + })); + (useUpdateConnectorState as jest.Mock).mockImplementation(() => ({ + mutateAsync: resumeConnector, + })); + renderComponent(); + await afterClickRestartButton(); + await userEvent.click(screen.getByRole('menuitem', { name: 'Resume' })); + expect(resumeConnector).toHaveBeenCalledWith(ConnectorAction.RESUME); }); }); }); diff --git a/kafka-ui-react-app/src/components/Connect/Edit/Edit.styled.ts b/kafka-ui-react-app/src/components/Connect/Details/Config/Config.styled.ts similarity index 100% rename from kafka-ui-react-app/src/components/Connect/Edit/Edit.styled.ts rename to kafka-ui-react-app/src/components/Connect/Details/Config/Config.styled.ts diff --git a/kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx b/kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx index 7c7d4a086e3..8a372e9d12b 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx @@ -1,56 +1,99 @@ import React from 'react'; import useAppParams from 'lib/hooks/useAppParams'; -import { - ClusterName, - ConnectName, - ConnectorConfig, - ConnectorName, -} from 'redux/interfaces'; -import PageLoader from 'components/common/PageLoader/PageLoader'; -import Editor from 'components/common/Editor/Editor'; -import styled from 'styled-components'; +import { Controller, useForm } from 'react-hook-form'; +import { ErrorMessage } from '@hookform/error-message'; +import { yupResolver } from '@hookform/resolvers/yup'; import { RouterParamsClusterConnectConnector } from 'lib/paths'; +import yup from 'lib/yupExtended'; +import Editor from 'components/common/Editor/Editor'; +import { Button } from 'components/common/Button/Button'; +import { + useConnectorConfig, + useUpdateConnectorConfig, +} from 'lib/hooks/api/kafkaConnect'; -export interface ConfigProps { - fetchConfig(payload: { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - }): void; - isConfigFetching: boolean; - config: ConnectorConfig | null; +import { + ConnectEditWarningMessageStyled, + ConnectEditWrapperStyled, +} from './Config.styled'; + +const validationSchema = yup.object().shape({ + config: yup.string().required().isJsonObject(), +}); + +interface FormValues { + config: string; } -const ConnectConfigWrapper = styled.div` - margin: 16px; -`; +const Config: React.FC = () => { + const routerParams = useAppParams(); + const { data: config } = useConnectorConfig(routerParams); + const mutation = useUpdateConnectorConfig(routerParams); -const Config: React.FC = ({ - fetchConfig, - isConfigFetching, - config, -}) => { - const { clusterName, connectName, connectorName } = - useAppParams(); + const { + handleSubmit, + control, + reset, + formState: { isDirty, isSubmitting, isValid, errors }, + setValue, + } = useForm({ + mode: 'onChange', + resolver: yupResolver(validationSchema), + defaultValues: { + config: JSON.stringify(config, null, '\t'), + }, + }); React.useEffect(() => { - fetchConfig({ clusterName, connectName, connectorName }); - }, [fetchConfig, clusterName, connectName, connectorName]); + if (config) { + setValue('config', JSON.stringify(config, null, '\t')); + } + }, [config, setValue]); - if (isConfigFetching) { - return ; - } + const onSubmit = async (values: FormValues) => { + try { + const requestBody = JSON.parse(values.config.trim()); + await mutation.mutateAsync(requestBody); + reset(values); + } catch (e) { + // do nothing + } + }; - if (!config) return null; + const hasCredentials = JSON.stringify(config, null, '\t').includes( + '"******"' + ); return ( - - - + + {hasCredentials && ( + + Please replace ****** with the real credential values to avoid + accidentally breaking your connector config! + + )} +
+
+ ( + + )} + /> +
+
+ +
+ + +
); }; diff --git a/kafka-ui-react-app/src/components/Connect/Details/Config/ConfigContainer.ts b/kafka-ui-react-app/src/components/Connect/Details/Config/ConfigContainer.ts deleted file mode 100644 index 10a28149c76..00000000000 --- a/kafka-ui-react-app/src/components/Connect/Details/Config/ConfigContainer.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { connect } from 'react-redux'; -import { RootState } from 'redux/interfaces'; -import { fetchConnectorConfig } from 'redux/reducers/connect/connectSlice'; -import { - getIsConnectorConfigFetching, - getConnectorConfig, -} from 'redux/reducers/connect/selectors'; - -import Config from './Config'; - -const mapStateToProps = (state: RootState) => ({ - isConfigFetching: getIsConnectorConfigFetching(state), - config: getConnectorConfig(state), -}); - -const mapDispatchToProps = { - fetchConfig: fetchConnectorConfig, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(Config); diff --git a/kafka-ui-react-app/src/components/Connect/Details/Config/__test__/Config.spec.tsx b/kafka-ui-react-app/src/components/Connect/Details/Config/__test__/Config.spec.tsx deleted file mode 100644 index 6f8fa909513..00000000000 --- a/kafka-ui-react-app/src/components/Connect/Details/Config/__test__/Config.spec.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import { render, WithRoute } from 'lib/testHelpers'; -import { clusterConnectConnectorConfigPath } from 'lib/paths'; -import Config, { ConfigProps } from 'components/Connect/Details/Config/Config'; -import { connector } from 'redux/reducers/connect/__test__/fixtures'; -import { screen } from '@testing-library/dom'; - -jest.mock('components/common/Editor/Editor', () => 'mock-Editor'); - -describe('Config', () => { - const pathname = clusterConnectConnectorConfigPath(); - const clusterName = 'my-cluster'; - const connectName = 'my-connect'; - const connectorName = 'my-connector'; - - const component = (props: Partial = {}) => ( - - - - ); - - it('to be in the document when fetching config', () => { - render(component({ isConfigFetching: true }), { - initialEntries: [ - clusterConnectConnectorConfigPath( - clusterName, - connectName, - connectorName - ), - ], - }); - expect(screen.getByRole('progressbar')).toBeInTheDocument(); - }); - - it('is empty when no config', () => { - const { container } = render(component({ config: null }), { - initialEntries: [ - clusterConnectConnectorConfigPath( - clusterName, - connectName, - connectorName - ), - ], - }); - expect(container).toBeEmptyDOMElement(); - }); - - it('fetches config on mount', () => { - const fetchConfig = jest.fn(); - render(component({ fetchConfig }), { - initialEntries: [ - clusterConnectConnectorConfigPath( - clusterName, - connectName, - connectorName - ), - ], - }); - expect(fetchConfig).toHaveBeenCalledTimes(1); - expect(fetchConfig).toHaveBeenCalledWith({ - clusterName, - connectName, - connectorName, - }); - }); -}); diff --git a/kafka-ui-react-app/src/components/Connect/Details/Config/__tests__/Config.spec.tsx b/kafka-ui-react-app/src/components/Connect/Details/Config/__tests__/Config.spec.tsx new file mode 100644 index 00000000000..16611072fca --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/Details/Config/__tests__/Config.spec.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { render, WithRoute } from 'lib/testHelpers'; +import { clusterConnectConnectorConfigPath } from 'lib/paths'; +import Config from 'components/Connect/Details/Config/Config'; +import { connector } from 'lib/fixtures/kafkaConnect'; +import { waitFor } from '@testing-library/dom'; +import { act, fireEvent, screen } from '@testing-library/react'; +import { + useConnectorConfig, + useUpdateConnectorConfig, +} from 'lib/hooks/api/kafkaConnect'; + +jest.mock('components/common/Editor/Editor', () => 'mock-Editor'); + +const mockHistoryPush = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockHistoryPush, +})); +jest.mock('lib/hooks/api/kafkaConnect', () => ({ + useConnectorConfig: jest.fn(), + useUpdateConnectorConfig: jest.fn(), +})); + +const [clusterName, connectName, connectorName] = [ + 'my-cluster', + 'my-connect', + 'my-connector', +]; + +describe('Config', () => { + const pathname = clusterConnectConnectorConfigPath(); + const renderComponent = () => + render( + + + , + { + initialEntries: [ + clusterConnectConnectorConfigPath( + clusterName, + connectName, + connectorName + ), + ], + } + ); + + beforeEach(() => { + (useConnectorConfig as jest.Mock).mockImplementation(() => ({ + data: connector.config, + })); + }); + + it('calls updateConfig and redirects to connector config view on successful submit', async () => { + const updateConfig = jest.fn(() => { + return Promise.resolve(connector); + }); + (useUpdateConnectorConfig as jest.Mock).mockImplementation(() => ({ + mutateAsync: updateConfig, + })); + + renderComponent(); + fireEvent.submit(screen.getByRole('form')); + await waitFor(() => expect(updateConfig).toHaveBeenCalledTimes(1)); + }); + + it('does not redirect to connector config view on unsuccessful submit', async () => { + const updateConfig = jest.fn(() => { + return Promise.resolve(); + }); + (useUpdateConnectorConfig as jest.Mock).mockImplementation(() => ({ + mutateAsync: updateConfig, + })); + renderComponent(); + await act(() => { + fireEvent.submit(screen.getByRole('form')); + }); + expect(mockHistoryPush).not.toHaveBeenCalled(); + }); +}); diff --git a/kafka-ui-react-app/src/components/Connect/Details/Details.tsx b/kafka-ui-react-app/src/components/Connect/Details/Details.tsx deleted file mode 100644 index f36fe3fedfd..00000000000 --- a/kafka-ui-react-app/src/components/Connect/Details/Details.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React from 'react'; -import { NavLink, Route, Routes } from 'react-router-dom'; -import useAppParams from 'lib/hooks/useAppParams'; -import { Connector, Task } from 'generated-sources'; -import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces'; -import { - clusterConnectConnectorConfigPath, - clusterConnectConnectorConfigRelativePath, - clusterConnectConnectorPath, - clusterConnectConnectorTasksPath, - clusterConnectConnectorTasksRelativePath, - RouterParamsClusterConnectConnector, -} from 'lib/paths'; -import PageLoader from 'components/common/PageLoader/PageLoader'; -import Navbar from 'components/common/Navigation/Navbar.styled'; -import PageHeading from 'components/common/PageHeading/PageHeading'; - -import OverviewContainer from './Overview/OverviewContainer'; -import TasksContainer from './Tasks/TasksContainer'; -import ConfigContainer from './Config/ConfigContainer'; -import ActionsContainer from './Actions/ActionsContainer'; - -export interface DetailsProps { - fetchConnector(payload: { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - }): void; - fetchTasks(payload: { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - }): void; - isConnectorFetching: boolean; - areTasksFetching: boolean; - connector: Connector | null; - tasks: Task[]; -} - -const Details: React.FC = ({ - fetchConnector, - fetchTasks, - isConnectorFetching, - areTasksFetching, - connector, -}) => { - const { clusterName, connectName, connectorName } = - useAppParams(); - - React.useEffect(() => { - fetchConnector({ clusterName, connectName, connectorName }); - }, [fetchConnector, clusterName, connectName, connectorName]); - - React.useEffect(() => { - fetchTasks({ clusterName, connectName, connectorName }); - }, [fetchTasks, clusterName, connectName, connectorName]); - - if (isConnectorFetching || areTasksFetching) { - return ; - } - - if (!connector) return null; - - return ( -
- - - - - (isActive ? 'is-active' : '')} - > - Overview - - (isActive ? 'is-active' : '')} - > - Tasks - - (isActive ? 'is-active' : '')} - > - Config - - - - } /> - } - /> - } - /> - -
- ); -}; - -export default Details; diff --git a/kafka-ui-react-app/src/components/Connect/Details/DetailsContainer.ts b/kafka-ui-react-app/src/components/Connect/Details/DetailsContainer.ts deleted file mode 100644 index 9f6d792d760..00000000000 --- a/kafka-ui-react-app/src/components/Connect/Details/DetailsContainer.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { connect } from 'react-redux'; -import { RootState } from 'redux/interfaces'; -import { - fetchConnector, - fetchConnectorTasks, -} from 'redux/reducers/connect/connectSlice'; -import { - getIsConnectorFetching, - getAreConnectorTasksFetching, - getConnector, - getConnectorTasks, -} from 'redux/reducers/connect/selectors'; - -import Details from './Details'; - -const mapStateToProps = (state: RootState) => ({ - isConnectorFetching: getIsConnectorFetching(state), - connector: getConnector(state), - areTasksFetching: getAreConnectorTasksFetching(state), - tasks: getConnectorTasks(state), -}); - -const mapDispatchToProps = { - fetchConnector, - fetchTasks: fetchConnectorTasks, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(Details); diff --git a/kafka-ui-react-app/src/components/Connect/Details/DetailsPage.tsx b/kafka-ui-react-app/src/components/Connect/Details/DetailsPage.tsx new file mode 100644 index 00000000000..a5175ea8bfa --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/Details/DetailsPage.tsx @@ -0,0 +1,70 @@ +import React, { Suspense } from 'react'; +import { NavLink, Route, Routes } from 'react-router-dom'; +import useAppParams from 'lib/hooks/useAppParams'; +import { + clusterConnectConnectorConfigPath, + clusterConnectConnectorConfigRelativePath, + clusterConnectConnectorPath, + clusterConnectorsPath, + RouterParamsClusterConnectConnector, +} from 'lib/paths'; +import Navbar from 'components/common/Navigation/Navbar.styled'; +import PageHeading from 'components/common/PageHeading/PageHeading'; +import PageLoader from 'components/common/PageLoader/PageLoader'; + +import Overview from './Overview/Overview'; +import Tasks from './Tasks/Tasks'; +import Config from './Config/Config'; +import Actions from './Actions/Actions'; + +const DetailsPage: React.FC = () => { + const { clusterName, connectName, connectorName } = + useAppParams(); + + return ( +
+ + + + + + (isActive ? 'is-active' : '')} + end + > + Tasks + + (isActive ? 'is-active' : '')} + > + Config + + + }> + + } /> + } + /> + + +
+ ); +}; + +export default DetailsPage; diff --git a/kafka-ui-react-app/src/components/Connect/Details/Overview/Overview.tsx b/kafka-ui-react-app/src/components/Connect/Details/Overview/Overview.tsx index 32931c3ecc0..d1fdc56fb95 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Overview/Overview.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Overview/Overview.tsx @@ -1,21 +1,24 @@ import React from 'react'; -import { Connector } from 'generated-sources'; import * as C from 'components/common/Tag/Tag.styled'; import * as Metrics from 'components/common/Metrics'; import getTagColor from 'components/common/Tag/getTagColor'; +import { RouterParamsClusterConnectConnector } from 'lib/paths'; +import useAppParams from 'lib/hooks/useAppParams'; +import { useConnector, useConnectorTasks } from 'lib/hooks/api/kafkaConnect'; -export interface OverviewProps { - connector: Connector | null; - runningTasksCount: number; - failedTasksCount: number; -} +import getTaskMetrics from './getTaskMetrics'; -const Overview: React.FC = ({ - connector, - runningTasksCount, - failedTasksCount, -}) => { - if (!connector) return null; +const Overview: React.FC = () => { + const routerProps = useAppParams(); + + const { data: connector } = useConnector(routerProps); + const { data: tasks } = useConnectorTasks(routerProps); + + if (!connector) { + return null; + } + + const { running, failed } = getTaskMetrics(tasks); return ( @@ -32,19 +35,17 @@ const Overview: React.FC = ({ )} - + {connector.status.state} - - {runningTasksCount} - + {running} 0 ? 'error' : 'success'} + alertType={failed > 0 ? 'error' : 'success'} > - {failedTasksCount} + {failed} diff --git a/kafka-ui-react-app/src/components/Connect/Details/Overview/OverviewContainer.ts b/kafka-ui-react-app/src/components/Connect/Details/Overview/OverviewContainer.ts deleted file mode 100644 index 6bca8b22a6d..00000000000 --- a/kafka-ui-react-app/src/components/Connect/Details/Overview/OverviewContainer.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { connect } from 'react-redux'; -import { RootState } from 'redux/interfaces'; -import { - getConnector, - getConnectorRunningTasksCount, - getConnectorFailedTasksCount, -} from 'redux/reducers/connect/selectors'; - -import Overview from './Overview'; - -const mapStateToProps = (state: RootState) => ({ - connector: getConnector(state), - runningTasksCount: getConnectorRunningTasksCount(state), - failedTasksCount: getConnectorFailedTasksCount(state), -}); - -export default connect(mapStateToProps)(Overview); diff --git a/kafka-ui-react-app/src/components/Connect/Details/Overview/__tests__/Overview.spec.tsx b/kafka-ui-react-app/src/components/Connect/Details/Overview/__tests__/Overview.spec.tsx index f5b353e681f..2d4a01d14f1 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Overview/__tests__/Overview.spec.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Overview/__tests__/Overview.spec.tsx @@ -1,40 +1,57 @@ import React from 'react'; import Overview from 'components/Connect/Details/Overview/Overview'; -import { connector } from 'redux/reducers/connect/__test__/fixtures'; +import { connector, tasks } from 'lib/fixtures/kafkaConnect'; import { screen } from '@testing-library/react'; import { render } from 'lib/testHelpers'; +import { useConnector, useConnectorTasks } from 'lib/hooks/api/kafkaConnect'; + +jest.mock('lib/hooks/api/kafkaConnect', () => ({ + useConnector: jest.fn(), + useConnectorTasks: jest.fn(), +})); describe('Overview', () => { it('is empty when no connector', () => { - const { container } = render( - - ); - expect(container).toBeEmptyDOMElement(); + (useConnector as jest.Mock).mockImplementation(() => ({ + data: undefined, + })); + (useConnectorTasks as jest.Mock).mockImplementation(() => ({ + data: undefined, + })); + + render(); + expect(screen.queryByText('Worker')).not.toBeInTheDocument(); }); - it('renders metrics', () => { - const running = 234789237; - const failed = 373737; - render( - - ); - expect(screen.getByText('Worker')).toBeInTheDocument(); - expect( - screen.getByText(connector.status.workerId as string) - ).toBeInTheDocument(); - - expect(screen.getByText('Type')).toBeInTheDocument(); - expect( - screen.getByText(connector.config['connector.class'] as string) - ).toBeInTheDocument(); - - expect(screen.getByText('Tasks Running')).toBeInTheDocument(); - expect(screen.getByText(running)).toBeInTheDocument(); - expect(screen.getByText('Tasks Failed')).toBeInTheDocument(); - expect(screen.getByText(failed)).toBeInTheDocument(); + describe('when connector is loaded', () => { + beforeEach(() => { + (useConnector as jest.Mock).mockImplementation(() => ({ + data: connector, + })); + }); + beforeEach(() => { + (useConnectorTasks as jest.Mock).mockImplementation(() => ({ + data: tasks, + })); + }); + + it('renders metrics', () => { + render(); + + expect(screen.getByText('Worker')).toBeInTheDocument(); + expect( + screen.getByText(connector.status.workerId as string) + ).toBeInTheDocument(); + + expect(screen.getByText('Type')).toBeInTheDocument(); + expect( + screen.getByText(connector.config['connector.class'] as string) + ).toBeInTheDocument(); + + expect(screen.getByText('Tasks Running')).toBeInTheDocument(); + expect(screen.getByText(2)).toBeInTheDocument(); + expect(screen.getByText('Tasks Failed')).toBeInTheDocument(); + expect(screen.getByText(1)).toBeInTheDocument(); + }); }); }); diff --git a/kafka-ui-react-app/src/components/Connect/Details/Overview/__tests__/getTaskMetrics.spec.ts b/kafka-ui-react-app/src/components/Connect/Details/Overview/__tests__/getTaskMetrics.spec.ts new file mode 100644 index 00000000000..4b7984c4a10 --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/Details/Overview/__tests__/getTaskMetrics.spec.ts @@ -0,0 +1,19 @@ +import { tasks } from 'lib/fixtures/kafkaConnect'; +import getTaskMetrics from 'components/Connect/Details/Overview/getTaskMetrics'; + +describe('getTaskMetrics', () => { + it('should return the correct metrics when task list is undefined', () => { + const metrics = getTaskMetrics(); + expect(metrics).toEqual({ + running: 0, + failed: 0, + }); + }); + + it('should return the correct metrics', () => { + expect(getTaskMetrics(tasks)).toEqual({ + running: 2, + failed: 1, + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/Connect/Details/Overview/getTaskMetrics.ts b/kafka-ui-react-app/src/components/Connect/Details/Overview/getTaskMetrics.ts new file mode 100644 index 00000000000..b1607f58c1e --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/Details/Overview/getTaskMetrics.ts @@ -0,0 +1,23 @@ +import { ConnectorTaskStatus, Task } from 'generated-sources'; + +export default function getTaskMetrics(tasks?: Task[]) { + const initialMetrics = { + running: 0, + failed: 0, + }; + + if (!tasks) { + return initialMetrics; + } + + return tasks.reduce((acc, { status }) => { + const state = status?.state; + if (state === ConnectorTaskStatus.RUNNING) { + return { ...acc, running: acc.running + 1 }; + } + if (state === ConnectorTaskStatus.FAILED) { + return { ...acc, failed: acc.failed + 1 }; + } + return acc; + }, initialMetrics); +} diff --git a/kafka-ui-react-app/src/components/Connect/Details/Tasks/ActionsCellTasks.tsx b/kafka-ui-react-app/src/components/Connect/Details/Tasks/ActionsCellTasks.tsx new file mode 100644 index 00000000000..01e0b7b800f --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/Details/Tasks/ActionsCellTasks.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Action, ResourceType, Task } from 'generated-sources'; +import { CellContext } from '@tanstack/react-table'; +import useAppParams from 'lib/hooks/useAppParams'; +import { useRestartConnectorTask } from 'lib/hooks/api/kafkaConnect'; +import { Dropdown } from 'components/common/Dropdown'; +import { ActionDropdownItem } from 'components/common/ActionComponent'; +import { RouterParamsClusterConnectConnector } from 'lib/paths'; + +const ActionsCellTasks: React.FC> = ({ row }) => { + const { id } = row.original; + const routerProps = useAppParams(); + const restartMutation = useRestartConnectorTask(routerProps); + + const restartTaskHandler = (taskId?: number) => { + if (taskId === undefined) return; + restartMutation.mutateAsync(taskId); + }; + + return ( + + restartTaskHandler(id?.task)} + danger + confirm="Are you sure you want to restart the task?" + permission={{ + resource: ResourceType.CONNECT, + action: Action.RESTART, + value: routerProps.connectorName, + }} + > + Restart task + + + ); +}; + +export default ActionsCellTasks; diff --git a/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItem.tsx b/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItem.tsx deleted file mode 100644 index 91e2d97c675..00000000000 --- a/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItem.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import useAppParams from 'lib/hooks/useAppParams'; -import { Task, TaskId } from 'generated-sources'; -import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces'; -import Dropdown from 'components/common/Dropdown/Dropdown'; -import DropdownItem from 'components/common/Dropdown/DropdownItem'; -import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon'; -import * as C from 'components/common/Tag/Tag.styled'; -import getTagColor from 'components/common/Tag/getTagColor'; -import { RouterParamsClusterConnectConnector } from 'lib/paths'; - -export interface ListItemProps { - task: Task; - restartTask(payload: { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - taskId: TaskId['task']; - }): Promise; -} - -const ListItem: React.FC = ({ task, restartTask }) => { - const { clusterName, connectName, connectorName } = - useAppParams(); - - const restartTaskHandler = async () => { - await restartTask({ - clusterName, - connectName, - connectorName, - taskId: task.id?.task, - }); - }; - - return ( -
- - - - - - - ); -}; - -export default ListItem; diff --git a/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItemContainer.ts b/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItemContainer.ts deleted file mode 100644 index 9e170da61c0..00000000000 --- a/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItemContainer.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { connect } from 'react-redux'; -import { Task } from 'generated-sources'; -import { RootState } from 'redux/interfaces'; -import { restartConnectorTask } from 'redux/reducers/connect/connectSlice'; - -import ListItem from './ListItem'; - -interface OwnProps { - task: Task; -} - -const mapStateToProps = (_state: RootState, { task }: OwnProps) => ({ - task, -}); - -const mapDispatchToProps = { - restartTask: restartConnectorTask, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(ListItem); diff --git a/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/__tests__/ListItem.spec.tsx b/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/__tests__/ListItem.spec.tsx deleted file mode 100644 index 8796281cc6e..00000000000 --- a/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/__tests__/ListItem.spec.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { render, WithRoute } from 'lib/testHelpers'; -import { clusterConnectConnectorTasksPath } from 'lib/paths'; -import ListItem, { - ListItemProps, -} from 'components/Connect/Details/Tasks/ListItem/ListItem'; -import { tasks } from 'redux/reducers/connect/__test__/fixtures'; -import { screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -const pathname = clusterConnectConnectorTasksPath(); -const clusterName = 'my-cluster'; -const connectName = 'my-connect'; -const connectorName = 'my-connector'; -const restartTask = jest.fn(); -const task = tasks[0]; - -const renderComponent = (props: ListItemProps = { task, restartTask }) => { - return render( - -
{task.status?.id}{task.status?.workerId} - {task.status.state} - {task.status.trace || 'null'} -
- } right> - - Restart task - - -
-
- - - -
- , - { - initialEntries: [ - clusterConnectConnectorTasksPath( - clusterName, - connectName, - connectorName - ), - ], - } - ); -}; - -describe('ListItem', () => { - it('renders', () => { - renderComponent(); - expect(screen.getByRole('row')).toBeInTheDocument(); - expect( - screen.getByRole('cell', { name: task.status.id.toString() }) - ).toBeInTheDocument(); - expect( - screen.getByRole('cell', { name: task.status.workerId }) - ).toBeInTheDocument(); - expect( - screen.getByRole('cell', { name: task.status.state }) - ).toBeInTheDocument(); - expect(screen.getByRole('button')).toBeInTheDocument(); - expect(screen.getByRole('menu')).toBeInTheDocument(); - expect(screen.getByRole('menuitem')).toBeInTheDocument(); - }); - it('calls restartTask on button click', () => { - renderComponent(); - - expect(restartTask).not.toBeCalled(); - userEvent.click(screen.getByRole('button')); - userEvent.click(screen.getByRole('menuitem')); - expect(restartTask).toBeCalledTimes(1); - expect(restartTask).toHaveBeenCalledWith({ - clusterName, - connectName, - connectorName, - taskId: task.id?.task, - }); - }); -}); diff --git a/kafka-ui-react-app/src/components/Connect/Details/Tasks/Tasks.tsx b/kafka-ui-react-app/src/components/Connect/Details/Tasks/Tasks.tsx index 0cd14ecd70b..bb21e895380 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Tasks/Tasks.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Tasks/Tasks.tsx @@ -1,43 +1,58 @@ import React from 'react'; +import { useConnectorTasks } from 'lib/hooks/api/kafkaConnect'; +import useAppParams from 'lib/hooks/useAppParams'; +import { RouterParamsClusterConnectConnector } from 'lib/paths'; +import { ColumnDef, Row } from '@tanstack/react-table'; import { Task } from 'generated-sources'; -import PageLoader from 'components/common/PageLoader/PageLoader'; -import { Table } from 'components/common/table/Table/Table.styled'; -import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; +import Table, { TagCell } from 'components/common/NewTable'; -import ListItemContainer from './ListItem/ListItemContainer'; +import ActionsCellTasks from './ActionsCellTasks'; -export interface TasksProps { - areTasksFetching: boolean; - tasks: Task[]; -} +const ExpandedTaskRow: React.FC<{ row: Row }> = ({ row }) => { + return
{row.original.status.trace}
; +}; + +const MAX_LENGTH = 100; -const Tasks: React.FC = ({ areTasksFetching, tasks }) => { - if (areTasksFetching) { - return ; - } +const Tasks: React.FC = () => { + const routerProps = useAppParams(); + const { data = [] } = useConnectorTasks(routerProps); + + const columns = React.useMemo[]>( + () => [ + { header: 'ID', accessorKey: 'status.id' }, + { header: 'Worker', accessorKey: 'status.workerId' }, + { header: 'State', accessorKey: 'status.state', cell: TagCell }, + { + header: 'Trace', + accessorKey: 'status.trace', + enableSorting: false, + cell: ({ getValue }) => { + const trace = getValue() || ''; + return trace.toString().length > MAX_LENGTH + ? `${trace.toString().substring(0, MAX_LENGTH - 3)}...` + : trace; + }, + meta: { width: '70%' }, + }, + { + id: 'actions', + header: '', + cell: ActionsCellTasks, + }, + ], + [] + ); return ( - - - - - - - - - - - - {tasks.length === 0 && ( - - - - )} - {tasks.map((task) => ( - - ))} - -
No tasks found
+ row.original.status.trace?.length > 0} + renderSubComponent={ExpandedTaskRow} + /> ); }; diff --git a/kafka-ui-react-app/src/components/Connect/Details/Tasks/TasksContainer.ts b/kafka-ui-react-app/src/components/Connect/Details/Tasks/TasksContainer.ts deleted file mode 100644 index 59162c43888..00000000000 --- a/kafka-ui-react-app/src/components/Connect/Details/Tasks/TasksContainer.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { connect } from 'react-redux'; -import { RootState } from 'redux/interfaces'; -import { fetchConnectorTasks } from 'redux/reducers/connect/connectSlice'; -import { - getAreConnectorTasksFetching, - getConnectorTasks, -} from 'redux/reducers/connect/selectors'; - -import Tasks from './Tasks'; - -const mapStateToProps = (state: RootState) => ({ - areTasksFetching: getAreConnectorTasksFetching(state), - tasks: getConnectorTasks(state), -}); - -const mapDispatchToProps = { - fetchTasks: fetchConnectorTasks, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(Tasks); diff --git a/kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx b/kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx index b3dfb82d293..dba12d4b0ea 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx @@ -1,59 +1,126 @@ import React from 'react'; import { render, WithRoute } from 'lib/testHelpers'; import { clusterConnectConnectorTasksPath } from 'lib/paths'; -import TasksContainer from 'components/Connect/Details/Tasks/TasksContainer'; -import Tasks, { TasksProps } from 'components/Connect/Details/Tasks/Tasks'; -import { tasks } from 'redux/reducers/connect/__test__/fixtures'; -import { screen } from '@testing-library/dom'; +import Tasks from 'components/Connect/Details/Tasks/Tasks'; +import { tasks } from 'lib/fixtures/kafkaConnect'; +import { screen, within, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { + useConnectorTasks, + useRestartConnectorTask, +} from 'lib/hooks/api/kafkaConnect'; +import { Task } from 'generated-sources'; -jest.mock( - 'components/Connect/Details/Tasks/ListItem/ListItemContainer', - () => 'tr' -); +jest.mock('lib/hooks/api/kafkaConnect', () => ({ + useConnectorTasks: jest.fn(), + useRestartConnectorTask: jest.fn(), +})); + +const path = clusterConnectConnectorTasksPath('local', 'ghp', '1'); + +const restartConnectorMock = jest.fn(); describe('Tasks', () => { - it('container renders view', () => { - render(); - expect(screen.getByRole('table')).toBeInTheDocument(); + beforeEach(() => { + (useRestartConnectorTask as jest.Mock).mockImplementation(() => ({ + mutateAsync: restartConnectorMock, + })); }); - describe('view', () => { - const clusterName = 'my-cluster'; - const connectName = 'my-connect'; - const connectorName = 'my-connector'; + const renderComponent = (currentData: Task[] | undefined = undefined) => { + (useConnectorTasks as jest.Mock).mockImplementation(() => ({ + data: currentData, + })); - const setupWrapper = (props: Partial = {}) => ( + render( - - + + , + { initialEntries: [path] } ); + }; - it('to be in the document when fetching tasks', () => { - render(setupWrapper({ areTasksFetching: true }), { - initialEntries: [ - clusterConnectConnectorTasksPath( - clusterName, - connectName, - connectorName - ), - ], - }); - expect(screen.getByRole('progressbar')).toBeInTheDocument(); - expect(screen.queryByRole('table')).not.toBeInTheDocument(); + it('renders empty table', () => { + renderComponent(); + expect(screen.getByRole('table')).toBeInTheDocument(); + expect(screen.getByText('No tasks found')).toBeInTheDocument(); + }); + + it('renders tasks table', () => { + renderComponent(tasks); + expect(screen.getAllByRole('row').length).toEqual(tasks.length + 1); + + expect( + screen.getByRole('row', { + name: '1 kafka-connect0:8083 RUNNING', + }) + ).toBeInTheDocument(); + }); + + it('renders truncates long trace and expands', async () => { + renderComponent(tasks); + + const trace = tasks[2]?.status?.trace || ''; + const truncatedTrace = trace.toString().substring(0, 100 - 3); + + const thirdRow = screen.getByRole('row', { + name: `3 kafka-connect0:8083 RUNNING ${truncatedTrace}...`, }); + expect(thirdRow).toBeInTheDocument(); - it('to be in the document when no tasks', () => { - render(setupWrapper({ tasks: [] }), { - initialEntries: [ - clusterConnectConnectorTasksPath( - clusterName, - connectName, - connectorName - ), - ], + const expandedDetails = screen.queryByText(trace); + // Full trace is not visible + expect(expandedDetails).not.toBeInTheDocument(); + + await userEvent.click(thirdRow); + + expect( + screen.getByRole('row', { + name: trace, + }) + ).toBeInTheDocument(); + }); + + describe('Action button', () => { + const expectDropdownExists = async () => { + const firstTaskRow = screen.getByRole('row', { + name: '1 kafka-connect0:8083 RUNNING', + }); + expect(firstTaskRow).toBeInTheDocument(); + const extBtn = within(firstTaskRow).getByRole('button', { + name: 'Dropdown Toggle', }); - expect(screen.getByRole('table')).toBeInTheDocument(); - expect(screen.getByText('No tasks found')).toBeInTheDocument(); + expect(extBtn).toBeEnabled(); + await userEvent.click(extBtn); + expect(screen.getByRole('menu')).toBeInTheDocument(); + }; + + it('renders action button', async () => { + renderComponent(tasks); + await expectDropdownExists(); + expect( + screen.getAllByRole('button', { name: 'Dropdown Toggle' }).length + ).toEqual(tasks.length); + // Action buttons are enabled + const actionBtn = screen.getAllByRole('menuitem'); + expect(actionBtn[0]).toHaveTextContent('Restart task'); + }); + + it('works as expected', async () => { + renderComponent(tasks); + await expectDropdownExists(); + const actionBtn = screen.getAllByRole('menuitem'); + expect(actionBtn[0]).toHaveTextContent('Restart task'); + + await userEvent.click(actionBtn[0]); + expect( + screen.getByText('Are you sure you want to restart the task?') + ).toBeInTheDocument(); + + expect(screen.getByText('Confirm the action')).toBeInTheDocument(); + userEvent.click(screen.getByRole('button', { name: 'Confirm' })); + + await waitFor(() => expect(restartConnectorMock).toHaveBeenCalled()); }); }); }); diff --git a/kafka-ui-react-app/src/components/Connect/Details/__tests__/Details.spec.tsx b/kafka-ui-react-app/src/components/Connect/Details/__tests__/Details.spec.tsx deleted file mode 100644 index ff4d9618a2f..00000000000 --- a/kafka-ui-react-app/src/components/Connect/Details/__tests__/Details.spec.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import React from 'react'; -import { render, WithRoute } from 'lib/testHelpers'; -import { - clusterConnectConnectorConfigPath, - clusterConnectConnectorPath, - clusterConnectConnectorTasksPath, - getNonExactPath, -} from 'lib/paths'; -import Details, { DetailsProps } from 'components/Connect/Details/Details'; -import { connector, tasks } from 'redux/reducers/connect/__test__/fixtures'; -import { screen } from '@testing-library/dom'; - -const DetailsCompText = { - overview: 'OverviewContainer', - tasks: 'TasksContainer', - config: 'ConfigContainer', - actions: 'ActionsContainer', -}; - -jest.mock('components/Connect/Details/Overview/OverviewContainer', () => () => ( -
{DetailsCompText.overview}
-)); - -jest.mock('components/Connect/Details/Tasks/TasksContainer', () => () => ( -
{DetailsCompText.tasks}
-)); - -jest.mock('components/Connect/Details/Config/ConfigContainer', () => () => ( -
{DetailsCompText.config}
-)); - -jest.mock('components/Connect/Details/Actions/ActionsContainer', () => () => ( -
{DetailsCompText.actions}
-)); - -describe('Details', () => { - const clusterName = 'my-cluster'; - const connectName = 'my-connect'; - const connectorName = 'my-connector'; - const defaultPath = clusterConnectConnectorPath( - clusterName, - connectName, - connectorName - ); - - const setupWrapper = ( - props: Partial = {}, - path: string = defaultPath - ) => - render( - -
- , - { initialEntries: [path] } - ); - - it('renders progressbar when fetching connector', () => { - setupWrapper({ isConnectorFetching: true }); - - expect(screen.getByRole('progressbar')).toBeInTheDocument(); - expect(screen.queryByRole('navigation')).not.toBeInTheDocument(); - }); - - it('renders progressbar when fetching tasks', () => { - setupWrapper({ areTasksFetching: true }); - - expect(screen.getByRole('progressbar')).toBeInTheDocument(); - expect(screen.queryByRole('navigation')).not.toBeInTheDocument(); - }); - - it('is empty when no connector', () => { - const { container } = setupWrapper({ connector: null }); - expect(container).toBeEmptyDOMElement(); - }); - - it('fetches connector on mount', () => { - const fetchConnector = jest.fn(); - setupWrapper({ fetchConnector }); - expect(fetchConnector).toHaveBeenCalledTimes(1); - expect(fetchConnector).toHaveBeenCalledWith({ - clusterName, - connectName, - connectorName, - }); - }); - - it('fetches tasks on mount', () => { - const fetchTasks = jest.fn(); - setupWrapper({ fetchTasks }); - expect(fetchTasks).toHaveBeenCalledTimes(1); - expect(fetchTasks).toHaveBeenCalledWith({ - clusterName, - connectName, - connectorName, - }); - }); - - describe('Router component tests', () => { - it('should test if overview is rendering', () => { - setupWrapper({}); - expect(screen.getByText(DetailsCompText.overview)); - }); - - it('should test if tasks is rendering', () => { - setupWrapper( - {}, - clusterConnectConnectorTasksPath( - clusterName, - connectName, - connectorName - ) - ); - expect(screen.getByText(DetailsCompText.tasks)); - }); - - it('should test if list is rendering', () => { - setupWrapper( - {}, - clusterConnectConnectorConfigPath( - clusterName, - connectName, - connectorName - ) - ); - expect(screen.getByText(DetailsCompText.config)); - }); - }); -}); diff --git a/kafka-ui-react-app/src/components/Connect/Details/__tests__/DetailsPage.spec.tsx b/kafka-ui-react-app/src/components/Connect/Details/__tests__/DetailsPage.spec.tsx new file mode 100644 index 00000000000..46aa1037bef --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/Details/__tests__/DetailsPage.spec.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { render, WithRoute } from 'lib/testHelpers'; +import { + clusterConnectConnectorConfigPath, + clusterConnectConnectorPath, + getNonExactPath, +} from 'lib/paths'; +import { screen } from '@testing-library/dom'; +import DetailsPage from 'components/Connect/Details/DetailsPage'; + +const DetailsCompText = { + overview: 'Overview Pane', + tasks: 'Tasks Page', + config: 'Config Page', + actions: 'Actions', +}; + +jest.mock('components/Connect/Details/Overview/Overview', () => () => ( +
{DetailsCompText.overview}
+)); + +jest.mock('components/Connect/Details/Tasks/Tasks', () => () => ( +
{DetailsCompText.tasks}
+)); + +jest.mock('components/Connect/Details/Config/Config', () => () => ( +
{DetailsCompText.config}
+)); + +jest.mock('components/Connect/Details/Actions/Actions', () => () => ( +
{DetailsCompText.actions}
+)); + +describe('Details Page', () => { + const clusterName = 'my-cluster'; + const connectName = 'my-connect'; + const connectorName = 'my-connector'; + const defaultPath = clusterConnectConnectorPath( + clusterName, + connectName, + connectorName + ); + + const renderComponent = (path: string = defaultPath) => + render( + + + , + { initialEntries: [path] } + ); + + it('renders actions', () => { + renderComponent(); + expect(screen.getByText(DetailsCompText.actions)); + }); + + it('renders overview pane', () => { + renderComponent(); + expect(screen.getByText(DetailsCompText.overview)); + }); + + describe('Router component tests', () => { + it('should test if tasks is rendering', () => { + renderComponent(); + expect(screen.getByText(DetailsCompText.tasks)); + }); + + it('should test if list is rendering', () => { + const path = clusterConnectConnectorConfigPath( + clusterName, + connectName, + connectorName + ); + renderComponent(path); + expect(screen.getByText(DetailsCompText.config)); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/Connect/Edit/Edit.tsx b/kafka-ui-react-app/src/components/Connect/Edit/Edit.tsx deleted file mode 100644 index c2b86ed3b22..00000000000 --- a/kafka-ui-react-app/src/components/Connect/Edit/Edit.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import React from 'react'; -import { useNavigate } from 'react-router-dom'; -import useAppParams from 'lib/hooks/useAppParams'; -import { Controller, useForm } from 'react-hook-form'; -import { ErrorMessage } from '@hookform/error-message'; -import { yupResolver } from '@hookform/resolvers/yup'; -import { - ClusterName, - ConnectName, - ConnectorConfig, - ConnectorName, -} from 'redux/interfaces'; -import { - clusterConnectConnectorConfigPath, - RouterParamsClusterConnectConnector, -} from 'lib/paths'; -import yup from 'lib/yupExtended'; -import Editor from 'components/common/Editor/Editor'; -import PageLoader from 'components/common/PageLoader/PageLoader'; -import { Button } from 'components/common/Button/Button'; - -import { - ConnectEditWarningMessageStyled, - ConnectEditWrapperStyled, -} from './Edit.styled'; - -const validationSchema = yup.object().shape({ - config: yup.string().required().isJsonObject(), -}); - -interface FormValues { - config: string; -} - -export interface EditProps { - fetchConfig(payload: { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - }): Promise; - isConfigFetching: boolean; - config: ConnectorConfig | null; - updateConfig(payload: { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - connectorConfig: ConnectorConfig; - }): Promise; -} - -const Edit: React.FC = ({ - fetchConfig, - isConfigFetching, - config, - updateConfig, -}) => { - const { clusterName, connectName, connectorName } = - useAppParams(); - const navigate = useNavigate(); - const { - handleSubmit, - control, - formState: { isDirty, isSubmitting, isValid, errors }, - setValue, - } = useForm({ - mode: 'onTouched', - resolver: yupResolver(validationSchema), - defaultValues: { - config: JSON.stringify(config, null, '\t'), - }, - }); - - React.useEffect(() => { - fetchConfig({ clusterName, connectName, connectorName }); - }, [fetchConfig, clusterName, connectName, connectorName]); - - React.useEffect(() => { - if (config) { - setValue('config', JSON.stringify(config, null, '\t')); - } - }, [config, setValue]); - - const onSubmit = async (values: FormValues) => { - const connector = await updateConfig({ - clusterName, - connectName, - connectorName, - connectorConfig: JSON.parse(values.config.trim()), - }); - if (connector) { - navigate( - clusterConnectConnectorConfigPath( - clusterName, - connectName, - connectorName - ) - ); - } - }; - - if (isConfigFetching) return ; - - const hasCredentials = JSON.stringify(config, null, '\t').includes( - '"******"' - ); - return ( - - {hasCredentials && ( - - Please replace ****** with the real credential values to avoid - accidentally breaking your connector config! - - )} -
-
- ( - - )} - /> -
-
- -
- - -
- ); -}; - -export default Edit; diff --git a/kafka-ui-react-app/src/components/Connect/Edit/EditContainer.ts b/kafka-ui-react-app/src/components/Connect/Edit/EditContainer.ts deleted file mode 100644 index 5c3dd2d3232..00000000000 --- a/kafka-ui-react-app/src/components/Connect/Edit/EditContainer.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { connect } from 'react-redux'; -import { RootState } from 'redux/interfaces'; -import { - fetchConnectorConfig, - updateConnectorConfig, -} from 'redux/reducers/connect/connectSlice'; -import { - getConnectorConfig, - getIsConnectorConfigFetching, -} from 'redux/reducers/connect/selectors'; - -import Edit from './Edit'; - -const mapStateToProps = (state: RootState) => ({ - isConfigFetching: getIsConnectorConfigFetching(state), - config: getConnectorConfig(state), -}); - -const mapDispatchToProps = { - fetchConfig: fetchConnectorConfig, - updateConfig: updateConnectorConfig, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(Edit); diff --git a/kafka-ui-react-app/src/components/Connect/Edit/__tests__/Edit.spec.tsx b/kafka-ui-react-app/src/components/Connect/Edit/__tests__/Edit.spec.tsx deleted file mode 100644 index c17637c7920..00000000000 --- a/kafka-ui-react-app/src/components/Connect/Edit/__tests__/Edit.spec.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import React from 'react'; -import { render, WithRoute } from 'lib/testHelpers'; -import { - clusterConnectConnectorConfigPath, - clusterConnectConnectorEditPath, -} from 'lib/paths'; -import Edit, { EditProps } from 'components/Connect/Edit/Edit'; -import { connector } from 'redux/reducers/connect/__test__/fixtures'; -import { waitFor } from '@testing-library/dom'; -import { act, fireEvent, screen } from '@testing-library/react'; - -jest.mock('components/common/PageLoader/PageLoader', () => 'mock-PageLoader'); - -jest.mock('components/common/Editor/Editor', () => 'mock-Editor'); - -const mockHistoryPush = jest.fn(); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useNavigate: () => mockHistoryPush, -})); - -describe('Edit', () => { - const pathname = clusterConnectConnectorEditPath(); - const clusterName = 'my-cluster'; - const connectName = 'my-connect'; - const connectorName = 'my-connector'; - - const renderComponent = (props: Partial = {}) => - render( - - - , - { - initialEntries: [ - clusterConnectConnectorEditPath( - clusterName, - connectName, - connectorName - ), - ], - } - ); - - it('fetches config on mount', async () => { - const fetchConfig = jest.fn(); - await waitFor(() => renderComponent({ fetchConfig })); - expect(fetchConfig).toHaveBeenCalledTimes(1); - expect(fetchConfig).toHaveBeenCalledWith({ - clusterName, - connectName, - connectorName, - }); - }); - - it('calls updateConfig on form submit', async () => { - const updateConfig = jest.fn(); - await waitFor(() => renderComponent({ updateConfig })); - fireEvent.submit(screen.getByRole('form')); - await waitFor(() => expect(updateConfig).toHaveBeenCalledTimes(1)); - expect(updateConfig).toHaveBeenCalledWith({ - clusterName, - connectName, - connectorName, - connectorConfig: connector.config, - }); - }); - - it('redirects to connector config view on successful submit', async () => { - const updateConfig = jest.fn().mockResolvedValueOnce(connector); - await waitFor(() => renderComponent({ updateConfig })); - fireEvent.submit(screen.getByRole('form')); - - await waitFor(() => expect(mockHistoryPush).toHaveBeenCalledTimes(1)); - expect(mockHistoryPush).toHaveBeenCalledWith( - clusterConnectConnectorConfigPath(clusterName, connectName, connectorName) - ); - }); - - it('does not redirect to connector config view on unsuccessful submit', async () => { - const updateConfig = jest.fn().mockResolvedValueOnce(undefined); - await waitFor(() => renderComponent({ updateConfig })); - await act(() => { - fireEvent.submit(screen.getByRole('form')); - }); - expect(mockHistoryPush).not.toHaveBeenCalled(); - }); -}); diff --git a/kafka-ui-react-app/src/components/Connect/List/ActionsCell.tsx b/kafka-ui-react-app/src/components/Connect/List/ActionsCell.tsx new file mode 100644 index 00000000000..246ad332c65 --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/List/ActionsCell.tsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { + Action, + ConnectorAction, + ConnectorState, + FullConnectorInfo, + ResourceType, +} from 'generated-sources'; +import { CellContext } from '@tanstack/react-table'; +import { ClusterNameRoute } from 'lib/paths'; +import useAppParams from 'lib/hooks/useAppParams'; +import { Dropdown, DropdownItem } from 'components/common/Dropdown'; +import { + useDeleteConnector, + useUpdateConnectorState, +} from 'lib/hooks/api/kafkaConnect'; +import { useConfirm } from 'lib/hooks/useConfirm'; +import { useIsMutating } from '@tanstack/react-query'; +import { ActionDropdownItem } from 'components/common/ActionComponent'; + +const ActionsCell: React.FC> = ({ + row, +}) => { + const { connect, name, status } = row.original; + const { clusterName } = useAppParams(); + const mutationsNumber = useIsMutating(); + const isMutating = mutationsNumber > 0; + const confirm = useConfirm(); + const deleteMutation = useDeleteConnector({ + clusterName, + connectName: connect, + connectorName: name, + }); + const stateMutation = useUpdateConnectorState({ + clusterName, + connectName: connect, + connectorName: name, + }); + const handleDelete = () => { + confirm( + <> + Are you sure want to remove {name} connector? + , + async () => { + await deleteMutation.mutateAsync(); + } + ); + }; + // const stateMutation = useUpdateConnectorState(routerProps); + const resumeConnectorHandler = () => + stateMutation.mutateAsync(ConnectorAction.RESUME); + const restartConnectorHandler = () => + stateMutation.mutateAsync(ConnectorAction.RESTART); + + const restartAllTasksHandler = () => + stateMutation.mutateAsync(ConnectorAction.RESTART_ALL_TASKS); + + const restartFailedTasksHandler = () => + stateMutation.mutateAsync(ConnectorAction.RESTART_FAILED_TASKS); + + return ( + + {status.state === ConnectorState.PAUSED && ( + + Resume + + )} + + Restart Connector + + + Restart All Tasks + + + Restart Failed Tasks + + + Remove Connector + + + ); +}; + +export default ActionsCell; diff --git a/kafka-ui-react-app/src/components/Connect/List/List.styled.ts b/kafka-ui-react-app/src/components/Connect/List/List.styled.ts index f0e2631d2c1..799915fcb11 100644 --- a/kafka-ui-react-app/src/components/Connect/List/List.styled.ts +++ b/kafka-ui-react-app/src/components/Connect/List/List.styled.ts @@ -3,4 +3,10 @@ import styled from 'styled-components'; export const TagsWrapper = styled.div` display: flex; flex-wrap: wrap; + span { + color: rgb(76, 76, 255) !important; + &:hover { + color: rgb(23, 23, 207) !important; + } + } `; diff --git a/kafka-ui-react-app/src/components/Connect/List/List.tsx b/kafka-ui-react-app/src/components/Connect/List/List.tsx index 0ea1f4e6c07..b5935e7bab2 100644 --- a/kafka-ui-react-app/src/components/Connect/List/List.tsx +++ b/kafka-ui-react-app/src/components/Connect/List/List.tsx @@ -1,140 +1,49 @@ import React from 'react'; import useAppParams from 'lib/hooks/useAppParams'; -import { Connect, FullConnectorInfo } from 'generated-sources'; -import { ClusterName, ConnectorSearch } from 'redux/interfaces'; -import { clusterConnectorNewRelativePath, ClusterNameRoute } from 'lib/paths'; -import ClusterContext from 'components/contexts/ClusterContext'; -import PageLoader from 'components/common/PageLoader/PageLoader'; -import Search from 'components/common/Search/Search'; -import * as Metrics from 'components/common/Metrics'; -import PageHeading from 'components/common/PageHeading/PageHeading'; -import { Button } from 'components/common/Button/Button'; -import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled'; -import { Table } from 'components/common/table/Table/Table.styled'; -import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; +import { clusterConnectConnectorPath, ClusterNameRoute } from 'lib/paths'; +import Table, { TagCell } from 'components/common/NewTable'; +import { FullConnectorInfo } from 'generated-sources'; +import { useConnectors } from 'lib/hooks/api/kafkaConnect'; +import { ColumnDef } from '@tanstack/react-table'; +import { useNavigate, useSearchParams } from 'react-router-dom'; -import ListItem from './ListItem'; +import ActionsCell from './ActionsCell'; +import TopicsCell from './TopicsCell'; +import RunningTasksCell from './RunningTasksCell'; -export interface ListProps { - areConnectsFetching: boolean; - areConnectorsFetching: boolean; - connectors: FullConnectorInfo[]; - connects: Connect[]; - failedConnectors: FullConnectorInfo[]; - failedTasks: number | undefined; - fetchConnects(clusterName: ClusterName): void; - fetchConnectors({ clusterName }: { clusterName: ClusterName }): void; - search: string; - setConnectorSearch(value: ConnectorSearch): void; -} - -const List: React.FC = ({ - connectors, - areConnectsFetching, - areConnectorsFetching, - failedConnectors, - failedTasks, - fetchConnects, - fetchConnectors, - search, - setConnectorSearch, -}) => { - const { isReadOnly } = React.useContext(ClusterContext); +const List: React.FC = () => { + const navigate = useNavigate(); const { clusterName } = useAppParams(); + const [searchParams] = useSearchParams(); + const { data: connectors } = useConnectors( + clusterName, + searchParams.get('q') || '' + ); - React.useEffect(() => { - fetchConnects(clusterName); - fetchConnectors({ clusterName }); - }, [fetchConnects, fetchConnectors, clusterName]); - - const handleSearch = (value: string) => - setConnectorSearch({ - clusterName, - search: value, - }); + const columns = React.useMemo[]>( + () => [ + { header: 'Name', accessorKey: 'name' }, + { header: 'Connect', accessorKey: 'connect' }, + { header: 'Type', accessorKey: 'type' }, + { header: 'Plugin', accessorKey: 'connectorClass' }, + { header: 'Topics', cell: TopicsCell }, + { header: 'Status', accessorKey: 'status.state', cell: TagCell }, + { header: 'Running Tasks', cell: RunningTasksCell }, + { header: '', id: 'action', cell: ActionsCell }, + ], + [] + ); return ( - <> - - {!isReadOnly && ( - - )} - - - - - {connectors.length} - - - {failedConnectors?.length} - - - {failedTasks} - - - - - - - {areConnectorsFetching ? ( - - ) : ( -
-
- - - - - - - - - - - - - - {connectors.length === 0 && ( - - - - )} - {connectors.map((connector) => ( - - ))} - -
No connectors found
- - )} - + + navigate(clusterConnectConnectorPath(clusterName, connect, name)) + } + emptyMessage="No connectors found" + /> ); }; diff --git a/kafka-ui-react-app/src/components/Connect/List/ListContainer.ts b/kafka-ui-react-app/src/components/Connect/List/ListContainer.ts deleted file mode 100644 index dbd7a717716..00000000000 --- a/kafka-ui-react-app/src/components/Connect/List/ListContainer.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { connect } from 'react-redux'; -import { RootState } from 'redux/interfaces'; -import { - fetchConnects, - fetchConnectors, - setConnectorSearch, -} from 'redux/reducers/connect/connectSlice'; -import { - getConnects, - getConnectors, - getAreConnectsFetching, - getAreConnectorsFetching, - getConnectorSearch, - getFailedConnectors, - getSortedTopics, - getFailedTasks, -} from 'redux/reducers/connect/selectors'; -import List from 'components/Connect/List/List'; - -const mapStateToProps = (state: RootState) => ({ - areConnectsFetching: getAreConnectsFetching(state), - areConnectorsFetching: getAreConnectorsFetching(state), - connects: getConnects(state), - failedConnectors: getFailedConnectors(state), - sortedTopics: getSortedTopics(state), - failedTasks: getFailedTasks(state), - connectors: getConnectors(state), - search: getConnectorSearch(state), -}); - -const mapDispatchToProps = { - fetchConnects, - fetchConnectors, - setConnectorSearch, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(List); diff --git a/kafka-ui-react-app/src/components/Connect/List/ListItem.tsx b/kafka-ui-react-app/src/components/Connect/List/ListItem.tsx deleted file mode 100644 index 50f8242e23e..00000000000 --- a/kafka-ui-react-app/src/components/Connect/List/ListItem.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React from 'react'; -import { FullConnectorInfo } from 'generated-sources'; -import { clusterConnectConnectorPath, clusterTopicPath } from 'lib/paths'; -import { ClusterName } from 'redux/interfaces'; -import { Link, NavLink } from 'react-router-dom'; -import { useDispatch } from 'react-redux'; -import { deleteConnector } from 'redux/reducers/connect/connectSlice'; -import Dropdown from 'components/common/Dropdown/Dropdown'; -import DropdownItem from 'components/common/Dropdown/DropdownItem'; -import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; -import { Tag } from 'components/common/Tag/Tag.styled'; -import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled'; -import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon'; -import getTagColor from 'components/common/Tag/getTagColor'; - -import * as S from './List.styled'; - -export interface ListItemProps { - clusterName: ClusterName; - connector: FullConnectorInfo; -} - -const ListItem: React.FC = ({ - clusterName, - connector: { - name, - connect, - type, - connectorClass, - topics, - status, - tasksCount, - failedTasksCount, - }, -}) => { - const dispatch = useDispatch(); - const [ - isDeleteConnectorConfirmationVisible, - setDeleteConnectorConfirmationVisible, - ] = React.useState(false); - - const handleDelete = () => { - if (clusterName && connect && name) { - dispatch( - deleteConnector({ - clusterName, - connectName: connect, - connectorName: name, - }) - ); - } - setDeleteConnectorConfirmationVisible(false); - }; - - const runningTasks = React.useMemo(() => { - if (!tasksCount) return null; - return tasksCount - (failedTasksCount || 0); - }, [tasksCount, failedTasksCount]); - - return ( - - - - {name} - - - - - - - - - - - ); -}; - -export default ListItem; diff --git a/kafka-ui-react-app/src/components/Connect/List/ListPage.tsx b/kafka-ui-react-app/src/components/Connect/List/ListPage.tsx new file mode 100644 index 00000000000..94ec8354c93 --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/List/ListPage.tsx @@ -0,0 +1,84 @@ +import React, { Suspense } from 'react'; +import useAppParams from 'lib/hooks/useAppParams'; +import { clusterConnectorNewRelativePath, ClusterNameRoute } from 'lib/paths'; +import ClusterContext from 'components/contexts/ClusterContext'; +import Search from 'components/common/Search/Search'; +import * as Metrics from 'components/common/Metrics'; +import PageHeading from 'components/common/PageHeading/PageHeading'; +import { ActionButton } from 'components/common/ActionComponent'; +import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled'; +import PageLoader from 'components/common/PageLoader/PageLoader'; +import { Action, ConnectorState, ResourceType } from 'generated-sources'; +import { useConnectors } from 'lib/hooks/api/kafkaConnect'; + +import List from './List'; + +const ListPage: React.FC = () => { + const { isReadOnly } = React.useContext(ClusterContext); + const { clusterName } = useAppParams(); + + // Fetches all connectors from the API, without search criteria. Used to display general metrics. + const { data: connectorsMetrics, isLoading } = useConnectors(clusterName); + + const numberOfFailedConnectors = connectorsMetrics?.filter( + ({ status: { state } }) => state === ConnectorState.FAILED + ).length; + + const numberOfFailedTasks = connectorsMetrics?.reduce( + (acc, metric) => acc + (metric.failedTasksCount ?? 0), + 0 + ); + + return ( + <> + + {!isReadOnly && ( + + Create Connector + + )} + + + + + {connectorsMetrics?.length || '-'} + + + {numberOfFailedConnectors ?? '-'} + + + {numberOfFailedTasks ?? '-'} + + + + + + + }> + + + + ); +}; + +export default ListPage; diff --git a/kafka-ui-react-app/src/components/Connect/List/RunningTasksCell.tsx b/kafka-ui-react-app/src/components/Connect/List/RunningTasksCell.tsx new file mode 100644 index 00000000000..4c3293d44c9 --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/List/RunningTasksCell.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { FullConnectorInfo } from 'generated-sources'; +import { CellContext } from '@tanstack/react-table'; + +const RunningTasksCell: React.FC> = ({ + row, +}) => { + const { tasksCount, failedTasksCount } = row.original; + + if (!tasksCount) { + return null; + } + + return ( + <> + {tasksCount - (failedTasksCount || 0)} of {tasksCount} + + ); +}; + +export default RunningTasksCell; diff --git a/kafka-ui-react-app/src/components/Connect/List/TopicsCell.tsx b/kafka-ui-react-app/src/components/Connect/List/TopicsCell.tsx new file mode 100644 index 00000000000..f5f634b7f2d --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/List/TopicsCell.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { FullConnectorInfo } from 'generated-sources'; +import { CellContext } from '@tanstack/react-table'; +import { useNavigate } from 'react-router-dom'; +import { Tag } from 'components/common/Tag/Tag.styled'; +import { ClusterNameRoute, clusterTopicPath } from 'lib/paths'; +import useAppParams from 'lib/hooks/useAppParams'; + +import * as S from './List.styled'; + +const TopicsCell: React.FC> = ({ + row, +}) => { + const { topics } = row.original; + const { clusterName } = useAppParams(); + const navigate = useNavigate(); + + const navigateToTopic = ( + e: React.KeyboardEvent | React.MouseEvent, + topic: string + ) => { + e.preventDefault(); + e.stopPropagation(); + navigate(clusterTopicPath(clusterName, topic)); + }; + + return ( + + {topics?.map((t) => ( + + navigateToTopic(e, t)} + onKeyDown={(e) => navigateToTopic(e, t)} + tabIndex={0} + > + {t} + + + ))} + + ); +}; + +export default TopicsCell; diff --git a/kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx b/kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx index bb8fb3cfa67..82b4aab2126 100644 --- a/kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx +++ b/kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx @@ -1,101 +1,131 @@ import React from 'react'; -import { - connectors, - failedConnectors, -} from 'redux/reducers/connect/__test__/fixtures'; +import { connectors } from 'lib/fixtures/kafkaConnect'; import ClusterContext, { ContextProps, initialValue, } from 'components/contexts/ClusterContext'; -import ListContainer from 'components/Connect/List/ListContainer'; -import List, { ListProps } from 'components/Connect/List/List'; -import { act, screen } from '@testing-library/react'; -import { render } from 'lib/testHelpers'; +import List from 'components/Connect/List/List'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { render, WithRoute } from 'lib/testHelpers'; +import { clusterConnectConnectorPath, clusterConnectorsPath } from 'lib/paths'; +import { + useConnectors, + useDeleteConnector, + useUpdateConnectorState, +} from 'lib/hooks/api/kafkaConnect'; + +const mockedUsedNavigate = jest.fn(); +const mockDelete = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockedUsedNavigate, +})); + +jest.mock('lib/hooks/api/kafkaConnect', () => ({ + useConnectors: jest.fn(), + useDeleteConnector: jest.fn(), + useUpdateConnectorState: jest.fn(), +})); + +const clusterName = 'local'; + +const renderComponent = (contextValue: ContextProps = initialValue) => + render( + + + + + , + { initialEntries: [clusterConnectorsPath(clusterName)] } + ); describe('Connectors List', () => { - describe('Container', () => { - it('renders view with initial state of storage', async () => { - await act(() => { - render(); - }); - expect(screen.getByRole('heading')).toHaveTextContent('Connectors'); + describe('when the connectors are loaded', () => { + beforeEach(() => { + (useConnectors as jest.Mock).mockImplementation(() => ({ + data: connectors, + })); + const restartConnector = jest.fn(); + (useUpdateConnectorState as jest.Mock).mockImplementation(() => ({ + mutateAsync: restartConnector, + })); }); - }); - describe('View', () => { - const fetchConnects = jest.fn(); - const fetchConnectors = jest.fn(); - const setConnectorSearch = jest.fn(); - const renderComponent = ( - props: Partial = {}, - contextValue: ContextProps = initialValue - ) => { - render( - - - - ); - }; + it('renders', async () => { + renderComponent(); + expect(screen.getByRole('table')).toBeInTheDocument(); + expect(screen.getAllByRole('row').length).toEqual(3); + }); - it('renders PageLoader', async () => { - await act(() => renderComponent({ areConnectorsFetching: true })); - expect(screen.getByRole('progressbar')).toBeInTheDocument(); - expect(screen.queryByRole('row')).not.toBeInTheDocument(); + it('opens broker when row clicked', async () => { + renderComponent(); + await userEvent.click( + screen.getByRole('row', { + name: 'hdfs-source-connector first SOURCE FileStreamSource a b c RUNNING 2 of 2', + }) + ); + await waitFor(() => + expect(mockedUsedNavigate).toBeCalledWith( + clusterConnectConnectorPath( + clusterName, + 'first', + 'hdfs-source-connector' + ) + ) + ); }); + }); - it('renders table', () => { - renderComponent({ areConnectorsFetching: false }); - expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); - expect(screen.getByRole('table')).toBeInTheDocument(); + describe('when table is empty', () => { + beforeEach(() => { + (useConnectors as jest.Mock).mockImplementation(() => ({ + data: [], + })); }); - it('renders connectors list', () => { - renderComponent({ - areConnectorsFetching: false, - connectors, - }); - expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); + it('renders empty table', async () => { + renderComponent(); expect(screen.getByRole('table')).toBeInTheDocument(); - expect(screen.getAllByRole('row').length).toEqual(3); + expect( + screen.getByRole('row', { name: 'No connectors found' }) + ).toBeInTheDocument(); }); + }); - it('renders failed connectors list', () => { - renderComponent({ - areConnectorsFetching: false, - failedConnectors, - }); - expect(screen.queryByRole('PageLoader')).not.toBeInTheDocument(); - expect(screen.getByTitle('Failed Connectors')).toBeInTheDocument(); + describe('when remove connector modal is open', () => { + beforeEach(() => { + (useConnectors as jest.Mock).mockImplementation(() => ({ + data: connectors, + })); + (useDeleteConnector as jest.Mock).mockImplementation(() => ({ + mutateAsync: mockDelete, + })); }); - it('handles fetchConnects and fetchConnectors', () => { + it('calls removeConnector on confirm', async () => { renderComponent(); - expect(fetchConnects).toHaveBeenCalledTimes(1); - expect(fetchConnectors).toHaveBeenCalledTimes(1); - }); + const removeButton = screen.getAllByText('Remove Connector')[0]; + await waitFor(() => userEvent.click(removeButton)); - it('renders actions if cluster is not readonly', () => { - renderComponent({}, { ...initialValue, isReadOnly: false }); - expect(screen.getByRole('button')).toBeInTheDocument(); + const submitButton = screen.getAllByRole('button', { + name: 'Confirm', + })[0]; + await userEvent.click(submitButton); + expect(mockDelete).toHaveBeenCalledWith(); }); - describe('readonly cluster', () => { - it('does not render actions if cluster is readonly', () => { - renderComponent({}, { ...initialValue, isReadOnly: true }); - expect(screen.queryByRole('button')).not.toBeInTheDocument(); - }); + it('closes the modal when cancel button is clicked', async () => { + renderComponent(); + const removeButton = screen.getAllByText('Remove Connector')[0]; + await waitFor(() => userEvent.click(removeButton)); + + const cancelButton = screen.getAllByRole('button', { + name: 'Cancel', + })[0]; + await waitFor(() => userEvent.click(cancelButton)); + expect(cancelButton).not.toBeInTheDocument(); }); }); }); diff --git a/kafka-ui-react-app/src/components/Connect/List/__tests__/ListItem.spec.tsx b/kafka-ui-react-app/src/components/Connect/List/__tests__/ListItem.spec.tsx deleted file mode 100644 index 6ecc8a5f186..00000000000 --- a/kafka-ui-react-app/src/components/Connect/List/__tests__/ListItem.spec.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React from 'react'; -import { connectors } from 'redux/reducers/connect/__test__/fixtures'; -import ListItem, { ListItemProps } from 'components/Connect/List/ListItem'; -import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; -import { screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { render } from 'lib/testHelpers'; - -const mockDeleteConnector = jest.fn(() => ({ type: 'test' })); - -jest.mock('redux/reducers/connect/connectSlice', () => ({ - ...jest.requireActual('redux/reducers/connect/connectSlice'), - deleteConnector: () => mockDeleteConnector, -})); - -jest.mock( - 'components/common/ConfirmationModal/ConfirmationModal', - () => 'mock-ConfirmationModal' -); - -describe('Connectors ListItem', () => { - const connector = connectors[0]; - const setupWrapper = (props: Partial = {}) => ( -
{connect}{type}{connectorClass} - - {topics?.map((t) => ( - - {t} - - ))} - - {status && {status.state}} - {runningTasks && ( - - {runningTasks} of {tasksCount} - - )} - -
- } right up> - setDeleteConnectorConfirmationVisible(true)} - danger - > - Remove Connector - - -
- setDeleteConnectorConfirmationVisible(false)} - onConfirm={handleDelete} - > - Are you sure want to remove {name} connector? - -
- - - -
- ); - - const onCancel = jest.fn(); - const onConfirm = jest.fn(); - const confirmationModal = (props: Partial = {}) => ( - - - {props.clusterName ? ( - - ) : ( - - )} - - ); - - it('renders item', () => { - render(setupWrapper()); - expect(screen.getAllByRole('cell')[6]).toHaveTextContent('2 of 2'); - }); - - it('topics tags are sorted', () => { - render(setupWrapper()); - const getLink = screen.getAllByRole('link'); - expect(getLink[1]).toHaveTextContent('a'); - expect(getLink[2]).toHaveTextContent('b'); - expect(getLink[3]).toHaveTextContent('c'); - }); - - it('renders item with failed tasks', () => { - render( - setupWrapper({ - connector: { - ...connector, - failedTasksCount: 1, - }, - }) - ); - expect(screen.getAllByRole('cell')[6]).toHaveTextContent('1 of 2'); - }); - - it('does not render info about tasks if taksCount is undefined', () => { - render( - setupWrapper({ - connector: { - ...connector, - tasksCount: undefined, - }, - }) - ); - expect(screen.getAllByRole('cell')[6]).toHaveTextContent(''); - }); - - it('handles cancel', async () => { - render(confirmationModal()); - userEvent.click(screen.getByText('Cancel')); - expect(onCancel).toHaveBeenCalled(); - }); - - it('handles delete', () => { - render(confirmationModal({ clusterName: 'test' })); - userEvent.click(screen.getByText('Confirm')); - expect(onConfirm).toHaveBeenCalled(); - }); - - it('handles delete when clusterName is not present', () => { - render(confirmationModal({ clusterName: undefined })); - userEvent.click(screen.getByText('Confirm')); - expect(onConfirm).toHaveBeenCalledTimes(0); - }); -}); diff --git a/kafka-ui-react-app/src/components/Connect/List/__tests__/ListPage.spec.tsx b/kafka-ui-react-app/src/components/Connect/List/__tests__/ListPage.spec.tsx new file mode 100644 index 00000000000..7da8e47e82a --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/List/__tests__/ListPage.spec.tsx @@ -0,0 +1,181 @@ +import React from 'react'; +import { connectors } from 'lib/fixtures/kafkaConnect'; +import ClusterContext, { + ContextProps, + initialValue, +} from 'components/contexts/ClusterContext'; +import ListPage from 'components/Connect/List/ListPage'; +import { screen, within } from '@testing-library/react'; +import { render, WithRoute } from 'lib/testHelpers'; +import { clusterConnectorsPath } from 'lib/paths'; +import { useConnectors } from 'lib/hooks/api/kafkaConnect'; + +jest.mock('components/Connect/List/List', () => () => ( +
Connectors List
+)); + +jest.mock('lib/hooks/api/kafkaConnect', () => ({ + useConnectors: jest.fn(), +})); + +jest.mock('components/common/Icons/SpinnerIcon', () => () => 'progressbar'); + +const clusterName = 'local'; + +describe('Connectors List Page', () => { + beforeEach(() => { + (useConnectors as jest.Mock).mockImplementation(() => ({ + isLoading: false, + data: [], + })); + }); + + const renderComponent = async (contextValue: ContextProps = initialValue) => + render( + + + + + , + { initialEntries: [clusterConnectorsPath(clusterName)] } + ); + + describe('Heading', () => { + it('renders header without create button for readonly cluster', async () => { + await renderComponent({ ...initialValue, isReadOnly: true }); + expect( + screen.getByRole('heading', { name: 'Connectors' }) + ).toBeInTheDocument(); + expect( + screen.queryByRole('link', { name: 'Create Connector' }) + ).not.toBeInTheDocument(); + }); + + it('renders header with create button for read/write cluster', async () => { + await renderComponent(); + expect( + screen.getByRole('heading', { name: 'Connectors' }) + ).toBeInTheDocument(); + expect( + screen.getByRole('link', { name: 'Create Connector' }) + ).toBeInTheDocument(); + }); + }); + + it('renders search input', async () => { + await renderComponent(); + expect( + screen.getByPlaceholderText('Search by Connect Name, Status or Type') + ).toBeInTheDocument(); + }); + + it('renders list', async () => { + await renderComponent(); + expect(screen.getByText('Connectors List')).toBeInTheDocument(); + }); + + describe('Metrics', () => { + it('renders indicators in loading state', async () => { + (useConnectors as jest.Mock).mockImplementation(() => ({ + isLoading: true, + data: connectors, + })); + + await renderComponent(); + const metrics = screen.getByRole('group'); + expect(metrics).toBeInTheDocument(); + expect(within(metrics).getAllByText('progressbar').length).toEqual(3); + }); + + it('renders indicators for empty list of connectors', async () => { + await renderComponent(); + const metrics = screen.getByRole('group'); + expect(metrics).toBeInTheDocument(); + + const connectorsIndicator = within(metrics).getByTitle( + 'Total number of connectors' + ); + expect(connectorsIndicator).toBeInTheDocument(); + expect(connectorsIndicator).toHaveTextContent('Connectors -'); + + const failedConnectorsIndicator = within(metrics).getByTitle( + 'Number of failed connectors' + ); + expect(failedConnectorsIndicator).toBeInTheDocument(); + expect(failedConnectorsIndicator).toHaveTextContent( + 'Failed Connectors 0' + ); + + const failedTasksIndicator = within(metrics).getByTitle( + 'Number of failed tasks' + ); + expect(failedTasksIndicator).toBeInTheDocument(); + expect(failedTasksIndicator).toHaveTextContent('Failed Tasks 0'); + }); + + it('renders indicators when connectors list is undefined', async () => { + (useConnectors as jest.Mock).mockImplementation(() => ({ + isFetching: false, + data: undefined, + })); + + await renderComponent(); + const metrics = screen.getByRole('group'); + expect(metrics).toBeInTheDocument(); + + const connectorsIndicator = within(metrics).getByTitle( + 'Total number of connectors' + ); + expect(connectorsIndicator).toBeInTheDocument(); + expect(connectorsIndicator).toHaveTextContent('Connectors -'); + + const failedConnectorsIndicator = within(metrics).getByTitle( + 'Number of failed connectors' + ); + expect(failedConnectorsIndicator).toBeInTheDocument(); + expect(failedConnectorsIndicator).toHaveTextContent( + 'Failed Connectors -' + ); + + const failedTasksIndicator = within(metrics).getByTitle( + 'Number of failed tasks' + ); + expect(failedTasksIndicator).toBeInTheDocument(); + expect(failedTasksIndicator).toHaveTextContent('Failed Tasks -'); + }); + + it('renders indicators list of connectors', async () => { + (useConnectors as jest.Mock).mockImplementation(() => ({ + isLoading: false, + data: connectors, + })); + + await renderComponent(); + + const metrics = screen.getByRole('group'); + expect(metrics).toBeInTheDocument(); + + const connectorsIndicator = within(metrics).getByTitle( + 'Total number of connectors' + ); + expect(connectorsIndicator).toBeInTheDocument(); + expect(connectorsIndicator).toHaveTextContent( + `Connectors ${connectors.length}` + ); + + const failedConnectorsIndicator = within(metrics).getByTitle( + 'Number of failed connectors' + ); + expect(failedConnectorsIndicator).toBeInTheDocument(); + expect(failedConnectorsIndicator).toHaveTextContent( + 'Failed Connectors 1' + ); + + const failedTasksIndicator = within(metrics).getByTitle( + 'Number of failed tasks' + ); + expect(failedTasksIndicator).toBeInTheDocument(); + expect(failedTasksIndicator).toHaveTextContent('Failed Tasks 1'); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/Connect/New/New.styled.ts b/kafka-ui-react-app/src/components/Connect/New/New.styled.ts index c24eff561a3..010dd25938f 100644 --- a/kafka-ui-react-app/src/components/Connect/New/New.styled.ts +++ b/kafka-ui-react-app/src/components/Connect/New/New.styled.ts @@ -1,4 +1,4 @@ -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; export const NewConnectFormStyled = styled.form` padding: 0 16px 16px; @@ -10,3 +10,11 @@ export const NewConnectFormStyled = styled.form` align-self: flex-start; } `; + +export const Filed = styled.div<{ $hidden: boolean }>( + ({ $hidden }) => + $hidden && + css` + display: none; + ` +); diff --git a/kafka-ui-react-app/src/components/Connect/New/New.tsx b/kafka-ui-react-app/src/components/Connect/New/New.tsx index a7b307b283a..bf285838d38 100644 --- a/kafka-ui-react-app/src/components/Connect/New/New.tsx +++ b/kafka-ui-react-app/src/components/Connect/New/New.tsx @@ -4,20 +4,22 @@ import useAppParams from 'lib/hooks/useAppParams'; import { Controller, FormProvider, useForm } from 'react-hook-form'; import { ErrorMessage } from '@hookform/error-message'; import { yupResolver } from '@hookform/resolvers/yup'; -import { Connect } from 'generated-sources'; -import { ClusterName, ConnectName } from 'redux/interfaces'; -import { clusterConnectConnectorPath, ClusterNameRoute } from 'lib/paths'; +import { + clusterConnectConnectorPath, + clusterConnectorsPath, + ClusterNameRoute, +} from 'lib/paths'; import yup from 'lib/yupExtended'; import Editor from 'components/common/Editor/Editor'; -import PageLoader from 'components/common/PageLoader/PageLoader'; -import { InputLabel } from 'components/common/Input/InputLabel.styled'; import Select from 'components/common/Select/Select'; import { FormError } from 'components/common/Input/Input.styled'; import Input from 'components/common/Input/Input'; import { Button } from 'components/common/Button/Button'; import PageHeading from 'components/common/PageHeading/PageHeading'; -import { createConnector } from 'redux/reducers/connect/connectSlice'; -import { useAppDispatch } from 'lib/hooks/redux'; +import Heading from 'components/common/heading/Heading.styled'; +import { useConnects, useCreateConnector } from 'lib/hooks/api/kafkaConnect'; +import get from 'lodash/get'; +import { Connect } from 'generated-sources'; import * as S from './New.styled'; @@ -26,32 +28,24 @@ const validationSchema = yup.object().shape({ config: yup.string().required().isJsonObject(), }); -export interface NewProps { - fetchConnects(clusterName: ClusterName): unknown; - areConnectsFetching: boolean; - connects: Connect[]; -} - interface FormValues { - connectName: ConnectName; + connectName: Connect['name']; name: string; config: string; } -const New: React.FC = ({ - fetchConnects, - areConnectsFetching, - connects, -}) => { +const New: React.FC = () => { const { clusterName } = useAppParams(); - const dispatch = useAppDispatch(); const navigate = useNavigate(); + const { data: connects = [] } = useConnects(clusterName); + const mutation = useCreateConnector(clusterName); + const methods = useForm({ - mode: 'onTouched', + mode: 'all', resolver: yupResolver(validationSchema), defaultValues: { - connectName: connects[0]?.name || '', + connectName: get(connects, '0.name', ''), name: '', config: '', }, @@ -64,51 +58,36 @@ const New: React.FC = ({ setValue, } = methods; - React.useEffect(() => { - fetchConnects(clusterName); - }, [fetchConnects, clusterName]); - React.useEffect(() => { if (connects && connects.length > 0 && !getValues().connectName) { setValue('connectName', connects[0].name); } }, [connects, getValues, setValue]); - const connectNameFieldClassName = React.useMemo( - () => (connects.length > 1 ? '' : 'is-hidden'), - [connects] - ); - const onSubmit = async (values: FormValues) => { - const { connector } = await dispatch( - createConnector({ - clusterName, + try { + const connector = await mutation.createResource({ connectName: values.connectName, newConnector: { name: values.name, config: JSON.parse(values.config.trim()), }, - }) - ).unwrap(); - if (connector) { - navigate( - clusterConnectConnectorPath( - clusterName, - connector.connect, - connector.name - ) - ); + }); + + if (connector) { + navigate( + clusterConnectConnectorPath( + clusterName, + connector.connect, + connector.name + ) + ); + } + } catch (e) { + // do nothing } }; - if (areConnectsFetching) { - return ; - } - - if (connects.length === 0) { - return null; - } - const connectOptions = connects.map(({ name: connectName }) => ({ value: connectName, label: connectName, @@ -116,15 +95,19 @@ const New: React.FC = ({ return ( - + -
- Connect * + + Connect * ( @@ -133,7 +116,7 @@ const New: React.FC = ({ name={name} disabled={isSubmitting} onChange={onChange} - value={connectOptions[0].value} + value={connectOptions[0]?.value} minWidth="100%" options={connectOptions} /> @@ -142,14 +125,15 @@ const New: React.FC = ({ -
+
- Name * + Name @@ -159,12 +143,12 @@ const New: React.FC = ({
- Config * + Config ( - + )} /> diff --git a/kafka-ui-react-app/src/components/Connect/New/NewContainer.ts b/kafka-ui-react-app/src/components/Connect/New/NewContainer.ts deleted file mode 100644 index d691043b54d..00000000000 --- a/kafka-ui-react-app/src/components/Connect/New/NewContainer.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { connect } from 'react-redux'; -import { fetchConnects } from 'redux/reducers/connect/connectSlice'; -import { RootState } from 'redux/interfaces'; -import { - getAreConnectsFetching, - getConnects, -} from 'redux/reducers/connect/selectors'; - -import New from './New'; - -const mapStateToProps = (state: RootState) => ({ - areConnectsFetching: getAreConnectsFetching(state), - connects: getConnects(state), -}); - -const mapDispatchToProps = { - fetchConnects, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(New); diff --git a/kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx b/kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx index d12b7af3d86..1284bfe6522 100644 --- a/kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx +++ b/kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx @@ -4,14 +4,13 @@ import { clusterConnectConnectorPath, clusterConnectorNewPath, } from 'lib/paths'; -import New, { NewProps } from 'components/Connect/New/New'; -import { connects, connector } from 'redux/reducers/connect/__test__/fixtures'; +import New from 'components/Connect/New/New'; +import { connects, connector } from 'lib/fixtures/kafkaConnect'; import { fireEvent, screen, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ControllerRenderProps } from 'react-hook-form'; -import * as redux from 'react-redux'; +import { useConnects, useCreateConnector } from 'lib/hooks/api/kafkaConnect'; -jest.mock('components/common/PageLoader/PageLoader', () => 'mock-PageLoader'); jest.mock( 'components/common/Editor/Editor', () => (props: ControllerRenderProps) => { @@ -25,19 +24,22 @@ jest.mock('react-router-dom', () => ({ useNavigate: () => mockHistoryPush, })); +jest.mock('lib/hooks/api/kafkaConnect', () => ({ + useConnects: jest.fn(), + useCreateConnector: jest.fn(), +})); + describe('New', () => { const clusterName = 'my-cluster'; const simulateFormSubmit = async () => { - await act(() => { - userEvent.type( - screen.getByPlaceholderText('Connector Name'), - 'my-connector' - ); - userEvent.type( - screen.getByPlaceholderText('json'), - '{"class":"MyClass"}'.replace(/[{[]/g, '$&$&') - ); - }); + await userEvent.type( + screen.getByPlaceholderText('Connector Name'), + 'my-connector' + ); + await userEvent.type( + screen.getByPlaceholderText('json'), + '{"class":"MyClass"}'.replace(/[{[]/g, '$&$&') + ); expect(screen.getByPlaceholderText('json')).toHaveValue( '{"class":"MyClass"}' @@ -47,68 +49,43 @@ describe('New', () => { }); }; - const renderComponent = (props: Partial = {}) => + const renderComponent = () => render( - + , { initialEntries: [clusterConnectorNewPath(clusterName)] } ); - it('fetches connects on mount', async () => { - const fetchConnects = jest.fn(); - await act(() => { - renderComponent({ fetchConnects }); - }); - expect(fetchConnects).toHaveBeenCalledTimes(1); - expect(fetchConnects).toHaveBeenCalledWith(clusterName); - }); - - it('calls createConnector on form submit', async () => { - const useDispatchSpy = jest.spyOn(redux, 'useDispatch'); - const useDispatchMock = jest.fn(() => ({ - unwrap: () => ({ connector }), - })) as jest.Mock; - useDispatchSpy.mockReturnValue(useDispatchMock); - - renderComponent(); - await simulateFormSubmit(); - - expect(useDispatchMock).toHaveBeenCalledTimes(1); + beforeEach(() => { + (useConnects as jest.Mock).mockImplementation(() => ({ + data: connects, + })); }); - it('redirects to connector details view on successful submit', async () => { - const route = clusterConnectConnectorPath( - clusterName, - connects[0].name, - connector.name - ); - - const useDispatchSpy = jest.spyOn(redux, 'useDispatch'); - const useDispatchMock = jest.fn(() => ({ - unwrap: () => ({ connector }), - })) as jest.Mock; - useDispatchSpy.mockReturnValue(useDispatchMock); - + it('calls createConnector on form submit and redirects to the list page on success', async () => { + const createConnectorMock = jest.fn(() => { + return Promise.resolve(connector); + }); + (useCreateConnector as jest.Mock).mockImplementation(() => ({ + createResource: createConnectorMock, + })); renderComponent(); - await simulateFormSubmit(); + expect(createConnectorMock).toHaveBeenCalledTimes(1); expect(mockHistoryPush).toHaveBeenCalledTimes(1); - expect(mockHistoryPush).toHaveBeenCalledWith(route); + expect(mockHistoryPush).toHaveBeenCalledWith( + clusterConnectConnectorPath(clusterName, connects[0].name, connector.name) + ); }); it('does not redirect to connector details view on unsuccessful submit', async () => { - const useDispatchSpy = jest.spyOn(redux, 'useDispatch'); - const useDispatchMock = jest.fn(async () => ({ - unwrap: () => ({}), - })) as jest.Mock; - useDispatchSpy.mockReturnValue(useDispatchMock); - + const createConnectorMock = jest.fn(() => { + return Promise.resolve(); + }); + (useCreateConnector as jest.Mock).mockImplementation(() => ({ + createResource: createConnectorMock, + })); renderComponent(); await simulateFormSubmit(); expect(mockHistoryPush).not.toHaveBeenCalled(); diff --git a/kafka-ui-react-app/src/components/Connect/__tests__/Connect.spec.tsx b/kafka-ui-react-app/src/components/Connect/__tests__/Connect.spec.tsx index b1f1d2b185b..855e5464581 100644 --- a/kafka-ui-react-app/src/components/Connect/__tests__/Connect.spec.tsx +++ b/kafka-ui-react-app/src/components/Connect/__tests__/Connect.spec.tsx @@ -2,35 +2,29 @@ import React from 'react'; import { render, WithRoute } from 'lib/testHelpers'; import { screen } from '@testing-library/react'; import Connect from 'components/Connect/Connect'; -import { store } from 'redux/store'; import { clusterConnectorsPath, clusterConnectorNewPath, clusterConnectConnectorPath, - clusterConnectConnectorEditPath, getNonExactPath, clusterConnectsPath, } from 'lib/paths'; const ConnectCompText = { - new: 'NewContainer', - list: 'ListContainer', - details: 'DetailsContainer', - edit: 'EditContainer', + new: 'New Page', + list: 'List Page', + details: 'Details Page', }; -jest.mock('components/Connect/New/NewContainer', () => () => ( +jest.mock('components/Connect/New/New', () => () => (
{ConnectCompText.new}
)); -jest.mock('components/Connect/List/ListContainer', () => () => ( +jest.mock('components/Connect/List/ListPage', () => () => (
{ConnectCompText.list}
)); -jest.mock('components/Connect/Details/DetailsContainer', () => () => ( +jest.mock('components/Connect/Details/DetailsPage', () => () => (
{ConnectCompText.details}
)); -jest.mock('components/Connect/Edit/EditContainer', () => () => ( -
{ConnectCompText.edit}
-)); describe('Connect', () => { const renderComponent = (pathname: string, routePath: string) => @@ -38,10 +32,10 @@ describe('Connect', () => { , - { initialEntries: [pathname], store } + { initialEntries: [pathname] } ); - it('renders ListContainer', () => { + it('renders ListPage', () => { renderComponent( clusterConnectorsPath('my-cluster'), clusterConnectorsPath() @@ -49,7 +43,7 @@ describe('Connect', () => { expect(screen.getByText(ConnectCompText.list)).toBeInTheDocument(); }); - it('renders NewContainer', () => { + it('renders New Page', () => { renderComponent( clusterConnectorNewPath('my-cluster'), clusterConnectorsPath() @@ -57,23 +51,11 @@ describe('Connect', () => { expect(screen.getByText(ConnectCompText.new)).toBeInTheDocument(); }); - it('renders DetailsContainer', () => { + it('renders Details Page', () => { renderComponent( clusterConnectConnectorPath('my-cluster', 'my-connect', 'my-connector'), clusterConnectsPath() ); expect(screen.getByText(ConnectCompText.details)).toBeInTheDocument(); }); - - it('renders EditContainer', () => { - renderComponent( - clusterConnectConnectorEditPath( - 'my-cluster', - 'my-connect', - 'my-connector' - ), - clusterConnectsPath() - ); - expect(screen.getByText(ConnectCompText.edit)).toBeInTheDocument(); - }); }); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx index 8e7dfc4c305..2b729718de6 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx @@ -1,40 +1,22 @@ import React from 'react'; import { Route, Routes } from 'react-router-dom'; import Details from 'components/ConsumerGroups/Details/Details'; -import ListContainer from 'components/ConsumerGroups/List/ListContainer'; import ResetOffsets from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets'; -import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route'; import { clusterConsumerGroupResetOffsetsRelativePath, RouteParams, } from 'lib/paths'; +import List from './List'; + const ConsumerGroups: React.FC = () => { return ( - - - - } - /> - -
- - } - /> + } /> + } /> - - - } + element={} /> ); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx index f76e2267eb0..f5cae44df72 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx @@ -1,85 +1,96 @@ import React from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import useAppParams from 'lib/hooks/useAppParams'; import { clusterConsumerGroupResetRelativePath, + clusterConsumerGroupsPath, ClusterGroupParam, } from 'lib/paths'; -import PageLoader from 'components/common/PageLoader/PageLoader'; -import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; +import Search from 'components/common/Search/Search'; import ClusterContext from 'components/contexts/ClusterContext'; import PageHeading from 'components/common/PageHeading/PageHeading'; -import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon'; import * as Metrics from 'components/common/Metrics'; import { Tag } from 'components/common/Tag/Tag.styled'; -import Dropdown from 'components/common/Dropdown/Dropdown'; -import DropdownItem from 'components/common/Dropdown/DropdownItem'; -import { groupBy } from 'lodash'; +import groupBy from 'lodash/groupBy'; import { Table } from 'components/common/table/Table/Table.styled'; +import getTagColor from 'components/common/Tag/getTagColor'; +import { Dropdown } from 'components/common/Dropdown'; +import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled'; +import { Action, ConsumerGroupState, ResourceType } from 'generated-sources'; +import { ActionDropdownItem } from 'components/common/ActionComponent'; import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; -import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; import { - fetchConsumerGroupDetails, - deleteConsumerGroup, - selectById, - getIsConsumerGroupDeleted, - getAreConsumerGroupDetailsFulfilled, -} from 'redux/reducers/consumerGroups/consumerGroupsSlice'; -import getTagColor from 'components/common/Tag/getTagColor'; + useConsumerGroupDetails, + useDeleteConsumerGroupMutation, +} from 'lib/hooks/api/consumers'; +import Tooltip from 'components/common/Tooltip/Tooltip'; +import { CONSUMER_GROUP_STATE_TOOLTIPS } from 'lib/constants'; import ListItem from './ListItem'; const Details: React.FC = () => { const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const searchValue = searchParams.get('q') || ''; const { isReadOnly } = React.useContext(ClusterContext); - const { consumerGroupID, clusterName } = useAppParams(); - const dispatch = useAppDispatch(); - const consumerGroup = useAppSelector((state) => - selectById(state, consumerGroupID) - ); - const isDeleted = useAppSelector(getIsConsumerGroupDeleted); - const isFetched = useAppSelector(getAreConsumerGroupDetailsFulfilled); - - const [isConfirmationModalVisible, setIsConfirmationModalVisible] = - React.useState(false); + const routeParams = useAppParams(); + const { clusterName, consumerGroupID } = routeParams; - React.useEffect(() => { - dispatch(fetchConsumerGroupDetails({ clusterName, consumerGroupID })); - }, [clusterName, consumerGroupID, dispatch]); + const consumerGroup = useConsumerGroupDetails(routeParams); + const deleteConsumerGroup = useDeleteConsumerGroupMutation(routeParams); - const onDelete = () => { - setIsConfirmationModalVisible(false); - dispatch(deleteConsumerGroup({ clusterName, consumerGroupID })); + const onDelete = async () => { + await deleteConsumerGroup.mutateAsync(); + navigate('../'); }; - React.useEffect(() => { - if (isDeleted) { - navigate('../'); - } - }, [clusterName, navigate, isDeleted]); const onResetOffsets = () => { navigate(clusterConsumerGroupResetRelativePath); }; - if (!isFetched || !consumerGroup) { - return ; - } + const partitionsByTopic = groupBy(consumerGroup.data?.partitions, 'topic'); + const filteredPartitionsByTopic = Object.keys(partitionsByTopic).filter( + (el) => el.includes(searchValue) + ); + const currentPartitionsByTopic = searchValue.length + ? filteredPartitionsByTopic + : Object.keys(partitionsByTopic); - const partitionsByTopic = groupBy(consumerGroup.partitions, 'topic'); + const hasAssignedTopics = consumerGroup?.data?.topics !== 0; return (
- + {!isReadOnly && ( - } right> - Reset offset - setIsConfirmationModalVisible(true)} + + + Reset offset + + Delete consumer group - + )} @@ -87,31 +98,49 @@ const Details: React.FC = () => { - {consumerGroup.state} + + {consumerGroup.data?.state} + + } + content={ + CONSUMER_GROUP_STATE_TOOLTIPS[ + consumerGroup.data?.state || ConsumerGroupState.UNKNOWN + ] + } + placement="bottom-start" + /> - {consumerGroup.members} + {consumerGroup.data?.members} - {consumerGroup.topics} + {consumerGroup.data?.topics} - {consumerGroup.partitions?.length} + {consumerGroup.data?.partitions?.length} - {consumerGroup.coordinator?.id} + {consumerGroup.data?.coordinator?.id} + + + {consumerGroup.data?.consumerLag} + + + - + - {Object.keys(partitionsByTopic).map((key) => ( + {currentPartitionsByTopic.map((key) => ( { ))}
- setIsConfirmationModalVisible(false)} - onConfirm={onDelete} - > - Are you sure you want to delete this consumer group? -
); }; diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/ListItem.styled.ts b/kafka-ui-react-app/src/components/ConsumerGroups/Details/ListItem.styled.ts index 11c852b3632..358a45e0a6f 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/Details/ListItem.styled.ts +++ b/kafka-ui-react-app/src/components/ConsumerGroups/Details/ListItem.styled.ts @@ -1,6 +1,7 @@ import styled from 'styled-components'; -export const ToggleButton = styled.td` - padding: 8px 8px 8px 16px !important; - width: 30px; +export const FlexWrapper = styled.div` + display: flex; + align-items: center; + gap: 8px; `; diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/ListItem.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/Details/ListItem.tsx index 7fab2e8b736..99a3c0bd8d4 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/Details/ListItem.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/Details/ListItem.tsx @@ -8,7 +8,7 @@ import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper'; import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled'; import TopicContents from './TopicContents/TopicContents'; -import { ToggleButton } from './ListItem.styled'; +import { FlexWrapper } from './ListItem.styled'; interface Props { clusterName: ClusterName; @@ -18,17 +18,29 @@ interface Props { const ListItem: React.FC = ({ clusterName, name, consumers }) => { const [isOpen, setIsOpen] = React.useState(false); + + const getTotalconsumerLag = () => { + let count = 0; + consumers.forEach((consumer) => { + count += consumer?.consumerLag || 0; + }); + return count; + }; + return ( <> - - setIsOpen(!isOpen)} aria-hidden> - - - - - {name} - + + + setIsOpen(!isOpen)} aria-hidden> + + + + {name} + + + + {getTotalconsumerLag()} {isOpen && } diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/Form.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/Form.tsx new file mode 100644 index 00000000000..0e4e05ba142 --- /dev/null +++ b/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/Form.tsx @@ -0,0 +1,197 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + ConsumerGroupDetails, + ConsumerGroupOffsetsReset, + ConsumerGroupOffsetsResetType, +} from 'generated-sources'; +import { ClusterGroupParam } from 'lib/paths'; +import { + Controller, + FormProvider, + useFieldArray, + useForm, +} from 'react-hook-form'; +import { MultiSelect, Option } from 'react-multi-select-component'; +import 'react-datepicker/dist/react-datepicker.css'; +import { ErrorMessage } from '@hookform/error-message'; +import { InputLabel } from 'components/common/Input/InputLabel.styled'; +import { Button } from 'components/common/Button/Button'; +import Input from 'components/common/Input/Input'; +import { FormError } from 'components/common/Input/Input.styled'; +import useAppParams from 'lib/hooks/useAppParams'; +import { useResetConsumerGroupOffsetsMutation } from 'lib/hooks/api/consumers'; +import { FlexFieldset, StyledForm } from 'components/common/Form/Form.styled'; +import ControlledSelect from 'components/common/Select/ControlledSelect'; + +import * as S from './ResetOffsets.styled'; + +interface FormProps { + defaultValues: ConsumerGroupOffsetsReset; + topics: string[]; + partitions: ConsumerGroupDetails['partitions']; +} + +const resetTypeOptions = Object.values(ConsumerGroupOffsetsResetType).map( + (value) => ({ value, label: value }) +); + +const Form: React.FC = ({ defaultValues, partitions, topics }) => { + const navigate = useNavigate(); + const routerParams = useAppParams(); + const reset = useResetConsumerGroupOffsetsMutation(routerParams); + const topicOptions = React.useMemo( + () => topics.map((value) => ({ value, label: value })), + [topics] + ); + const methods = useForm({ + mode: 'onChange', + defaultValues, + }); + + const { + handleSubmit, + setValue, + watch, + control, + formState: { errors }, + } = methods; + const { fields } = useFieldArray({ + control, + name: 'partitionsOffsets', + }); + + const resetTypeValue = watch('resetType'); + const topicValue = watch('topic'); + const offsetsValue = watch('partitionsOffsets'); + const partitionsValue = watch('partitions') || []; + + const partitionOptions = + partitions + ?.filter((p) => p.topic === topicValue) + .map((p) => ({ + label: `Partition #${p.partition.toString()}`, + value: p.partition, + })) || []; + + const onSelectedPartitionsChange = (selected: Option[]) => { + setValue( + 'partitions', + selected.map(({ value }) => value) + ); + + setValue( + 'partitionsOffsets', + selected.map(({ value }) => { + const currentOffset = offsetsValue?.find( + ({ partition }) => partition === value + ); + return { offset: currentOffset?.offset, partition: value }; + }) + ); + }; + + React.useEffect(() => { + onSelectedPartitionsChange([]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [topicValue]); + + const onSubmit = async (data: ConsumerGroupOffsetsReset) => { + await reset.mutateAsync(data); + navigate('../'); + }; + + return ( + + + + + +
+ Partitions + ({ + value: p, + label: String(p), + }))} + onChange={onSelectedPartitionsChange} + labelledBy="Select partitions" + /> +
+ {resetTypeValue === ConsumerGroupOffsetsResetType.TIMESTAMP && + partitionsValue.length > 0 && ( +
+ Timestamp + ( + onChange(e?.getTime())} + onBlur={onBlur} + /> + )} + /> + {message}} + /> +
+ )} + + {resetTypeValue === ConsumerGroupOffsetsResetType.OFFSET && + partitionsValue.length > 0 && ( + + {fields.map((field, index) => ( + + ))} + + )} +
+
+ +
+
+
+ ); +}; + +export default Form; diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.styled.ts b/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.styled.ts index 828d8a3997e..8a0cf02b4c3 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.styled.ts +++ b/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.styled.ts @@ -1,69 +1,33 @@ import styled from 'styled-components'; +import DatePicker from 'react-datepicker'; -export const ResetOffsetsStyledWrapper = styled.div` - padding: 16px; - padding-top: 0; - - & > form { - display: flex; - flex-direction: column; - gap: 16px; - - & > button:last-child { - align-self: flex-start; - } - } - - & .multi-select { - height: 32px; - & > .dropdown-container { - height: 32px; - & > .dropdown-heading { - height: 32px; - } - } - } - - & .date-picker { - height: 32px; - border: 1px ${(props) => props.theme.select.borderColor.normal} solid; - border-radius: 4px; - font-size: 14px; - width: 50%; - padding-left: 12px; - color: ${(props) => props.theme.select.color.normal}; - - background-image: url('data:image/svg+xml,%3Csvg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M1 1L5 5L9 1" stroke="%23454F54"/%3E%3C/svg%3E%0A') !important; - background-repeat: no-repeat !important; - background-position-x: 96% !important; - background-position-y: 55% !important; - appearance: none !important; - - &:hover { - cursor: pointer; - } - &:focus { - outline: none; - } - } -`; - -export const MainSelectorsWrapperStyled = styled.div` - display: flex; - gap: 16px; - & > * { - flex-grow: 1; - } -`; - -export const OffsetsWrapperStyled = styled.div` +export const OffsetsWrapper = styled.div` display: flex; width: 100%; flex-wrap: wrap; gap: 16px; `; -export const OffsetsTitleStyled = styled.h1` - font-size: 18px; - font-weight: 500; +export const DatePickerInput = styled(DatePicker).attrs({ + showTimeInput: true, + timeInputLabel: 'Time:', + dateFormat: 'MMMM d, yyyy h:mm aa', +})` + height: 40px; + border: 1px ${({ theme }) => theme.select.borderColor.normal} solid; + border-radius: 4px; + font-size: 14px; + width: 270px; + padding-left: 12px; + background-color: ${({ theme }) => theme.input.backgroundColor.normal}; + color: ${({ theme }) => theme.input.color.normal}; + &::placeholder { + color: ${({ theme }) => theme.input.color.normal}; + } + &:hover { + cursor: pointer; + } + &:focus { + outline: none; + } `; diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.tsx index 7c32d76c0fc..0b586479462 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.tsx @@ -1,318 +1,53 @@ import React from 'react'; -import { useNavigate } from 'react-router-dom'; -import { ConsumerGroupOffsetsResetType } from 'generated-sources'; -import { ClusterGroupParam } from 'lib/paths'; -import { - Controller, - FormProvider, - useFieldArray, - useForm, -} from 'react-hook-form'; -import MultiSelect from 'react-multi-select-component'; -import { Option } from 'react-multi-select-component/dist/lib/interfaces'; -import DatePicker from 'react-datepicker'; +import { clusterConsumerGroupsPath, ClusterGroupParam } from 'lib/paths'; import 'react-datepicker/dist/react-datepicker.css'; -import { groupBy } from 'lodash'; -import PageLoader from 'components/common/PageLoader/PageLoader'; -import { ErrorMessage } from '@hookform/error-message'; -import Select from 'components/common/Select/Select'; -import { InputLabel } from 'components/common/Input/InputLabel.styled'; -import { Button } from 'components/common/Button/Button'; -import Input from 'components/common/Input/Input'; -import { FormError } from 'components/common/Input/Input.styled'; import PageHeading from 'components/common/PageHeading/PageHeading'; -import { - fetchConsumerGroupDetails, - selectById, - getAreConsumerGroupDetailsFulfilled, - getIsOffsetReseted, - resetConsumerGroupOffsets, -} from 'redux/reducers/consumerGroups/consumerGroupsSlice'; -import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; import useAppParams from 'lib/hooks/useAppParams'; -import { resetLoaderById } from 'redux/reducers/loader/loaderSlice'; - +import { useConsumerGroupDetails } from 'lib/hooks/api/consumers'; +import PageLoader from 'components/common/PageLoader/PageLoader'; import { - MainSelectorsWrapperStyled, - OffsetsWrapperStyled, - ResetOffsetsStyledWrapper, - OffsetsTitleStyled, -} from './ResetOffsets.styled'; + ConsumerGroupOffsetsReset, + ConsumerGroupOffsetsResetType, +} from 'generated-sources'; -interface FormType { - topic: string; - resetType: ConsumerGroupOffsetsResetType; - partitionsOffsets: { offset: string | undefined; partition: number }[]; - resetToTimestamp: Date; -} +import Form from './Form'; const ResetOffsets: React.FC = () => { - const dispatch = useAppDispatch(); - const { consumerGroupID, clusterName } = useAppParams(); - const consumerGroup = useAppSelector((state) => - selectById(state, consumerGroupID) - ); - - const isFetched = useAppSelector(getAreConsumerGroupDetailsFulfilled); - const isOffsetReseted = useAppSelector(getIsOffsetReseted); - - React.useEffect(() => { - dispatch(fetchConsumerGroupDetails({ clusterName, consumerGroupID })); - }, [clusterName, consumerGroupID, dispatch]); + const routerParams = useAppParams(); - const [uniqueTopics, setUniqueTopics] = React.useState([]); - const [selectedPartitions, setSelectedPartitions] = React.useState( - [] - ); - - const methods = useForm({ - mode: 'onChange', - defaultValues: { - resetType: ConsumerGroupOffsetsResetType.EARLIEST, - topic: '', - partitionsOffsets: [], - }, - }); - const { - handleSubmit, - setValue, - watch, - control, - setError, - clearErrors, - formState: { errors, isValid }, - } = methods; - const { fields } = useFieldArray({ - control, - name: 'partitionsOffsets', - }); - const resetTypeValue = watch('resetType'); - const topicValue = watch('topic'); - const offsetsValue = watch('partitionsOffsets'); + const { consumerGroupID } = routerParams; + const consumerGroup = useConsumerGroupDetails(routerParams); - React.useEffect(() => { - if (isFetched && consumerGroup?.partitions) { - setValue('topic', consumerGroup.partitions[0].topic); - setUniqueTopics(Object.keys(groupBy(consumerGroup.partitions, 'topic'))); - } - }, [consumerGroup?.partitions, isFetched, setValue]); + if (consumerGroup.isLoading || !consumerGroup.isSuccess) + return ; - const onSelectedPartitionsChange = (value: Option[]) => { - clearErrors(); - setValue( - 'partitionsOffsets', - value.map((partition) => { - const currentOffset = offsetsValue.find( - (offset) => offset.partition === partition.value - ); - return { - offset: currentOffset ? currentOffset?.offset : undefined, - partition: partition.value, - }; - }) - ); - setSelectedPartitions(value); - }; + const partitions = consumerGroup.data.partitions || []; + const { topic } = partitions[0] || ''; - React.useEffect(() => { - onSelectedPartitionsChange([]); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [topicValue]); + const uniqTopics = Array.from( + new Set(partitions.map((partition) => partition.topic)) + ); - const onSubmit = (data: FormType) => { - const augmentedData = { - ...data, - partitions: selectedPartitions.map((partition) => partition.value), - partitionsOffsets: data.partitionsOffsets as { - offset: string; - partition: number; - }[], - }; - let isValidAugmentedData = true; - if (augmentedData.resetType === ConsumerGroupOffsetsResetType.OFFSET) { - augmentedData.partitionsOffsets.forEach((offset, index) => { - if (!offset.offset) { - setError(`partitionsOffsets.${index}.offset`, { - type: 'manual', - message: "This field shouldn't be empty!", - }); - isValidAugmentedData = false; - } - }); - } else if ( - augmentedData.resetType === ConsumerGroupOffsetsResetType.TIMESTAMP - ) { - if (!augmentedData.resetToTimestamp) { - setError(`resetToTimestamp`, { - type: 'manual', - message: "This field shouldn't be empty!", - }); - isValidAugmentedData = false; - } - } - if (isValidAugmentedData) { - dispatch( - resetConsumerGroupOffsets({ - clusterName, - consumerGroupID, - requestBody: augmentedData, - }) - ); - } + const defaultValues: ConsumerGroupOffsetsReset = { + resetType: ConsumerGroupOffsetsResetType.EARLIEST, + topic, + partitionsOffsets: [], + resetToTimestamp: new Date().getTime(), }; - const navigate = useNavigate(); - React.useEffect(() => { - if (isOffsetReseted) { - dispatch(resetLoaderById('consumerGroups/resetConsumerGroupOffsets')); - navigate('../'); - } - }, [clusterName, consumerGroupID, dispatch, navigate, isOffsetReseted]); - - if (!isFetched || !consumerGroup) { - return ; - } - return ( - - - -
- -
- Topic - ( - ({ value: type, label: type }) - )} - /> - )} - /> -
-
- Partitions - p.topic === topicValue) - .map((p) => ({ - label: `Partition #${p.partition.toString()}`, - value: p.partition, - })) || [] - } - value={selectedPartitions} - onChange={onSelectedPartitionsChange} - labelledBy="Select partitions" - /> -
-
- {resetTypeValue === ConsumerGroupOffsetsResetType.TIMESTAMP && - selectedPartitions.length > 0 && ( -
- Timestamp - ( - - )} - /> - {message}} - /> -
- )} - {resetTypeValue === ConsumerGroupOffsetsResetType.OFFSET && - selectedPartitions.length > 0 && ( -
- Offsets - - {fields.map((field, index) => ( -
- - Partition #{field.partition} - - - ( - {message} - )} - /> -
- ))} -
-
- )} - -
-
-
+ <> + +
+ ); }; diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/__test__/ResetOffsets.spec.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/__test__/ResetOffsets.spec.tsx deleted file mode 100644 index 963600b7976..00000000000 --- a/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/__test__/ResetOffsets.spec.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import React from 'react'; -import fetchMock from 'fetch-mock'; -import { act, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { render, WithRoute } from 'lib/testHelpers'; -import { clusterConsumerGroupResetOffsetsPath } from 'lib/paths'; -import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures'; -import ResetOffsets from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets'; - -const clusterName = 'cluster1'; -const { groupId } = consumerGroupPayload; - -const renderComponent = () => - render( - - - , - { - initialEntries: [ - clusterConsumerGroupResetOffsetsPath( - clusterName, - consumerGroupPayload.groupId - ), - ], - } - ); - -const resetConsumerGroupOffsetsMockCalled = () => - expect( - fetchMock.called( - `/api/clusters/${clusterName}/consumer-groups/${groupId}/offsets` - ) - ).toBeTruthy(); - -const selectresetTypeAndPartitions = async (resetType: string) => { - userEvent.click(screen.getByLabelText('Reset Type')); - userEvent.click(screen.getByText(resetType)); - userEvent.click(screen.getByText('Select...')); - await waitFor(() => { - userEvent.click(screen.getByText('Partition #0')); - }); -}; - -const resetConsumerGroupOffsetsWith = async ( - resetType: string, - offset: null | number = null -) => { - userEvent.click(screen.getByLabelText('Reset Type')); - const options = screen.getAllByText(resetType); - userEvent.click(options.length > 1 ? options[1] : options[0]); - userEvent.click(screen.getByText('Select...')); - await waitFor(() => { - userEvent.click(screen.getByText('Partition #0')); - }); - fetchMock.postOnce( - `/api/clusters/${clusterName}/consumer-groups/${groupId}/offsets`, - 200, - { - body: { - topic: '__amazon_msk_canary', - resetType, - partitions: [0], - partitionsOffsets: [{ partition: 0, offset }], - }, - } - ); - userEvent.click(screen.getByText('Submit')); - await waitFor(() => resetConsumerGroupOffsetsMockCalled()); -}; - -describe('ResetOffsets', () => { - afterEach(() => { - fetchMock.reset(); - }); - - it('renders progress bar for initial state', async () => { - fetchMock.getOnce( - `/api/clusters/${clusterName}/consumer-groups/${groupId}`, - 404 - ); - await waitFor(() => renderComponent()); - expect(screen.getByRole('progressbar')).toBeInTheDocument(); - }); - - describe('with consumer group', () => { - describe('submit handles resetConsumerGroupOffsets', () => { - beforeEach(async () => { - const fetchConsumerGroupMock = fetchMock.getOnce( - `/api/clusters/${clusterName}/consumer-groups/${groupId}`, - consumerGroupPayload - ); - await act(() => { - renderComponent(); - }); - expect(fetchConsumerGroupMock.called()).toBeTruthy(); - }); - - it('calls resetConsumerGroupOffsets with EARLIEST', async () => { - await resetConsumerGroupOffsetsWith('EARLIEST'); - }); - - it('calls resetConsumerGroupOffsets with LATEST', async () => { - await resetConsumerGroupOffsetsWith('LATEST'); - }); - it('calls resetConsumerGroupOffsets with OFFSET', async () => { - await selectresetTypeAndPartitions('OFFSET'); - fetchMock.postOnce( - `/api/clusters/${clusterName}/consumer-groups/${groupId}/offsets`, - 200, - { - body: { - topic: '__amazon_msk_canary', - resetType: 'OFFSET', - partitions: [0], - partitionsOffsets: [{ partition: 0, offset: 10 }], - }, - } - ); - await waitFor(() => { - userEvent.click(screen.getAllByLabelText('Partition #0')[1]); - }); - await waitFor(() => { - userEvent.keyboard('10'); - }); - userEvent.click(screen.getByText('Submit')); - await waitFor(() => resetConsumerGroupOffsetsMockCalled()); - }); - it('calls resetConsumerGroupOffsets with TIMESTAMP', async () => { - await selectresetTypeAndPartitions('TIMESTAMP'); - const resetConsumerGroupOffsetsMock = fetchMock.postOnce( - `/api/clusters/${clusterName}/consumer-groups/${groupId}/offsets`, - 200, - { - body: { - topic: '__amazon_msk_canary', - resetType: 'OFFSET', - partitions: [0], - partitionsOffsets: [{ partition: 0, offset: 10 }], - }, - } - ); - userEvent.click(screen.getByText('Submit')); - await waitFor(() => - expect( - screen.getByText("This field shouldn't be empty!") - ).toBeInTheDocument() - ); - - await waitFor(() => - expect( - resetConsumerGroupOffsetsMock.called( - `/api/clusters/${clusterName}/consumer-groups/${groupId}/offsets` - ) - ).toBeFalsy() - ); - }); - }); - }); -}); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/__test__/fixtures.ts b/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/__test__/fixtures.ts deleted file mode 100644 index c4833d79070..00000000000 --- a/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/__test__/fixtures.ts +++ /dev/null @@ -1,35 +0,0 @@ -export const expectedOutputs = { - EARLIEST: { - partitions: [0], - partitionsOffsets: [ - { - offset: undefined, - partition: 0, - }, - ], - resetType: 'EARLIEST', - topic: '__amazon_msk_canary', - }, - LATEST: { - partitions: [0], - partitionsOffsets: [ - { - offset: undefined, - partition: 0, - }, - ], - resetType: 'LATEST', - topic: '__amazon_msk_canary', - }, - OFFSET: { - partitions: [0], - partitionsOffsets: [ - { - offset: '10', - partition: 0, - }, - ], - resetType: 'OFFSET', - topic: '__amazon_msk_canary', - }, -}; diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/TopicContent.styled.ts b/kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/TopicContent.styled.ts index 67b03ab127f..af36dfbee70 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/TopicContent.styled.ts +++ b/kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/TopicContent.styled.ts @@ -1,16 +1,17 @@ import styled, { css } from 'styled-components'; export const TopicContentWrapper = styled.tr` - background-color: ${({ theme }) => - theme.consumerTopicContent.backgroundColor}; + background-color: ${({ theme }) => theme.default.backgroundColor}; & > td { padding: 16px !important; + background-color: ${({ theme }) => + theme.consumerTopicContent.td.backgroundColor}; } `; export const ContentBox = styled.div( ({ theme }) => css` - background-color: ${theme.menu.backgroundColor.normal}; + background-color: ${theme.default.backgroundColor}; padding: 20px; border-radius: 8px; ` diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/TopicContents.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/TopicContents.tsx index 6637821020a..8686c26ffd7 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/TopicContents.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/TopicContents.tsx @@ -1,6 +1,6 @@ import { Table } from 'components/common/table/Table/Table.styled'; import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; -import { ConsumerGroupTopicPartition } from 'generated-sources'; +import { ConsumerGroupTopicPartition, SortOrder } from 'generated-sources'; import React from 'react'; import { ContentBox, TopicContentWrapper } from './TopicContent.styled'; @@ -9,7 +9,125 @@ interface Props { consumers: ConsumerGroupTopicPartition[]; } +type OrderByKey = keyof ConsumerGroupTopicPartition; +interface Headers { + title: string; + orderBy: OrderByKey | undefined; +} + +const TABLE_HEADERS_MAP: Headers[] = [ + { title: 'Partition', orderBy: 'partition' }, + { title: 'Consumer ID', orderBy: 'consumerId' }, + { title: 'Host', orderBy: 'host' }, + { title: 'Consumer Lag', orderBy: 'consumerLag' }, + { title: 'Current Offset', orderBy: 'currentOffset' }, + { title: 'End offset', orderBy: 'endOffset' }, +]; + +const ipV4ToNum = (ip?: string) => { + if (typeof ip === 'string' && ip.length !== 0) { + const withoutSlash = ip.indexOf('/') !== -1 ? ip.slice(1) : ip; + return Number( + withoutSlash + .split('.') + .map((octet) => `000${octet}`.slice(-3)) + .join('') + ); + } + return 0; +}; + +type ComparatorFunction = ( + valueA: T, + valueB: T, + order: SortOrder, + property?: keyof T +) => number; + +const numberComparator: ComparatorFunction = ( + valueA, + valueB, + order, + property +) => { + if (property !== undefined) { + return order === SortOrder.ASC + ? Number(valueA[property]) - Number(valueB[property]) + : Number(valueB[property]) - Number(valueA[property]); + } + return 0; +}; + +const ipComparator: ComparatorFunction = ( + valueA, + valueB, + order +) => + order === SortOrder.ASC + ? ipV4ToNum(valueA.host) - ipV4ToNum(valueB.host) + : ipV4ToNum(valueB.host) - ipV4ToNum(valueA.host); + +const consumerIdComparator: ComparatorFunction = ( + valueA, + valueB, + order +) => { + if (valueA.consumerId && valueB.consumerId) { + if (order === SortOrder.ASC) { + if (valueA.consumerId?.toLowerCase() > valueB.consumerId?.toLowerCase()) { + return 1; + } + } + + if (order === SortOrder.DESC) { + if (valueB.consumerId?.toLowerCase() > valueA.consumerId?.toLowerCase()) { + return -1; + } + } + } + + return 0; +}; + const TopicContents: React.FC = ({ consumers }) => { + const [orderBy, setOrderBy] = React.useState('partition'); + const [sortOrder, setSortOrder] = React.useState(SortOrder.DESC); + + const handleOrder = React.useCallback((columnName: string | null) => { + if (typeof columnName === 'string') { + setOrderBy(columnName as OrderByKey); + setSortOrder((prevOrder) => + prevOrder === SortOrder.DESC ? SortOrder.ASC : SortOrder.DESC + ); + } + }, []); + + const sortedConsumers = React.useMemo(() => { + if (orderBy && sortOrder) { + const isNumberProperty = + orderBy === 'partition' || + orderBy === 'currentOffset' || + orderBy === 'endOffset' || + orderBy === 'consumerLag'; + + let comparator: ComparatorFunction; + if (isNumberProperty) { + comparator = numberComparator; + } + + if (orderBy === 'host') { + comparator = ipComparator; + } + + if (orderBy === 'consumerId') { + comparator = consumerIdComparator; + } + + return consumers.sort((a, b) => comparator(a, b, sortOrder, orderBy)); + } + return consumers; + }, [orderBy, sortOrder, consumers]); + return ( @@ -17,21 +135,25 @@ const TopicContents: React.FC = ({ consumers }) => { - - - - - - + {TABLE_HEADERS_MAP.map((header) => ( + + ))} - {consumers.map((consumer) => ( + {sortedConsumers.map((consumer) => ( - + diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/__test__/TopicContents.spec.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/__test__/TopicContents.spec.tsx index e1a10b7cd57..0cc91e02cb1 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/__test__/TopicContents.spec.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/__test__/TopicContents.spec.tsx @@ -2,9 +2,9 @@ import React from 'react'; import { clusterConsumerGroupDetailsPath } from 'lib/paths'; import { screen } from '@testing-library/react'; import TopicContents from 'components/ConsumerGroups/Details/TopicContents/TopicContents'; -import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures'; import { render, WithRoute } from 'lib/testHelpers'; import { ConsumerGroupTopicPartition } from 'generated-sources'; +import { consumerGroupPayload } from 'lib/fixtures/consumerGroups'; const clusterName = 'cluster1'; diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/Details.spec.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/Details.spec.tsx deleted file mode 100644 index 315cc4b7680..00000000000 --- a/kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/Details.spec.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import Details from 'components/ConsumerGroups/Details/Details'; -import React from 'react'; -import fetchMock from 'fetch-mock'; -import { render, WithRoute } from 'lib/testHelpers'; -import { - clusterConsumerGroupDetailsPath, - clusterConsumerGroupResetRelativePath, -} from 'lib/paths'; -import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures'; -import { - screen, - waitFor, - waitForElementToBeRemoved, -} from '@testing-library/dom'; -import userEvent from '@testing-library/user-event'; -import { act } from '@testing-library/react'; - -const clusterName = 'cluster1'; -const { groupId } = consumerGroupPayload; - -const mockNavigate = jest.fn(); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useNavigate: () => mockNavigate, -})); - -const renderComponent = () => { - render( - -
- , - { initialEntries: [clusterConsumerGroupDetailsPath(clusterName, groupId)] } - ); -}; -describe('Details component', () => { - afterEach(() => { - fetchMock.reset(); - mockNavigate.mockClear(); - }); - - describe('when consumer groups are NOT fetched', () => { - it('renders progress bar for initial state', () => { - fetchMock.getOnce( - `/api/clusters/${clusterName}/consumer-groups/${groupId}`, - 404 - ); - renderComponent(); - expect(screen.getByRole('progressbar')).toBeInTheDocument(); - }); - }); - - describe('when consumer gruops are fetched', () => { - beforeEach(async () => { - const fetchConsumerGroupMock = fetchMock.getOnce( - `/api/clusters/${clusterName}/consumer-groups/${groupId}`, - consumerGroupPayload - ); - renderComponent(); - await waitForElementToBeRemoved(() => screen.getByRole('progressbar')); - await waitFor(() => expect(fetchConsumerGroupMock.called()).toBeTruthy()); - }); - - it('renders component', () => { - expect(screen.getByRole('heading')).toBeInTheDocument(); - expect(screen.getByText(groupId)).toBeInTheDocument(); - - expect(screen.getByRole('table')).toBeInTheDocument(); - expect(screen.getAllByRole('columnheader').length).toEqual(2); - - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - }); - - it('handles [Reset offset] click', async () => { - userEvent.click(screen.getByText('Reset offset')); - expect(mockNavigate).toHaveBeenLastCalledWith( - clusterConsumerGroupResetRelativePath - ); - }); - - it('shows confirmation modal on consumer group delete', async () => { - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - userEvent.click(screen.getByText('Delete consumer group')); - await waitFor(() => - expect(screen.queryByRole('dialog')).toBeInTheDocument() - ); - userEvent.click(screen.getByText('Cancel')); - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - }); - - it('handles [Delete consumer group] click', async () => { - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - await act(() => { - userEvent.click(screen.getByText('Delete consumer group')); - }); - expect(screen.queryByRole('dialog')).toBeInTheDocument(); - const deleteConsumerGroupMock = fetchMock.deleteOnce( - `/api/clusters/${clusterName}/consumer-groups/${groupId}`, - 200 - ); - await act(() => { - userEvent.click(screen.getByText('Submit')); - }); - expect(deleteConsumerGroupMock.called()).toBeTruthy(); - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - expect(mockNavigate).toHaveBeenLastCalledWith('../'); - }); - }); -}); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/ListItem.spec.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/ListItem.spec.tsx deleted file mode 100644 index 5bbce7a9271..00000000000 --- a/kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/ListItem.spec.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import { clusterConsumerGroupDetailsPath } from 'lib/paths'; -import { screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import ListItem from 'components/ConsumerGroups/Details/ListItem'; -import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures'; -import { render, WithRoute } from 'lib/testHelpers'; -import { ConsumerGroupTopicPartition } from 'generated-sources'; - -const clusterName = 'cluster1'; - -const renderComponent = (consumers: ConsumerGroupTopicPartition[] = []) => - render( - -
{consumer.partition} {consumer.consumerId} {consumer.host}{consumer.messagesBehind}{consumer.consumerLag} {consumer.currentOffset} {consumer.endOffset}
- - - -
- , - { - initialEntries: [ - clusterConsumerGroupDetailsPath( - clusterName, - consumerGroupPayload.groupId - ), - ], - } - ); - -describe('ListItem', () => { - beforeEach(() => renderComponent(consumerGroupPayload.partitions)); - - it('should renders list item with topic content closed and check if element exists', () => { - expect(screen.getByRole('row')).toBeInTheDocument(); - }); - - it('should renders list item with topic content open', () => { - userEvent.click(screen.getAllByRole('cell')[0].children[0]); - expect(screen.getByText('Consumer ID')).toBeInTheDocument(); - }); -}); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/List.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/List.tsx new file mode 100644 index 00000000000..da35c6bbad6 --- /dev/null +++ b/kafka-ui-react-app/src/components/ConsumerGroups/List.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import PageHeading from 'components/common/PageHeading/PageHeading'; +import Search from 'components/common/Search/Search'; +import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled'; +import { + ConsumerGroupDetails, + ConsumerGroupOrdering, + ConsumerGroupState, + SortOrder, +} from 'generated-sources'; +import useAppParams from 'lib/hooks/useAppParams'; +import { clusterConsumerGroupDetailsPath, ClusterNameRoute } from 'lib/paths'; +import { ColumnDef } from '@tanstack/react-table'; +import Table, { LinkCell, TagCell } from 'components/common/NewTable'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { CONSUMER_GROUP_STATE_TOOLTIPS, PER_PAGE } from 'lib/constants'; +import { useConsumerGroups } from 'lib/hooks/api/consumers'; +import Tooltip from 'components/common/Tooltip/Tooltip'; + +const List = () => { + const { clusterName } = useAppParams(); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + const consumerGroups = useConsumerGroups({ + clusterName, + orderBy: (searchParams.get('sortBy') as ConsumerGroupOrdering) || undefined, + sortOrder: + (searchParams.get('sortDirection')?.toUpperCase() as SortOrder) || + undefined, + page: Number(searchParams.get('page') || 1), + perPage: Number(searchParams.get('perPage') || PER_PAGE), + search: searchParams.get('q') || '', + }); + + const columns = React.useMemo[]>( + () => [ + { + id: ConsumerGroupOrdering.NAME, + header: 'Group ID', + accessorKey: 'groupId', + // eslint-disable-next-line react/no-unstable-nested-components + cell: ({ getValue }) => ( + ()}`} + to={encodeURIComponent(`${getValue()}`)} + /> + ), + }, + { + id: ConsumerGroupOrdering.MEMBERS, + header: 'Num Of Members', + accessorKey: 'members', + }, + { + id: ConsumerGroupOrdering.TOPIC_NUM, + header: 'Num Of Topics', + accessorKey: 'topics', + }, + { + id: ConsumerGroupOrdering.MESSAGES_BEHIND, + header: 'Consumer Lag', + accessorKey: 'consumerLag', + cell: (args) => { + return args.getValue() || 'N/A'; + }, + }, + { + header: 'Coordinator', + accessorKey: 'coordinator.id', + enableSorting: false, + }, + { + id: ConsumerGroupOrdering.STATE, + header: 'State', + accessorKey: 'state', + // eslint-disable-next-line react/no-unstable-nested-components + cell: (args) => { + const value = args.getValue() as ConsumerGroupState; + return ( + } + content={CONSUMER_GROUP_STATE_TOOLTIPS[value]} + placement="bottom-end" + /> + ); + }, + }, + ], + [] + ); + + return ( + <> + + + + + + navigate( + clusterConsumerGroupDetailsPath(clusterName, original.groupId) + ) + } + disabled={consumerGroups.isFetching} + /> + + ); +}; + +export default List; diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/List/ConsumerGroupsTableCells.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/List/ConsumerGroupsTableCells.tsx deleted file mode 100644 index f4295600c14..00000000000 --- a/kafka-ui-react-app/src/components/ConsumerGroups/List/ConsumerGroupsTableCells.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; -import { Tag } from 'components/common/Tag/Tag.styled'; -import { TableCellProps } from 'components/common/SmartTable/TableColumn'; -import { ConsumerGroup } from 'generated-sources'; -import { SmartTableKeyLink } from 'components/common/table/Table/TableKeyLink.styled'; -import getTagColor from 'components/common/Tag/getTagColor'; - -export const StatusCell: React.FC> = ({ - dataItem, -}) => { - return {dataItem.state}; -}; - -export const GroupIDCell: React.FC> = ({ - dataItem: { groupId }, -}) => { - return ( - - {groupId} - - ); -}; diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx deleted file mode 100644 index 18c9142f5d1..00000000000 --- a/kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import React from 'react'; -import PageHeading from 'components/common/PageHeading/PageHeading'; -import Search from 'components/common/Search/Search'; -import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled'; -import { - ConsumerGroupDetails, - ConsumerGroupOrdering, - SortOrder, -} from 'generated-sources'; -import { useTableState } from 'lib/hooks/useTableState'; -import { SmartTable } from 'components/common/SmartTable/SmartTable'; -import { TableColumn } from 'components/common/SmartTable/TableColumn'; -import { - GroupIDCell, - StatusCell, -} from 'components/ConsumerGroups/List/ConsumerGroupsTableCells'; -import usePagination from 'lib/hooks/usePagination'; -import useSearch from 'lib/hooks/useSearch'; -import { useAppDispatch } from 'lib/hooks/redux'; -import useAppParams from 'lib/hooks/useAppParams'; -import { ClusterNameRoute } from 'lib/paths'; -import { fetchConsumerGroupsPaged } from 'redux/reducers/consumerGroups/consumerGroupsSlice'; -import PageLoader from 'components/common/PageLoader/PageLoader'; - -export interface Props { - consumerGroups: ConsumerGroupDetails[]; - orderBy: ConsumerGroupOrdering | null; - sortOrder: SortOrder; - totalPages: number; - isFetched: boolean; - setConsumerGroupsSortOrderBy(orderBy: ConsumerGroupOrdering | null): void; -} - -const List: React.FC = ({ - consumerGroups, - sortOrder, - orderBy, - totalPages, - isFetched, - setConsumerGroupsSortOrderBy, -}) => { - const { page, perPage } = usePagination(); - const [searchText, handleSearchText] = useSearch(); - const dispatch = useAppDispatch(); - const { clusterName } = useAppParams(); - - React.useEffect(() => { - dispatch( - fetchConsumerGroupsPaged({ - clusterName, - orderBy: orderBy || undefined, - sortOrder, - page, - perPage, - search: searchText, - }) - ); - }, [clusterName, orderBy, searchText, sortOrder, page, perPage, dispatch]); - - const tableState = useTableState< - ConsumerGroupDetails, - string, - ConsumerGroupOrdering - >( - consumerGroups, - { - totalPages, - idSelector: (consumerGroup) => consumerGroup.groupId, - }, - { - handleOrderBy: setConsumerGroupsSortOrderBy, - orderBy, - sortOrder, - } - ); - - if (!isFetched) { - return ; - } - - return ( -
- - - - - - - - - - - - -
- ); -}; - -export default List; diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/List/ListContainer.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/List/ListContainer.tsx deleted file mode 100644 index f72ae2c16d2..00000000000 --- a/kafka-ui-react-app/src/components/ConsumerGroups/List/ListContainer.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { connect } from 'react-redux'; -import { RootState } from 'redux/interfaces'; -import { - getConsumerGroupsOrderBy, - getConsumerGroupsSortOrder, - getConsumerGroupsTotalPages, - sortBy, - selectAll, - getAreConsumerGroupsPagedFulfilled, -} from 'redux/reducers/consumerGroups/consumerGroupsSlice'; -import List from 'components/ConsumerGroups/List/List'; - -const mapStateToProps = (state: RootState) => ({ - consumerGroups: selectAll(state), - orderBy: getConsumerGroupsOrderBy(state), - sortOrder: getConsumerGroupsSortOrder(state), - totalPages: getConsumerGroupsTotalPages(state), - isFetched: getAreConsumerGroupsPagedFulfilled(state), -}); - -const mapDispatchToProps = { - setConsumerGroupsSortOrderBy: sortBy, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(List); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/List/ListItem.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/List/ListItem.tsx deleted file mode 100644 index 9078487cefc..00000000000 --- a/kafka-ui-react-app/src/components/ConsumerGroups/List/ListItem.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; -import { ConsumerGroup } from 'generated-sources'; -import { Tag } from 'components/common/Tag/Tag.styled'; -import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled'; -import getTagColor from 'components/common/Tag/getTagColor'; - -const ListItem: React.FC<{ consumerGroup: ConsumerGroup }> = ({ - consumerGroup, -}) => { - return ( -
- - - {consumerGroup.groupId} - - - - - - - - - ); -}; - -export default ListItem; diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ConsumerGroupsTableCells.spec.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ConsumerGroupsTableCells.spec.tsx deleted file mode 100644 index 7aff424cb81..00000000000 --- a/kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ConsumerGroupsTableCells.spec.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import { render } from 'lib/testHelpers'; -import { - GroupIDCell, - StatusCell, -} from 'components/ConsumerGroups/List/ConsumerGroupsTableCells'; -import { TableState } from 'lib/hooks/useTableState'; -import { ConsumerGroup, ConsumerGroupState } from 'generated-sources'; -import { screen } from '@testing-library/react'; - -describe('Consumer Groups Table Cells', () => { - const consumerGroup: ConsumerGroup = { - groupId: 'groupId', - members: 1, - topics: 1, - simple: true, - state: ConsumerGroupState.STABLE, - coordinator: { - id: 6598, - }, - }; - const mockTableState: TableState = { - data: [consumerGroup], - selectedIds: new Set([]), - idSelector: jest.fn(), - isRowSelectable: jest.fn(), - selectedCount: 0, - setRowsSelection: jest.fn(), - toggleSelection: jest.fn(), - }; - - describe('StatusCell', () => { - it('should Tag props render normally', () => { - render( - - ); - const linkElement = screen.getByRole('link'); - expect(linkElement).toBeInTheDocument(); - expect(linkElement).toHaveAttribute('href', `/${consumerGroup.groupId}`); - }); - }); - - describe('GroupIdCell', () => { - it('should GroupIdCell props render normally', () => { - render( - - ); - expect( - screen.getByText(consumerGroup.state as string) - ).toBeInTheDocument(); - }); - }); -}); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/List.spec.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/List.spec.tsx deleted file mode 100644 index 04b78a983be..00000000000 --- a/kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/List.spec.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import List, { Props } from 'components/ConsumerGroups/List/List'; -import { screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { render } from 'lib/testHelpers'; -import { consumerGroups as consumerGroupMock } from 'redux/reducers/consumerGroups/__test__/fixtures'; -import { ConsumerGroupOrdering, SortOrder } from 'generated-sources'; -import theme from 'theme/theme'; - -describe('List', () => { - const setUpComponent = (props: Partial = {}) => { - const { - consumerGroups, - orderBy, - sortOrder, - totalPages, - setConsumerGroupsSortOrderBy, - } = props; - return render( - - ); - }; - - it('renders empty table', () => { - setUpComponent(); - expect(screen.getByRole('table')).toBeInTheDocument(); - expect(screen.getByText('No active consumer groups')).toBeInTheDocument(); - }); - - describe('consumerGroups are fetched', () => { - beforeEach(() => setUpComponent({ consumerGroups: consumerGroupMock })); - - it('renders all rows with consumers', () => { - expect(screen.getByText('groupId1')).toBeInTheDocument(); - expect(screen.getByText('groupId2')).toBeInTheDocument(); - }); - - describe('Testing the Ordering', () => { - it('should test the sort order functionality', async () => { - const thElement = screen.getByText(/consumer group id/i); - expect(thElement).toBeInTheDocument(); - expect(thElement).toHaveStyle(`color:${theme.table.th.color.active}`); - }); - }); - }); - - describe('consumerGroups are fetched with custom parameters', () => { - it('should test the order by functionality of another element', async () => { - const sortOrder = jest.fn(); - setUpComponent({ - consumerGroups: consumerGroupMock, - setConsumerGroupsSortOrderBy: sortOrder, - }); - const thElement = screen.getByText(/num of members/i); - expect(thElement).toBeInTheDocument(); - - userEvent.click(thElement); - expect(sortOrder).toBeCalled(); - }); - - it('should view the ordered list with the right prop', () => { - setUpComponent({ - consumerGroups: consumerGroupMock, - orderBy: ConsumerGroupOrdering.MEMBERS, - }); - expect(screen.getByText(/num of members/i)).toHaveStyle( - `color:${theme.table.th.color.active}` - ); - }); - }); -}); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ListItem.spec.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ListItem.spec.tsx deleted file mode 100644 index 856271f5740..00000000000 --- a/kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ListItem.spec.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react'; -import ListItem from 'components/ConsumerGroups/List/ListItem'; -import { ConsumerGroupState, ConsumerGroup } from 'generated-sources'; -import { screen } from '@testing-library/react'; -import { render } from 'lib/testHelpers'; - -describe('List', () => { - const mockConsumerGroup = { - groupId: 'groupId', - members: 0, - topics: 1, - simple: false, - partitionAssignor: '', - coordinator: { - id: 1, - host: 'host', - }, - partitions: [ - { - consumerId: null, - currentOffset: 0, - endOffset: 0, - host: null, - messagesBehind: 0, - partition: 1, - topic: 'topic', - }, - ], - }; - const setupWrapper = (consumerGroup: ConsumerGroup) => ( -
{consumerGroup.members}{consumerGroup.topics}{consumerGroup.messagesBehind}{consumerGroup.coordinator?.id} - {consumerGroup.state} -
- - - -
- ); - - const getCell = () => screen.getAllByRole('cell')[5]; - - it('render empty ListItem', () => { - render(setupWrapper(mockConsumerGroup)); - expect(screen.getByRole('row')).toBeInTheDocument(); - }); - - it('renders item with stable status', () => { - render( - setupWrapper({ - ...mockConsumerGroup, - state: ConsumerGroupState.STABLE, - }) - ); - expect(screen.getByRole('row')).toHaveTextContent( - ConsumerGroupState.STABLE - ); - }); - - it('renders item with dead status', () => { - render( - setupWrapper({ - ...mockConsumerGroup, - state: ConsumerGroupState.DEAD, - }) - ); - expect(getCell()).toHaveTextContent(ConsumerGroupState.DEAD); - }); - - it('renders item with empty status', () => { - render( - setupWrapper({ - ...mockConsumerGroup, - state: ConsumerGroupState.EMPTY, - }) - ); - expect(getCell()).toHaveTextContent(ConsumerGroupState.EMPTY); - }); - - it('renders item with empty-string status', () => { - render( - setupWrapper({ - ...mockConsumerGroup, - state: ConsumerGroupState.UNKNOWN, - }) - ); - expect(getCell()).toHaveTextContent(ConsumerGroupState.UNKNOWN); - }); -}); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/__test__/ConsumerGroups.spec.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/__test__/ConsumerGroups.spec.tsx index 7aedbd038c7..4d06c3ecc4b 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/__test__/ConsumerGroups.spec.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/__test__/ConsumerGroups.spec.tsx @@ -1,22 +1,25 @@ import React from 'react'; -import { clusterConsumerGroupsPath, getNonExactPath } from 'lib/paths'; import { - act, - screen, - waitFor, - waitForElementToBeRemoved, -} from '@testing-library/react'; + clusterConsumerGroupDetailsPath, + clusterConsumerGroupResetOffsetsPath, + clusterConsumerGroupsPath, + getNonExactPath, +} from 'lib/paths'; +import { screen } from '@testing-library/react'; import ConsumerGroups from 'components/ConsumerGroups/ConsumerGroups'; -import { - consumerGroups, - noConsumerGroupsResponse, -} from 'redux/reducers/consumerGroups/__test__/fixtures'; import { render, WithRoute } from 'lib/testHelpers'; -import fetchMock from 'fetch-mock'; -import { ConsumerGroupOrdering, SortOrder } from 'generated-sources'; const clusterName = 'cluster1'; +jest.mock('components/ConsumerGroups/List', () => () =>
ListPage
); +jest.mock('components/ConsumerGroups/Details/Details', () => () => ( +
DetailsMock
+)); +jest.mock( + 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets', + () => () =>
ResetOffsetsMock
+); + const renderComponent = (path?: string) => render( @@ -28,104 +31,18 @@ const renderComponent = (path?: string) => ); describe('ConsumerGroups', () => { - it('renders with initial state', async () => { + it('renders ListContainer', async () => { renderComponent(); - expect(screen.getByRole('progressbar')).toBeInTheDocument(); + expect(screen.getByText('ListPage')).toBeInTheDocument(); }); - - describe('Default Route and Fetching Consumer Groups', () => { - const url = `/api/clusters/${clusterName}/consumer-groups/paged`; - afterEach(() => { - fetchMock.reset(); - }); - - it('renders empty table on no consumer group response', async () => { - fetchMock.getOnce(url, noConsumerGroupsResponse, { - query: { - orderBy: ConsumerGroupOrdering.NAME, - sortOrder: SortOrder.ASC, - }, - }); - await act(() => { - renderComponent(); - }); - expect(fetchMock.calls().length).toBe(1); - expect(screen.getByRole('table')).toBeInTheDocument(); - expect(screen.getByText('No active consumer groups')).toBeInTheDocument(); - }); - - it('renders with 404 from consumer groups', async () => { - const consumerGroupsMock = fetchMock.getOnce(url, 404, { - query: { - orderBy: ConsumerGroupOrdering.NAME, - sortOrder: SortOrder.ASC, - }, - }); - - renderComponent(); - - await waitFor(() => expect(consumerGroupsMock.called()).toBeTruthy()); - - expect(screen.queryByText('Consumers')).not.toBeInTheDocument(); - expect(screen.queryByRole('table')).not.toBeInTheDocument(); - }); - - it('renders with 200 from consumer groups', async () => { - const consumerGroupsMock = fetchMock.getOnce( - url, - { - pagedCount: 1, - consumerGroups, - }, - { - query: { - orderBy: ConsumerGroupOrdering.NAME, - sortOrder: SortOrder.ASC, - }, - } - ); - - renderComponent(); - - await waitForElementToBeRemoved(() => screen.getByRole('progressbar')); - await waitFor(() => expect(consumerGroupsMock.called()).toBeTruthy()); - - expect(screen.getByText('Consumers')).toBeInTheDocument(); - expect(screen.getByRole('table')).toBeInTheDocument(); - expect(screen.getByText(consumerGroups[0].groupId)).toBeInTheDocument(); - expect(screen.getByText(consumerGroups[1].groupId)).toBeInTheDocument(); - }); - - it('renders with 200 from consumer groups with Searched Query ', async () => { - const searchResult = consumerGroups[0]; - const searchText = searchResult.groupId; - - const consumerGroupsMock = fetchMock.getOnce( - url, - { - pagedCount: 1, - consumerGroups: [searchResult], - }, - { - query: { - orderBy: ConsumerGroupOrdering.NAME, - sortOrder: SortOrder.ASC, - search: searchText, - }, - } - ); - - renderComponent( - `${clusterConsumerGroupsPath(clusterName)}?q=${searchText}` - ); - - await waitForElementToBeRemoved(() => screen.getByRole('progressbar')); - await waitFor(() => expect(consumerGroupsMock.called()).toBeTruthy()); - - expect(screen.getByText(searchText)).toBeInTheDocument(); - expect( - screen.queryByText(consumerGroups[1].groupId) - ).not.toBeInTheDocument(); - }); + it('renders ResetOffsets', async () => { + renderComponent( + clusterConsumerGroupResetOffsetsPath(clusterName, 'groupId1') + ); + expect(screen.getByText('ResetOffsetsMock')).toBeInTheDocument(); + }); + it('renders Details', async () => { + renderComponent(clusterConsumerGroupDetailsPath(clusterName, 'groupId1')); + expect(screen.getByText('DetailsMock')).toBeInTheDocument(); }); }); diff --git a/kafka-ui-react-app/src/components/Dashboard/ClusterName.tsx b/kafka-ui-react-app/src/components/Dashboard/ClusterName.tsx new file mode 100644 index 00000000000..7ab581d405a --- /dev/null +++ b/kafka-ui-react-app/src/components/Dashboard/ClusterName.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { CellContext } from '@tanstack/react-table'; +import { Tag } from 'components/common/Tag/Tag.styled'; +import { Cluster } from 'generated-sources'; + +type ClusterNameProps = CellContext; + +const ClusterName: React.FC = ({ row }) => { + const { readOnly, name } = row.original; + return ( + <> + {readOnly && readonly} + {name} + + ); +}; + +export default ClusterName; diff --git a/kafka-ui-react-app/src/components/Dashboard/ClusterTableActionsCell.tsx b/kafka-ui-react-app/src/components/Dashboard/ClusterTableActionsCell.tsx new file mode 100644 index 00000000000..946f8b9ddaf --- /dev/null +++ b/kafka-ui-react-app/src/components/Dashboard/ClusterTableActionsCell.tsx @@ -0,0 +1,33 @@ +import React, { useMemo } from 'react'; +import { Cluster, ResourceType } from 'generated-sources'; +import { CellContext } from '@tanstack/react-table'; +import { clusterConfigPath } from 'lib/paths'; +import { useGetUserInfo } from 'lib/hooks/api/roles'; +import { ActionCanButton } from 'components/common/ActionComponent'; + +type Props = CellContext; + +const ClusterTableActionsCell: React.FC = ({ row }) => { + const { name } = row.original; + const { data } = useGetUserInfo(); + + const hasPermissions = useMemo(() => { + if (!data?.rbacEnabled) return true; + return !!data?.userInfo?.permissions.some( + (permission) => permission.resource === ResourceType.APPLICATIONCONFIG + ); + }, [data]); + + return ( + + Configure + + ); +}; + +export default ClusterTableActionsCell; diff --git a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.styled.ts b/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.styled.ts deleted file mode 100644 index 341cb245ff0..00000000000 --- a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.styled.ts +++ /dev/null @@ -1,15 +0,0 @@ -import styled from 'styled-components'; - -interface TableCellProps { - maxWidth?: string; -} - -export const SwitchWrapper = styled.div` - padding: 16px; -`; - -export const TableCell = styled.td.attrs({ role: 'cells' })` - padding: 16px; - word-break: break-word; - max-width: ${(props) => props.maxWidth}; -`; diff --git a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.tsx b/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.tsx deleted file mode 100644 index e921609c1a4..00000000000 --- a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React from 'react'; -import { chunk } from 'lodash'; -import { v4 } from 'uuid'; -import * as Metrics from 'components/common/Metrics'; -import { Cluster } from 'generated-sources'; -import { Tag } from 'components/common/Tag/Tag.styled'; -import { Table } from 'components/common/table/Table/Table.styled'; -import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; -import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted'; -import { NavLink } from 'react-router-dom'; -import { clusterTopicsPath } from 'lib/paths'; -import Switch from 'components/common/Switch/Switch'; - -import * as S from './ClustersWidget.styled'; - -interface Props { - clusters: Cluster[]; - onlineClusters: Cluster[]; - offlineClusters: Cluster[]; -} - -interface ChunkItem { - id: string; - data: Cluster[]; -} - -const ClustersWidget: React.FC = ({ - clusters, - onlineClusters, - offlineClusters, -}) => { - const [showOfflineOnly, setShowOfflineOnly] = React.useState(false); - - const clusterList: ChunkItem[] = React.useMemo(() => { - let list = clusters; - - if (showOfflineOnly) { - list = offlineClusters; - } - - return chunk(list, 2).map((data) => ({ - id: v4(), - data, - })); - }, [clusters, offlineClusters, showOfflineOnly]); - - const handleSwitch = () => setShowOfflineOnly(!showOfflineOnly); - - return ( - <> - - - Online}> - {onlineClusters.length}{' '} - clusters - - Offline}> - {offlineClusters.length}{' '} - clusters - - - - - - - - {clusterList.map((chunkItem) => ( - - - - - - - - - - - - - - {chunkItem.data.map((cluster) => ( - - - {cluster.readOnly && readonly}{' '} - {cluster.name} - - {cluster.version} - {cluster.brokerCount} - - {cluster.onlinePartitionCount} - - - - {cluster.topicCount} - - - - - - - - - - ))} - -
- ))} - - ); -}; - -export default ClustersWidget; diff --git a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidgetContainer.ts b/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidgetContainer.ts deleted file mode 100644 index 7533e5947a2..00000000000 --- a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidgetContainer.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { connect } from 'react-redux'; -import { - getClusterList, - getOnlineClusters, - getOfflineClusters, -} from 'redux/reducers/clusters/clustersSlice'; -import { RootState } from 'redux/interfaces'; - -import ClustersWidget from './ClustersWidget'; - -const mapStateToProps = (state: RootState) => ({ - clusters: getClusterList(state), - onlineClusters: getOnlineClusters(state), - offlineClusters: getOfflineClusters(state), -}); - -export default connect(mapStateToProps)(ClustersWidget); diff --git a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClustersWidget.spec.tsx b/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClustersWidget.spec.tsx deleted file mode 100644 index a79c567070b..00000000000 --- a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClustersWidget.spec.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { screen } from '@testing-library/react'; -import ClustersWidget from 'components/Dashboard/ClustersWidget/ClustersWidget'; -import userEvent from '@testing-library/user-event'; -import { render } from 'lib/testHelpers'; - -import { offlineCluster, onlineCluster, clusters } from './fixtures'; - -const setupComponent = () => - render( - - ); - -describe('ClustersWidget', () => { - beforeEach(() => setupComponent()); - - it('renders clusterWidget list', () => { - expect(screen.getAllByRole('row').length).toBe(3); - }); - - it('hides online cluster widgets', () => { - expect(screen.getAllByRole('row').length).toBe(3); - userEvent.click(screen.getByRole('checkbox')); - expect(screen.getAllByRole('row').length).toBe(2); - }); - - it('when cluster is read-only', () => { - expect(screen.getByText('readonly')).toBeInTheDocument(); - }); - - it('render clusterWidget cells', () => { - const cells = screen.getAllByRole('cells'); - expect(cells.length).toBe(14); - expect(cells[0]).toHaveStyle('max-width: 99px'); - }); -}); diff --git a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClustersWidgetContainer.spec.tsx b/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClustersWidgetContainer.spec.tsx deleted file mode 100644 index 666211288b4..00000000000 --- a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClustersWidgetContainer.spec.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import ClustersWidget from 'components/Dashboard/ClustersWidget/ClustersWidget'; -import { getByTextContent, render } from 'lib/testHelpers'; - -describe('ClustersWidgetContainer', () => { - it('renders ClustersWidget', () => { - render( - - ); - expect(getByTextContent('Online 0 clusters')).toBeInTheDocument(); - }); -}); diff --git a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/fixtures.ts b/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/fixtures.ts deleted file mode 100644 index c60027ac2c2..00000000000 --- a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/fixtures.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Cluster, ServerStatus } from 'generated-sources'; - -export const onlineCluster: Cluster = { - name: 'secondLocal', - defaultCluster: false, - status: ServerStatus.ONLINE, - brokerCount: 1, - onlinePartitionCount: 6, - topicCount: 3, - bytesInPerSec: 0.00003061819685376471, - bytesOutPerSec: 5.737800890036267, - readOnly: false, -}; - -export const offlineCluster: Cluster = { - name: 'local', - defaultCluster: true, - status: ServerStatus.OFFLINE, - brokerCount: 1, - onlinePartitionCount: 2, - topicCount: 2, - bytesInPerSec: 8000.00000673768, - bytesOutPerSec: 0.8153063567297119, - readOnly: true, -}; - -export const clusters: Cluster[] = [onlineCluster, offlineCluster]; diff --git a/kafka-ui-react-app/src/components/Dashboard/Dashboard.styled.ts b/kafka-ui-react-app/src/components/Dashboard/Dashboard.styled.ts new file mode 100644 index 00000000000..6bdd80d4def --- /dev/null +++ b/kafka-ui-react-app/src/components/Dashboard/Dashboard.styled.ts @@ -0,0 +1,9 @@ +import styled from 'styled-components'; + +export const Toolbar = styled.div` + padding: 8px 16px; + display: flex; + justify-content: space-between; + align-items: center; + color: ${({ theme }) => theme.default.color.normal}; +`; diff --git a/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx b/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx index 87d54a5eb32..1fb8076fd48 100644 --- a/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx +++ b/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx @@ -1,13 +1,111 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import PageHeading from 'components/common/PageHeading/PageHeading'; +import * as Metrics from 'components/common/Metrics'; +import { Tag } from 'components/common/Tag/Tag.styled'; +import Switch from 'components/common/Switch/Switch'; +import { useClusters } from 'lib/hooks/api/clusters'; +import { Cluster, ResourceType, ServerStatus } from 'generated-sources'; +import { ColumnDef } from '@tanstack/react-table'; +import Table, { SizeCell } from 'components/common/NewTable'; +import useBoolean from 'lib/hooks/useBoolean'; +import { clusterNewConfigPath } from 'lib/paths'; +import { GlobalSettingsContext } from 'components/contexts/GlobalSettingsContext'; +import { ActionCanButton } from 'components/common/ActionComponent'; +import { useGetUserInfo } from 'lib/hooks/api/roles'; -import ClustersWidgetContainer from './ClustersWidget/ClustersWidgetContainer'; +import * as S from './Dashboard.styled'; +import ClusterName from './ClusterName'; +import ClusterTableActionsCell from './ClusterTableActionsCell'; -const Dashboard: React.FC = () => ( - <> - - - -); +const Dashboard: React.FC = () => { + const { data } = useGetUserInfo(); + const clusters = useClusters(); + const { value: showOfflineOnly, toggle } = useBoolean(false); + const appInfo = React.useContext(GlobalSettingsContext); + + const config = React.useMemo(() => { + const clusterList = clusters.data || []; + const offlineClusters = clusterList.filter( + ({ status }) => status === ServerStatus.OFFLINE + ); + return { + list: showOfflineOnly ? offlineClusters : clusterList, + online: clusterList.length - offlineClusters.length, + offline: offlineClusters.length, + }; + }, [clusters, showOfflineOnly]); + + const columns = React.useMemo[]>(() => { + const initialColumns: ColumnDef[] = [ + { header: 'Cluster name', accessorKey: 'name', cell: ClusterName }, + { header: 'Version', accessorKey: 'version' }, + { header: 'Brokers count', accessorKey: 'brokerCount' }, + { header: 'Partitions', accessorKey: 'onlinePartitionCount' }, + { header: 'Topics', accessorKey: 'topicCount' }, + { header: 'Production', accessorKey: 'bytesInPerSec', cell: SizeCell }, + { header: 'Consumption', accessorKey: 'bytesOutPerSec', cell: SizeCell }, + ]; + + if (appInfo.hasDynamicConfig) { + initialColumns.push({ + header: '', + id: 'actions', + cell: ClusterTableActionsCell, + }); + } + + return initialColumns; + }, []); + + const hasPermissions = useMemo(() => { + if (!data?.rbacEnabled) return true; + return !!data?.userInfo?.permissions.some( + (permission) => permission.resource === ResourceType.APPLICATIONCONFIG + ); + }, [data]); + return ( + <> + + + + Online}> + {config.online || 0}{' '} + clusters + + Offline}> + {config.offline || 0}{' '} + clusters + + + + +
+ + +
+ {appInfo.hasDynamicConfig && ( + + Configure new cluster + + )} +
+ + + ); +}; export default Dashboard; diff --git a/kafka-ui-react-app/src/components/Dashboard/__test__/Dashboard.spec.tsx b/kafka-ui-react-app/src/components/Dashboard/__test__/Dashboard.spec.tsx deleted file mode 100644 index e6454cce11f..00000000000 --- a/kafka-ui-react-app/src/components/Dashboard/__test__/Dashboard.spec.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import Dashboard from 'components/Dashboard/Dashboard'; -import { render } from 'lib/testHelpers'; -import { screen } from '@testing-library/dom'; - -jest.mock( - 'components/Dashboard/ClustersWidget/ClustersWidgetContainer.ts', - () => () =>
mock-ClustersWidgetContainer
-); - -describe('Dashboard', () => { - it('renders ClustersWidget', () => { - render(); - expect(screen.getByText('Dashboard')).toBeInTheDocument(); - expect( - screen.getByText('mock-ClustersWidgetContainer') - ).toBeInTheDocument(); - }); -}); diff --git a/kafka-ui-react-app/src/components/ErrorPage/ErrorPage.styled.ts b/kafka-ui-react-app/src/components/ErrorPage/ErrorPage.styled.ts new file mode 100644 index 00000000000..a307c8e96f8 --- /dev/null +++ b/kafka-ui-react-app/src/components/ErrorPage/ErrorPage.styled.ts @@ -0,0 +1,21 @@ +import styled from 'styled-components'; + +export const Wrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + gap: 20px; + margin-top: 100px; +`; + +export const Status = styled.div` + font-size: 100px; + color: ${({ theme }) => theme.default.color.normal}; + line-height: initial; +`; + +export const Text = styled.div` + font-size: 20px; + color: ${({ theme }) => theme.default.color.normal}; +`; diff --git a/kafka-ui-react-app/src/components/ErrorPage/ErrorPage.tsx b/kafka-ui-react-app/src/components/ErrorPage/ErrorPage.tsx new file mode 100644 index 00000000000..91f6d48488f --- /dev/null +++ b/kafka-ui-react-app/src/components/ErrorPage/ErrorPage.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Button } from 'components/common/Button/Button'; + +import * as S from './ErrorPage.styled'; + +interface Props { + status?: number; + text?: string; + btnText?: string; +} + +const ErrorPage: React.FC = ({ + status = 404, + text = 'Page is not found', + btnText = 'Go Back to Dashboard', +}) => { + return ( + + {status} + {text} + + + ); +}; + +export default ErrorPage; diff --git a/kafka-ui-react-app/src/components/ErrorPage/__tests__/ErrorPage.spec.tsx b/kafka-ui-react-app/src/components/ErrorPage/__tests__/ErrorPage.spec.tsx new file mode 100644 index 00000000000..18a745b1922 --- /dev/null +++ b/kafka-ui-react-app/src/components/ErrorPage/__tests__/ErrorPage.spec.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; +import { render } from 'lib/testHelpers'; +import ErrorPage from 'components/ErrorPage/ErrorPage'; + +describe('ErrorPage', () => { + it('should check Error Page rendering with default text', () => { + render(); + expect(screen.getByText('404')).toBeInTheDocument(); + expect(screen.getByText('Page is not found')).toBeInTheDocument(); + expect(screen.getByText('Go Back to Dashboard')).toBeInTheDocument(); + }); + it('should check Error Page rendering with custom text', () => { + const props = { + status: 403, + text: 'access is denied', + btnText: 'Go back', + }; + render(); + expect(screen.getByText(props.status)).toBeInTheDocument(); + expect(screen.getByText(props.text)).toBeInTheDocument(); + expect(screen.getByText(props.btnText)).toBeInTheDocument(); + }); +}); diff --git a/kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx b/kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx index cb64314c690..d105720aa7f 100644 --- a/kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx +++ b/kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx @@ -1,30 +1,109 @@ import React from 'react'; -import { Route, Routes } from 'react-router-dom'; -import { clusterKsqlDbQueryRelativePath } from 'lib/paths'; -import List from 'components/KsqlDb/List/List'; import Query from 'components/KsqlDb/Query/Query'; -import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route'; +import useAppParams from 'lib/hooks/useAppParams'; +import * as Metrics from 'components/common/Metrics'; +import { + clusterKsqlDbQueryRelativePath, + clusterKsqlDbStreamsPath, + clusterKsqlDbStreamsRelativePath, + clusterKsqlDbTablesPath, + clusterKsqlDbTablesRelativePath, + ClusterNameRoute, +} from 'lib/paths'; +import PageHeading from 'components/common/PageHeading/PageHeading'; +import { ActionButton } from 'components/common/ActionComponent'; +import Navbar from 'components/common/Navigation/Navbar.styled'; +import { Navigate, NavLink, Route, Routes } from 'react-router-dom'; +import { Action, ResourceType } from 'generated-sources'; +import { useKsqlkDb } from 'lib/hooks/api/ksqlDb'; +import 'ace-builds/src-noconflict/ace'; + +import TableView from './TableView'; const KsqlDb: React.FC = () => { + const { clusterName } = useAppParams(); + + const [tables, streams] = useKsqlkDb(clusterName); + + const isFetching = tables.isFetching || streams.isFetching; + return ( - - - - - } - /> - - - - } - /> - + <> + + + Execute KSQL Request + + + + + + {tables.isSuccess ? tables.data.length : '-'} + + + {streams.isSuccess ? streams.data.length : '-'} + + + +
+ + (isActive ? 'is-active' : '')} + end + > + Tables + + (isActive ? 'is-active' : '')} + end + > + Streams + + + + } + /> + + } + /> + + } + /> + } /> + +
+ ); }; diff --git a/kafka-ui-react-app/src/components/KsqlDb/List/List.tsx b/kafka-ui-react-app/src/components/KsqlDb/List/List.tsx deleted file mode 100644 index 1c0d5ffcc1c..00000000000 --- a/kafka-ui-react-app/src/components/KsqlDb/List/List.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React, { FC, useEffect } from 'react'; -import useAppParams from 'lib/hooks/useAppParams'; -import * as Metrics from 'components/common/Metrics'; -import PageLoader from 'components/common/PageLoader/PageLoader'; -import ListItem from 'components/KsqlDb/List/ListItem'; -import { useDispatch, useSelector } from 'react-redux'; -import { fetchKsqlDbTables } from 'redux/reducers/ksqlDb/ksqlDbSlice'; -import { getKsqlDbTables } from 'redux/reducers/ksqlDb/selectors'; -import { clusterKsqlDbQueryRelativePath, ClusterNameRoute } from 'lib/paths'; -import PageHeading from 'components/common/PageHeading/PageHeading'; -import { Table } from 'components/common/table/Table/Table.styled'; -import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; -import { Button } from 'components/common/Button/Button'; - -const headers = [ - { Header: 'Type', accessor: 'type' }, - { Header: 'Name', accessor: 'name' }, - { Header: 'Topic', accessor: 'topic' }, - { Header: 'Key Format', accessor: 'keyFormat' }, - { Header: 'Value Format', accessor: 'valueFormat' }, -]; - -const accessors = headers.map((header) => header.accessor); - -const List: FC = () => { - const dispatch = useDispatch(); - - const { clusterName } = useAppParams(); - - const { rows, fetching, tablesCount, streamsCount } = - useSelector(getKsqlDbTables); - - useEffect(() => { - dispatch(fetchKsqlDbTables(clusterName)); - }, [clusterName, dispatch]); - - return ( - <> - - - - - - - {tablesCount} - - - {streamsCount} - - - -
- {fetching ? ( - - ) : ( -
- - - - {headers.map(({ Header, accessor }) => ( - - ))} - - - - {rows.map((row) => ( - - ))} - {rows.length === 0 && ( - - - - )} - -
- No tables or streams found -
- )} -
- - ); -}; - -export default List; diff --git a/kafka-ui-react-app/src/components/KsqlDb/List/ListItem.tsx b/kafka-ui-react-app/src/components/KsqlDb/List/ListItem.tsx deleted file mode 100644 index 65390caa2cc..00000000000 --- a/kafka-ui-react-app/src/components/KsqlDb/List/ListItem.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; - -interface Props { - accessors: string[]; - data: Record; -} - -const ListItem: React.FC = ({ accessors, data }) => { - return ( - - {accessors.map((accessor: string) => ( - {data[accessor]} - ))} - - ); -}; - -export default ListItem; diff --git a/kafka-ui-react-app/src/components/KsqlDb/List/__test__/List.spec.tsx b/kafka-ui-react-app/src/components/KsqlDb/List/__test__/List.spec.tsx deleted file mode 100644 index b367a6117ce..00000000000 --- a/kafka-ui-react-app/src/components/KsqlDb/List/__test__/List.spec.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import List from 'components/KsqlDb/List/List'; -import { clusterKsqlDbPath } from 'lib/paths'; -import { render, WithRoute } from 'lib/testHelpers'; -import fetchMock from 'fetch-mock'; -import { screen, waitForElementToBeRemoved } from '@testing-library/dom'; - -const clusterName = 'local'; - -const renderComponent = () => { - render( - - - , - { initialEntries: [clusterKsqlDbPath(clusterName)] } - ); -}; - -describe('KsqlDb List', () => { - afterEach(() => fetchMock.reset()); - it('renders placeholder on empty data', async () => { - fetchMock.post( - { - url: `/api/clusters/${clusterName}/ksql`, - }, - { data: [] } - ); - renderComponent(); - await waitForElementToBeRemoved(() => screen.getByRole('progressbar')); - expect(screen.getByText('No tables or streams found')).toBeInTheDocument(); - }); -}); diff --git a/kafka-ui-react-app/src/components/KsqlDb/List/__test__/ListItem.spec.tsx b/kafka-ui-react-app/src/components/KsqlDb/List/__test__/ListItem.spec.tsx deleted file mode 100644 index 0942ee89185..00000000000 --- a/kafka-ui-react-app/src/components/KsqlDb/List/__test__/ListItem.spec.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import { clusterKsqlDbPath } from 'lib/paths'; -import { render, WithRoute } from 'lib/testHelpers'; -import { screen } from '@testing-library/dom'; -import ListItem from 'components/KsqlDb/List/ListItem'; - -const clusterName = 'local'; - -const renderComponent = ({ - accessors, - data, -}: { - accessors: string[]; - data: Record; -}) => { - render( - - - , - { initialEntries: [clusterKsqlDbPath(clusterName)] } - ); -}; - -describe('KsqlDb List Item', () => { - it('renders placeholder on one data', async () => { - renderComponent({ - accessors: ['accessors'], - data: { accessors: 'accessors text' }, - }); - - expect(screen.getByText('accessors text')).toBeInTheDocument(); - }); -}); diff --git a/kafka-ui-react-app/src/components/KsqlDb/Query/Query.styled.ts b/kafka-ui-react-app/src/components/KsqlDb/Query/Query.styled.ts deleted file mode 100644 index 8b145a89daa..00000000000 --- a/kafka-ui-react-app/src/components/KsqlDb/Query/Query.styled.ts +++ /dev/null @@ -1,9 +0,0 @@ -import PageLoader from 'components/common/PageLoader/PageLoader'; -import styled from 'styled-components'; - -export const ContinuousLoader = styled(PageLoader)` - & > div { - transform: scale(0.5); - padding-top: 0; - } -`; diff --git a/kafka-ui-react-app/src/components/KsqlDb/Query/Query.tsx b/kafka-ui-react-app/src/components/KsqlDb/Query/Query.tsx index 9e60d101286..267498b923b 100644 --- a/kafka-ui-react-app/src/components/KsqlDb/Query/Query.tsx +++ b/kafka-ui-react-app/src/components/KsqlDb/Query/Query.tsx @@ -1,238 +1,54 @@ -import React, { useCallback, useEffect, FC, useState } from 'react'; +import React from 'react'; import useAppParams from 'lib/hooks/useAppParams'; import TableRenderer from 'components/KsqlDb/Query/renderer/TableRenderer/TableRenderer'; -import { - executeKsql, - resetExecutionResult, -} from 'redux/reducers/ksqlDb/ksqlDbSlice'; -import { useDispatch, useSelector } from 'react-redux'; -import { getKsqlExecution } from 'redux/reducers/ksqlDb/selectors'; -import { BASE_PARAMS } from 'lib/constants'; -import { KsqlResponse, KsqlTableResponse } from 'generated-sources'; -import { alertAdded, alertDissmissed } from 'redux/reducers/alerts/alertsSlice'; -import { now } from 'lodash'; import { ClusterNameRoute } from 'lib/paths'; +import { + useExecuteKsqlkDbQueryMutation, + useKsqlkDbSSE, +} from 'lib/hooks/api/ksqlDb'; import type { FormValues } from './QueryForm/QueryForm'; -import * as S from './Query.styled'; import QueryForm from './QueryForm/QueryForm'; -const AUTO_DISMISS_TIME = 8_000; - -export const getFormattedErrorFromTableData = ( - responseValues: KsqlTableResponse['values'] -): { title: string; message: string } => { - // We expect someting like that - // [[ - // "@type", - // "error_code", - // "message", - // "statementText"?, - // "entities"? - // ]], - // or - // [["message"]] - - if (!responseValues || !responseValues.length) { - return { - title: 'Unknown error', - message: 'Recieved empty response', - }; - } - - let title = ''; - let message = ''; - if (responseValues[0].length < 2) { - const [messageText] = responseValues[0]; - title = messageText; - } else { - const [type, errorCode, messageText, statementText, entities] = - responseValues[0]; - title = `[Error #${errorCode}] ${type}`; - message = - (entities?.length ? `[${entities.join(', ')}] ` : '') + - (statementText ? `"${statementText}" ` : '') + - messageText; - } - - return { - title, - message, - }; -}; - -const Query: FC = () => { +const Query = () => { const { clusterName } = useAppParams(); - - const sseRef = React.useRef<{ sse: EventSource | null; isOpen: boolean }>({ - sse: null, - isOpen: false, - }); - const [fetching, setFetching] = useState(false); - const dispatch = useDispatch(); - - const { executionResult } = useSelector(getKsqlExecution); - const [KSQLTable, setKSQLTable] = useState(null); - - const reset = useCallback(() => { - dispatch(resetExecutionResult()); - }, [dispatch]); - - useEffect(() => { - return reset; - }, [reset]); - - const destroySSE = () => { - if (sseRef.current?.sse) { - sseRef.current.sse.close(); - setFetching(false); - sseRef.current.sse = null; - sseRef.current.isOpen = false; - } - }; - - const handleSSECancel = useCallback(() => { - reset(); - destroySSE(); - }, [reset]); - - const createSSE = useCallback( - (pipeId: string) => { - const url = `${BASE_PARAMS.basePath}/api/clusters/${clusterName}/ksql/response?pipeId=${pipeId}`; - const sse = new EventSource(url); - sseRef.current.sse = sse; - setFetching(true); - - sse.onopen = () => { - sseRef.current.isOpen = true; - }; - - sse.onmessage = ({ data }) => { - const { table }: KsqlResponse = JSON.parse(data); - if (table) { - switch (table?.header) { - case 'Execution error': { - const { title, message } = getFormattedErrorFromTableData( - table.values - ); - const id = `${url}-executionError`; - dispatch( - alertAdded({ - id, - type: 'error', - title, - message, - createdAt: now(), - }) - ); - break; - } - case 'Schema': { - setKSQLTable(table); - break; - } - case 'Row': { - setKSQLTable((PrevKSQLTable) => { - return { - header: PrevKSQLTable?.header, - columnNames: PrevKSQLTable?.columnNames, - values: [ - ...(PrevKSQLTable?.values || []), - ...(table?.values || []), - ], - }; - }); - break; - } - case 'Query Result': { - const id = `${url}-querySuccess`; - dispatch( - alertAdded({ - id, - type: 'success', - title: 'Query succeed', - message: '', - createdAt: now(), - }) - ); - - setTimeout(() => { - dispatch(alertDissmissed(id)); - }, AUTO_DISMISS_TIME); - break; - } - case 'Source Description': - case 'properties': - default: { - setKSQLTable(table); - break; - } - } - } - return sse; - }; - - sse.onerror = () => { - // if it's open - we know that server responded without opening SSE - if (!sseRef.current.isOpen) { - const id = `${url}-connectionClosedError`; - dispatch( - alertAdded({ - id, - type: 'error', - title: 'SSE connection closed', - message: '', - createdAt: now(), - }) - ); - - setTimeout(() => { - dispatch(alertDissmissed(id)); - }, AUTO_DISMISS_TIME); - } - destroySSE(); - }; - }, - [clusterName, dispatch] - ); - - const submitHandler = useCallback( - (values: FormValues) => { - setFetching(true); - dispatch( - executeKsql({ - clusterName, - ksqlCommandV2: { - ...values, - streamsProperties: values.streamsProperties - ? JSON.parse(values.streamsProperties) + const executeQuery = useExecuteKsqlkDbQueryMutation(); + const [pipeId, setPipeId] = React.useState(false); + + const sse = useKsqlkDbSSE({ clusterName, pipeId }); + + const isFetching = executeQuery.isLoading || sse.isFetching; + + const submitHandler = async (values: FormValues) => { + const filtered = values.streamsProperties.filter(({ key }) => key != null); + const streamsProperties = filtered.reduce>( + (acc, current) => ({ ...acc, [current.key]: current.value }), + {} + ); + await executeQuery.mutateAsync( + { + clusterName, + ksqlCommandV2: { + ...values, + streamsProperties: + values.streamsProperties[0].key !== '' + ? JSON.parse(JSON.stringify(streamsProperties)) : undefined, - }, - }) - ); - }, - [dispatch, clusterName] - ); - useEffect(() => { - if (executionResult?.pipeId) { - createSSE(executionResult.pipeId); - } - return () => { - destroySSE(); - }; - }, [createSSE, executionResult]); + }, + }, + { onSuccess: (data) => setPipeId(data.pipeId) } + ); + }; return ( <> setKSQLTable(null)} - handleSSECancel={handleSSECancel} + fetching={isFetching} + hasResults={!!sse.data && !!pipeId} + resetResults={() => setPipeId(false)} submitHandler={submitHandler} /> - {KSQLTable && } - {fetching && } + {pipeId && !!sse.data && } ); }; diff --git a/kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/QueryForm.styled.ts b/kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/QueryForm.styled.ts index 980fa0c2165..eb71ad1ef27 100644 --- a/kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/QueryForm.styled.ts +++ b/kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/QueryForm.styled.ts @@ -1,57 +1,75 @@ import styled, { css } from 'styled-components'; import BaseSQLEditor from 'components/common/SQLEditor/SQLEditor'; -import BaseEditor from 'components/common/Editor/Editor'; export const QueryWrapper = styled.div` padding: 16px; `; export const KSQLInputsWrapper = styled.div` - width: 100%; display: flex; gap: 24px; - padding-bottom: 16px; - & > div { - flex-grow: 1; + + @media screen and (max-width: 769px) { + flex-direction: column; } `; export const KSQLInputHeader = styled.div` display: flex; justify-content: space-between; + color: ${({ theme }) => theme.default.color.normal}; `; -export const KSQLButtons = styled.div` - display: flex; - gap: 16px; +export const InputsContainer = styled.div` + display: grid; + grid-template-columns: 1fr 1fr 30px; + align-items: center; + gap: 10px; `; export const Fieldset = styled.fieldset` - width: 100%; + display: flex; + flex: 1; + flex-direction: column; + gap: 8px; + color: ${({ theme }) => theme.default.color.normal}; `; -export const Editor = styled(BaseEditor)( - ({ readOnly, theme }) => - readOnly && - css` - &, - &.ace-tomorrow { - background: ${theme.ksqlDb.query.editor.readonly.background}; - } - .ace-cursor { - ${theme.ksqlDb.query.editor.readonly.cursor} - } - ` -); +export const ButtonsContainer = styled.div` + display: flex; + gap: 8px; +`; export const SQLEditor = styled(BaseSQLEditor)( ({ readOnly, theme }) => - readOnly && css` - background: ${theme.ksqlDb.query.editor.readonly.background}; + background: ${readOnly && theme.ksqlDb.query.editor.readonly.background}; .ace-cursor { - ${theme.ksqlDb.query.editor.readonly.cursor} + ${readOnly && `background: ${theme.default.transparentColor} `} + } + + .ace_content { + background-color: ${theme.default.backgroundColor}; + color: ${theme.default.color.normal}; + } + .ace_line { + background-color: ${theme.ksqlDb.query.editor.activeLine + .backgroundColor}; + } + .ace_gutter-cell { + background-color: ${theme.ksqlDb.query.editor.cell.backgroundColor}; + } + .ace_gutter-layer { + background-color: ${theme.ksqlDb.query.editor.layer.backgroundColor}; + color: ${theme.default.color.normal}; + } + .ace_cursor { + color: ${theme.ksqlDb.query.editor.cursor}; + } + + .ace_print-margin { + display: none; } ` ); diff --git a/kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/QueryForm.tsx b/kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/QueryForm.tsx index f824dd256a7..7456b5d2d97 100644 --- a/kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/QueryForm.tsx +++ b/kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/QueryForm.tsx @@ -1,169 +1,217 @@ import React from 'react'; import { FormError } from 'components/common/Input/Input.styled'; import { ErrorMessage } from '@hookform/error-message'; +import { + useForm, + Controller, + useFieldArray, + FormProvider, +} from 'react-hook-form'; +import { Button } from 'components/common/Button/Button'; +import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper'; +import CloseCircleIcon from 'components/common/Icons/CloseCircleIcon'; import { yupResolver } from '@hookform/resolvers/yup'; import yup from 'lib/yupExtended'; -import { useForm, Controller } from 'react-hook-form'; -import { Button } from 'components/common/Button/Button'; -import { SchemaType } from 'generated-sources'; +import PlusIcon from 'components/common/Icons/PlusIcon'; +import ReactAce from 'react-ace'; +import Input from 'components/common/Input/Input'; import * as S from './QueryForm.styled'; -export interface Props { +interface QueryFormProps { fetching: boolean; hasResults: boolean; - handleClearResults: () => void; - handleSSECancel: () => void; + resetResults: () => void; submitHandler: (values: FormValues) => void; } - +type StreamsPropertiesType = { + key: string; + value: string; +}; export type FormValues = { ksql: string; - streamsProperties: string; + streamsProperties: StreamsPropertiesType[]; }; +const streamsPropertiesSchema = yup.object().shape({ + key: yup.string().trim(), + value: yup.string().trim(), +}); const validationSchema = yup.object({ ksql: yup.string().trim().required(), - streamsProperties: yup.lazy((value) => - value === '' ? yup.string().trim() : yup.string().trim().isJsonObject() - ), + streamsProperties: yup.array().of(streamsPropertiesSchema), }); -const QueryForm: React.FC = ({ +const QueryForm: React.FC = ({ fetching, hasResults, - handleClearResults, - handleSSECancel, submitHandler, + resetResults, }) => { - const { - handleSubmit, - setValue, - control, - formState: { errors }, - } = useForm({ + const methods = useForm({ mode: 'onTouched', resolver: yupResolver(validationSchema), defaultValues: { ksql: '', - streamsProperties: '', + streamsProperties: [{ key: '', value: '' }], }, }); + const { + handleSubmit, + setValue, + control, + watch, + formState: { errors, isDirty }, + } = methods; + + const { fields, append, remove, update } = useFieldArray< + FormValues, + 'streamsProperties' + >({ + control, + name: 'streamsProperties', + }); + + const watchStreamProps = watch('streamsProperties'); + + const appendProperty = () => { + append({ key: '', value: '' }); + }; + const removeProperty = (index: number) => () => { + if (fields.length === 1) { + update(index, { key: '', value: '' }); + return; + } + + remove(index); + }; + + const isAppendDisabled = + fetching || !!watchStreamProps.find((field) => !field.key); + + const inputRef = React.useRef(null); + + const handleFocus = () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const textInput = inputRef?.current?.editor?.textInput as any; + + if (textInput) { + textInput.focus(); + } + }; + + const handleClear = () => { + handleFocus(); + resetResults(); + }; + return ( - - - - - - - - - ( - { - handleSubmit(submitHandler)(); + + + + + + + + + + ( + { + handleSubmit(submitHandler)(); + }, }, - }, - ]} - readOnly={fetching} - /> - )} - /> - - - - - - - + ]} + readOnly={fetching} + ref={inputRef} + /> + )} + /> + + + + + + + Stream properties: + {fields.map((field, index) => ( + + + + + + + + ))} - - ( - { - handleSubmit(submitHandler)(); - }, - }, - ]} - schemaType={SchemaType.JSON} - readOnly={fetching} - /> - )} - /> - - - - - - - - - - - - + + + + + + + + + ); }; diff --git a/kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/__test__/QueryForm.spec.tsx b/kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/__test__/QueryForm.spec.tsx deleted file mode 100644 index 09f61cccb4a..00000000000 --- a/kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/__test__/QueryForm.spec.tsx +++ /dev/null @@ -1,310 +0,0 @@ -import { render } from 'lib/testHelpers'; -import React from 'react'; -import QueryForm, { Props } from 'components/KsqlDb/Query/QueryForm/QueryForm'; -import { screen, within } from '@testing-library/dom'; -import userEvent from '@testing-library/user-event'; -import { act } from '@testing-library/react'; - -const renderComponent = (props: Props) => render(); - -describe('QueryForm', () => { - it('renders', () => { - renderComponent({ - fetching: false, - hasResults: false, - handleClearResults: jest.fn(), - handleSSECancel: jest.fn(), - submitHandler: jest.fn(), - }); - - const KSQLBlock = screen.getByLabelText('KSQL'); - expect(KSQLBlock).toBeInTheDocument(); - expect(within(KSQLBlock).getByText('KSQL')).toBeInTheDocument(); - expect( - within(KSQLBlock).getByRole('button', { name: 'Clear' }) - ).toBeInTheDocument(); - // Represents SQL editor - expect(within(KSQLBlock).getByRole('textbox')).toBeInTheDocument(); - - const streamPropertiesBlock = screen.getByLabelText( - 'Stream properties (JSON format)' - ); - expect(streamPropertiesBlock).toBeInTheDocument(); - expect( - within(streamPropertiesBlock).getByText('Stream properties (JSON format)') - ).toBeInTheDocument(); - expect( - within(streamPropertiesBlock).getByRole('button', { name: 'Clear' }) - ).toBeInTheDocument(); - // Represents JSON editor - expect( - within(streamPropertiesBlock).getByRole('textbox') - ).toBeInTheDocument(); - - // Form controls - expect(screen.getByRole('button', { name: 'Execute' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Execute' })).toBeEnabled(); - expect( - screen.getByRole('button', { name: 'Stop query' }) - ).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Stop query' })).toBeDisabled(); - expect( - screen.getByRole('button', { name: 'Clear results' }) - ).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: 'Clear results' }) - ).toBeDisabled(); - }); - - it('renders error with empty input', async () => { - const submitFn = jest.fn(); - renderComponent({ - fetching: false, - hasResults: false, - handleClearResults: jest.fn(), - handleSSECancel: jest.fn(), - submitHandler: submitFn, - }); - - await act(() => - userEvent.click(screen.getByRole('button', { name: 'Execute' })) - ); - expect(screen.getByText('ksql is a required field')).toBeInTheDocument(); - expect(submitFn).not.toBeCalled(); - }); - - it('renders error with non-JSON streamProperties', async () => { - renderComponent({ - fetching: false, - hasResults: false, - handleClearResults: jest.fn(), - handleSSECancel: jest.fn(), - submitHandler: jest.fn(), - }); - - await act(() => { - // the use of `paste` is a hack that i found somewhere, - // `type` won't work - userEvent.paste( - within( - screen.getByLabelText('Stream properties (JSON format)') - ).getByRole('textbox'), - 'not-a-JSON-string' - ); - - userEvent.click(screen.getByRole('button', { name: 'Execute' })); - }); - - expect( - screen.getByText('streamsProperties is not JSON object') - ).toBeInTheDocument(); - }); - - it('renders without error with correct JSON', async () => { - renderComponent({ - fetching: false, - hasResults: false, - handleClearResults: jest.fn(), - handleSSECancel: jest.fn(), - submitHandler: jest.fn(), - }); - - await act(() => { - userEvent.paste( - within( - screen.getByLabelText('Stream properties (JSON format)') - ).getByRole('textbox'), - '{"totallyJSON": "string"}' - ); - userEvent.click(screen.getByRole('button', { name: 'Execute' })); - }); - expect( - screen.queryByText('streamsProperties is not JSON object') - ).not.toBeInTheDocument(); - }); - - it('submits with correct inputs', async () => { - const submitFn = jest.fn(); - renderComponent({ - fetching: false, - hasResults: false, - handleClearResults: jest.fn(), - handleSSECancel: jest.fn(), - submitHandler: submitFn, - }); - - await act(() => { - userEvent.paste( - within(screen.getByLabelText('KSQL')).getByRole('textbox'), - 'show tables;' - ); - - userEvent.paste( - within( - screen.getByLabelText('Stream properties (JSON format)') - ).getByRole('textbox'), - '{"totallyJSON": "string"}' - ); - - userEvent.click(screen.getByRole('button', { name: 'Execute' })); - }); - - expect( - screen.queryByText('ksql is a required field') - ).not.toBeInTheDocument(); - - expect( - screen.queryByText('streamsProperties is not JSON object') - ).not.toBeInTheDocument(); - - expect(submitFn).toBeCalled(); - }); - - it('clear results is enabled when has results', async () => { - const clearFn = jest.fn(); - renderComponent({ - fetching: false, - hasResults: true, - handleClearResults: clearFn, - handleSSECancel: jest.fn(), - submitHandler: jest.fn(), - }); - - expect(screen.getByRole('button', { name: 'Clear results' })).toBeEnabled(); - - await act(() => - userEvent.click(screen.getByRole('button', { name: 'Clear results' })) - ); - - expect(clearFn).toBeCalled(); - }); - - it('stop query query is enabled when is fetching', async () => { - const cancelFn = jest.fn(); - renderComponent({ - fetching: true, - hasResults: false, - handleClearResults: jest.fn(), - handleSSECancel: cancelFn, - submitHandler: jest.fn(), - }); - - expect(screen.getByRole('button', { name: 'Stop query' })).toBeEnabled(); - - await act(() => - userEvent.click(screen.getByRole('button', { name: 'Stop query' })) - ); - - expect(cancelFn).toBeCalled(); - }); - - it('submits form with ctrl+enter on KSQL editor', async () => { - const submitFn = jest.fn(); - renderComponent({ - fetching: false, - hasResults: false, - handleClearResults: jest.fn(), - handleSSECancel: jest.fn(), - submitHandler: submitFn, - }); - - await act(() => { - userEvent.paste( - within(screen.getByLabelText('KSQL')).getByRole('textbox'), - 'show tables;' - ); - - userEvent.type( - within(screen.getByLabelText('KSQL')).getByRole('textbox'), - '{ctrl}{enter}' - ); - }); - - expect(submitFn.mock.calls.length).toBe(1); - }); - - it('submits form with ctrl+enter on streamProperties editor', async () => { - const submitFn = jest.fn(); - renderComponent({ - fetching: false, - hasResults: false, - handleClearResults: jest.fn(), - handleSSECancel: jest.fn(), - submitHandler: submitFn, - }); - - await act(() => { - userEvent.paste( - within(screen.getByLabelText('KSQL')).getByRole('textbox'), - 'show tables;' - ); - - userEvent.paste( - within( - screen.getByLabelText('Stream properties (JSON format)') - ).getByRole('textbox'), - '{"some":"json"}' - ); - - userEvent.type( - within( - screen.getByLabelText('Stream properties (JSON format)') - ).getByRole('textbox'), - '{ctrl}{enter}' - ); - }); - - expect(submitFn.mock.calls.length).toBe(1); - }); - - it('clears KSQL with Clear button', async () => { - renderComponent({ - fetching: false, - hasResults: false, - handleClearResults: jest.fn(), - handleSSECancel: jest.fn(), - submitHandler: jest.fn(), - }); - - await act(() => { - userEvent.paste( - within(screen.getByLabelText('KSQL')).getByRole('textbox'), - 'show tables;' - ); - userEvent.click( - within(screen.getByLabelText('KSQL')).getByRole('button', { - name: 'Clear', - }) - ); - }); - - expect(screen.queryByText('show tables;')).not.toBeInTheDocument(); - }); - - it('clears streamProperties with Clear button', async () => { - renderComponent({ - fetching: false, - hasResults: false, - handleClearResults: jest.fn(), - handleSSECancel: jest.fn(), - submitHandler: jest.fn(), - }); - - await act(() => { - userEvent.paste( - within( - screen.getByLabelText('Stream properties (JSON format)') - ).getByRole('textbox'), - '{"some":"json"}' - ); - userEvent.click( - within( - screen.getByLabelText('Stream properties (JSON format)') - ).getByRole('button', { - name: 'Clear', - }) - ); - }); - expect(screen.queryByText('{"some":"json"}')).not.toBeInTheDocument(); - }); -}); diff --git a/kafka-ui-react-app/src/components/KsqlDb/Query/__test__/Query.spec.tsx b/kafka-ui-react-app/src/components/KsqlDb/Query/__test__/Query.spec.tsx deleted file mode 100644 index fd2cacfd7f1..00000000000 --- a/kafka-ui-react-app/src/components/KsqlDb/Query/__test__/Query.spec.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { render, EventSourceMock, WithRoute } from 'lib/testHelpers'; -import React from 'react'; -import Query, { - getFormattedErrorFromTableData, -} from 'components/KsqlDb/Query/Query'; -import { screen, within } from '@testing-library/dom'; -import fetchMock from 'fetch-mock'; -import userEvent from '@testing-library/user-event'; -import { clusterKsqlDbQueryPath } from 'lib/paths'; -import { act } from '@testing-library/react'; - -const clusterName = 'testLocal'; -const renderComponent = () => - render( - - - , - { - initialEntries: [clusterKsqlDbQueryPath(clusterName)], - } - ); - -describe('Query', () => { - it('renders', () => { - renderComponent(); - - expect(screen.getByLabelText('KSQL')).toBeInTheDocument(); - expect( - screen.getByLabelText('Stream properties (JSON format)') - ).toBeInTheDocument(); - }); - - afterEach(() => fetchMock.reset()); - it('fetch on execute', async () => { - renderComponent(); - - const mock = fetchMock.postOnce(`/api/clusters/${clusterName}/ksql/v2`, { - pipeId: 'testPipeID', - }); - - Object.defineProperty(window, 'EventSource', { - value: EventSourceMock, - }); - - await act(() => { - userEvent.paste( - within(screen.getByLabelText('KSQL')).getByRole('textbox'), - 'show tables;' - ); - userEvent.click(screen.getByRole('button', { name: 'Execute' })); - }); - - expect(mock.calls().length).toBe(1); - }); - - it('fetch on execute with streamParams', async () => { - renderComponent(); - - const mock = fetchMock.postOnce(`/api/clusters/${clusterName}/ksql/v2`, { - pipeId: 'testPipeID', - }); - - Object.defineProperty(window, 'EventSource', { - value: EventSourceMock, - }); - - await act(() => { - userEvent.paste( - within(screen.getByLabelText('KSQL')).getByRole('textbox'), - 'show tables;' - ); - userEvent.paste( - within( - screen.getByLabelText('Stream properties (JSON format)') - ).getByRole('textbox'), - '{"some":"json"}' - ); - userEvent.click(screen.getByRole('button', { name: 'Execute' })); - }); - expect(mock.calls().length).toBe(1); - }); - - it('fetch on execute with streamParams', async () => { - renderComponent(); - - const mock = fetchMock.postOnce(`/api/clusters/${clusterName}/ksql/v2`, { - pipeId: 'testPipeID', - }); - - Object.defineProperty(window, 'EventSource', { - value: EventSourceMock, - }); - - await act(() => { - userEvent.paste( - within(screen.getByLabelText('KSQL')).getByRole('textbox'), - 'show tables;' - ); - userEvent.paste( - within( - screen.getByLabelText('Stream properties (JSON format)') - ).getByRole('textbox'), - '{"some":"json"}' - ); - userEvent.click(screen.getByRole('button', { name: 'Execute' })); - }); - expect(mock.calls().length).toBe(1); - }); -}); - -describe('getFormattedErrorFromTableData', () => { - it('works', () => { - expect(getFormattedErrorFromTableData([['Test Error']])).toStrictEqual({ - title: 'Test Error', - message: '', - }); - - expect( - getFormattedErrorFromTableData([ - ['some_type', 'errorCode', 'messageText'], - ]) - ).toStrictEqual({ - title: '[Error #errorCode] some_type', - message: 'messageText', - }); - - expect( - getFormattedErrorFromTableData([ - [ - 'some_type', - 'errorCode', - 'messageText', - 'statementText', - ['test1', 'test2'], - ], - ]) - ).toStrictEqual({ - title: '[Error #errorCode] some_type', - message: '[test1, test2] "statementText" messageText', - }); - - expect(getFormattedErrorFromTableData([])).toStrictEqual({ - title: 'Unknown error', - message: 'Recieved empty response', - }); - }); -}); diff --git a/kafka-ui-react-app/src/components/KsqlDb/Query/renderer/TableRenderer/TableRenderer.styled.tsx b/kafka-ui-react-app/src/components/KsqlDb/Query/renderer/TableRenderer/TableRenderer.styled.tsx index c02c30d3691..96ef549aa69 100644 --- a/kafka-ui-react-app/src/components/KsqlDb/Query/renderer/TableRenderer/TableRenderer.styled.tsx +++ b/kafka-ui-react-app/src/components/KsqlDb/Query/renderer/TableRenderer/TableRenderer.styled.tsx @@ -8,6 +8,7 @@ export const Wrapper = styled.div` export const ScrollableTable = styled(Table)` overflow-y: scroll; + width: 100%; td { vertical-align: top; diff --git a/kafka-ui-react-app/src/components/KsqlDb/Query/renderer/TableRenderer/TableRenderer.tsx b/kafka-ui-react-app/src/components/KsqlDb/Query/renderer/TableRenderer/TableRenderer.tsx index e34b3d25db5..4e1acb38d6f 100644 --- a/kafka-ui-react-app/src/components/KsqlDb/Query/renderer/TableRenderer/TableRenderer.tsx +++ b/kafka-ui-react-app/src/components/KsqlDb/Query/renderer/TableRenderer/TableRenderer.tsx @@ -6,13 +6,11 @@ import { TableTitle } from 'components/common/table/TableTitle/TableTitle.styled import * as S from './TableRenderer.styled'; -export interface Props { +interface TableRendererProps { table: KsqlTableResponse; } -export function hasJsonStructure( - str: string | Record -): boolean { +function hasJsonStructure(str: string | Record): boolean { if (typeof str === 'object') { return true; } @@ -30,13 +28,7 @@ export function hasJsonStructure( return false; } -const TableRenderer: React.FC = ({ table }) => { - const heading = React.useMemo(() => { - return table.header || ''; - }, [table.header]); - const ths = React.useMemo(() => { - return table.columnNames || []; - }, [table.columnNames]); +const TableRenderer: React.FC = ({ table }) => { const rows = React.useMemo(() => { return (table.values || []).map((row) => { return { @@ -53,9 +45,11 @@ const TableRenderer: React.FC = ({ table }) => { }); }, [table.values]); + const ths = table.columnNames || []; + return ( - {heading} + {table.header} @@ -73,7 +67,7 @@ const TableRenderer: React.FC = ({ table }) => { rows.map((row) => ( {row.cells.map((cell) => ( - {cell.value} + {cell.value.toString()} ))} )) diff --git a/kafka-ui-react-app/src/components/KsqlDb/Query/renderer/TableRenderer/__test__/TableRenderer.spec.tsx b/kafka-ui-react-app/src/components/KsqlDb/Query/renderer/TableRenderer/__test__/TableRenderer.spec.tsx deleted file mode 100644 index 775b16b51a9..00000000000 --- a/kafka-ui-react-app/src/components/KsqlDb/Query/renderer/TableRenderer/__test__/TableRenderer.spec.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { render } from 'lib/testHelpers'; -import React from 'react'; -import TableRenderer, { - Props, - hasJsonStructure, -} from 'components/KsqlDb/Query/renderer/TableRenderer/TableRenderer'; -import { screen } from '@testing-library/dom'; - -const renderComponent = (props: Props) => render(); - -describe('TableRenderer', () => { - it('renders', () => { - renderComponent({ - table: { - header: 'Test header', - columnNames: ['Test column name'], - values: [['Table row #1'], ['Table row #2'], ['{"jsonrow": "#3"}']], - }, - }); - - expect( - screen.getByRole('heading', { name: 'Test header' }) - ).toBeInTheDocument(); - expect( - screen.getByRole('columnheader', { name: 'Test column name' }) - ).toBeInTheDocument(); - expect( - screen.getByRole('cell', { name: 'Table row #1' }) - ).toBeInTheDocument(); - expect( - screen.getByRole('cell', { name: 'Table row #2' }) - ).toBeInTheDocument(); - }); - - it('renders with empty arrays', () => { - renderComponent({ - table: {}, - }); - - expect(screen.getByText('No tables or streams found')).toBeInTheDocument(); - }); -}); - -describe('hasJsonStructure', () => { - it('works', () => { - expect(hasJsonStructure('simplestring')).toBeFalsy(); - expect( - hasJsonStructure("{'looksLikeJson': 'but has wrong quotes'}") - ).toBeFalsy(); - expect( - hasJsonStructure('{"json": "but doesnt have closing brackets"') - ).toBeFalsy(); - expect(hasJsonStructure('"string":"that looks like json"')).toBeFalsy(); - - expect(hasJsonStructure('1')).toBeFalsy(); - expect(hasJsonStructure('{1:}')).toBeFalsy(); - expect(hasJsonStructure('{1:"1"}')).toBeFalsy(); - - // @ts-expect-error We suppress error because this function works with unknown data from server - expect(hasJsonStructure(1)).toBeFalsy(); - - expect(hasJsonStructure('{}')).toBeTruthy(); - expect(hasJsonStructure('{"correct": "json"}')).toBeTruthy(); - - expect(hasJsonStructure('[]')).toBeTruthy(); - expect(hasJsonStructure('[{}]')).toBeTruthy(); - - expect(hasJsonStructure({})).toBeTruthy(); - expect(hasJsonStructure({ correct: 'json' })).toBeTruthy(); - }); -}); diff --git a/kafka-ui-react-app/src/components/KsqlDb/TableView.tsx b/kafka-ui-react-app/src/components/KsqlDb/TableView.tsx new file mode 100644 index 00000000000..538345954da --- /dev/null +++ b/kafka-ui-react-app/src/components/KsqlDb/TableView.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { KsqlStreamDescription, KsqlTableDescription } from 'generated-sources'; +import Table from 'components/common/NewTable'; +import { ColumnDef } from '@tanstack/react-table'; + +interface TableViewProps { + fetching: boolean; + rows: KsqlTableDescription[] | KsqlStreamDescription[]; +} + +const TableView: React.FC = ({ fetching, rows }) => { + const columns = React.useMemo< + ColumnDef[] + >( + () => [ + { header: 'Name', accessorKey: 'name' }, + { header: 'Topic', accessorKey: 'topic' }, + { header: 'Key Format', accessorKey: 'keyFormat' }, + { header: 'Value Format', accessorKey: 'valueFormat' }, + { + header: 'Is Windowed', + accessorKey: 'isWindowed', + cell: ({ row }) => + 'isWindowed' in row.original ? String(row.original.isWindowed) : '-', + }, + ], + [] + ); + return ( + + ); +}; + +export default TableView; diff --git a/kafka-ui-react-app/src/components/KsqlDb/__test__/KsqlDb.spec.tsx b/kafka-ui-react-app/src/components/KsqlDb/__test__/KsqlDb.spec.tsx deleted file mode 100644 index b07a3936da7..00000000000 --- a/kafka-ui-react-app/src/components/KsqlDb/__test__/KsqlDb.spec.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import KsqlDb from 'components/KsqlDb/KsqlDb'; -import { render, WithRoute } from 'lib/testHelpers'; -import { screen } from '@testing-library/dom'; -import { - clusterKsqlDbPath, - clusterKsqlDbQueryPath, - getNonExactPath, -} from 'lib/paths'; - -const KSqLComponentText = { - list: 'list', - query: 'query', -}; - -jest.mock('components/KsqlDb/List/List', () => () => ( -
{KSqLComponentText.list}
-)); -jest.mock('components/KsqlDb/Query/Query', () => () => ( -
{KSqLComponentText.query}
-)); - -describe('KsqlDb Component', () => { - const clusterName = 'clusterName'; - const renderComponent = (path: string) => - render( - - - , - { initialEntries: [path] } - ); - - it('Renders the List', () => { - renderComponent(clusterKsqlDbPath(clusterName)); - expect(screen.getByText(KSqLComponentText.list)).toBeInTheDocument(); - }); - - it('Renders the List', () => { - renderComponent(clusterKsqlDbQueryPath(clusterName)); - expect(screen.getByText(KSqLComponentText.query)).toBeInTheDocument(); - }); -}); diff --git a/kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx b/kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx index 95043b99641..54f4a8f5fc4 100644 --- a/kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx +++ b/kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx @@ -7,6 +7,7 @@ import { clusterSchemasPath, clusterConnectorsPath, clusterKsqlDbPath, + clusterACLPath, } from 'lib/paths'; import ClusterMenuItem from './ClusterMenuItem'; @@ -27,7 +28,7 @@ const ClusterMenu: React.FC = ({ const [isOpen, setIsOpen] = React.useState(!!singleMode); return ( - +
= ({ to={clusterConsumerGroupsPath(name)} title="Consumers" /> - {hasFeatureConfigured(ClusterFeaturesEnum.SCHEMA_REGISTRY) && ( = ({ {hasFeatureConfigured(ClusterFeaturesEnum.KSQL_DB) && ( )} + {(hasFeatureConfigured(ClusterFeaturesEnum.KAFKA_ACL_VIEW) || + hasFeatureConfigured(ClusterFeaturesEnum.KAFKA_ACL_EDIT)) && ( + + )}
)} diff --git a/kafka-ui-react-app/src/components/Nav/ClusterTab/ClusterTab.styled.ts b/kafka-ui-react-app/src/components/Nav/ClusterTab/ClusterTab.styled.ts index 9b61878bba1..5ef2467889c 100644 --- a/kafka-ui-react-app/src/components/Nav/ClusterTab/ClusterTab.styled.ts +++ b/kafka-ui-react-app/src/components/Nav/ClusterTab/ClusterTab.styled.ts @@ -36,6 +36,7 @@ export const Title = styled.div` max-width: 110px; overflow: hidden; text-overflow: ellipsis; + color: ${({ theme }) => theme.menu.titleColor}; `; export const StatusIconWrapper = styled.svg.attrs({ diff --git a/kafka-ui-react-app/src/components/Nav/ClusterTab/__tests__/ClusterTab.styled.spec.tsx b/kafka-ui-react-app/src/components/Nav/ClusterTab/__tests__/ClusterTab.styled.spec.tsx index 68ff3b5efe9..c3bdf8b5821 100644 --- a/kafka-ui-react-app/src/components/Nav/ClusterTab/__tests__/ClusterTab.styled.spec.tsx +++ b/kafka-ui-react-app/src/components/Nav/ClusterTab/__tests__/ClusterTab.styled.spec.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { render } from 'lib/testHelpers'; -import theme from 'theme/theme'; +import { theme } from 'theme/theme'; import { screen } from '@testing-library/react'; import * as S from 'components/Nav/ClusterTab/ClusterTab.styled'; import { ServerStatus } from 'generated-sources'; diff --git a/kafka-ui-react-app/src/components/Nav/Nav.styled.ts b/kafka-ui-react-app/src/components/Nav/Nav.styled.ts index 6cce7c43654..36e9f6454bb 100644 --- a/kafka-ui-react-app/src/components/Nav/Nav.styled.ts +++ b/kafka-ui-react-app/src/components/Nav/Nav.styled.ts @@ -9,11 +9,6 @@ export const List = styled.ul.attrs({ role: 'menu' })` } `; -export const Divider = styled.hr` - margin: 0; - height: 1px; -`; - export const Link = styled(NavLink)( ({ theme }) => css` width: 100%; diff --git a/kafka-ui-react-app/src/components/Nav/Nav.tsx b/kafka-ui-react-app/src/components/Nav/Nav.tsx index a21a104ae8d..45c755d32d1 100644 --- a/kafka-ui-react-app/src/components/Nav/Nav.tsx +++ b/kafka-ui-react-app/src/components/Nav/Nav.tsx @@ -1,30 +1,28 @@ +import { useClusters } from 'lib/hooks/api/clusters'; import React from 'react'; -import { Cluster } from 'generated-sources'; import ClusterMenu from './ClusterMenu'; import ClusterMenuItem from './ClusterMenuItem'; import * as S from './Nav.styled'; -interface Props { - areClustersFulfilled?: boolean; - clusters: Cluster[]; -} +const Nav: React.FC = () => { + const clusters = useClusters(); -const Nav: React.FC = ({ areClustersFulfilled, clusters }) => ( - -); + return ( + + ); +}; export default Nav; diff --git a/kafka-ui-react-app/src/components/Nav/__tests__/ClusterMenu.spec.tsx b/kafka-ui-react-app/src/components/Nav/__tests__/ClusterMenu.spec.tsx index f817a0c6d29..22bc1eabf54 100644 --- a/kafka-ui-react-app/src/components/Nav/__tests__/ClusterMenu.spec.tsx +++ b/kafka-ui-react-app/src/components/Nav/__tests__/ClusterMenu.spec.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { screen } from '@testing-library/react'; import { Cluster, ClusterFeaturesEnum } from 'generated-sources'; -import { onlineClusterPayload } from 'redux/reducers/clusters/__test__/fixtures'; import ClusterMenu from 'components/Nav/ClusterMenu'; import userEvent from '@testing-library/user-event'; import { clusterConnectorsPath } from 'lib/paths'; import { render } from 'lib/testHelpers'; +import { onlineClusterPayload } from 'lib/fixtures/clusters'; describe('ClusterMenu', () => { const setupComponent = (cluster: Cluster, singleMode?: boolean) => ( @@ -19,19 +19,19 @@ describe('ClusterMenu', () => { const getKafkaConnect = () => screen.getByTitle('Kafka Connect'); const getCluster = () => screen.getByText(onlineClusterPayload.name); - it('renders cluster menu with default set of features', () => { + it('renders cluster menu with default set of features', async () => { render(setupComponent(onlineClusterPayload)); expect(getCluster()).toBeInTheDocument(); expect(getMenuItems().length).toEqual(1); - userEvent.click(getMenuItem()); + await userEvent.click(getMenuItem()); expect(getMenuItems().length).toEqual(4); expect(getBrokers()).toBeInTheDocument(); expect(getTopics()).toBeInTheDocument(); expect(getConsumers()).toBeInTheDocument(); }); - it('renders cluster menu with correct set of features', () => { + it('renders cluster menu with correct set of features', async () => { render( setupComponent({ ...onlineClusterPayload, @@ -43,7 +43,7 @@ describe('ClusterMenu', () => { }) ); expect(getMenuItems().length).toEqual(1); - userEvent.click(getMenuItem()); + await userEvent.click(getMenuItem()); expect(getMenuItems().length).toEqual(7); expect(getBrokers()).toBeInTheDocument(); @@ -64,7 +64,7 @@ describe('ClusterMenu', () => { expect(getTopics()).toBeInTheDocument(); expect(getConsumers()).toBeInTheDocument(); }); - it('makes Kafka Connect link active', () => { + it('makes Kafka Connect link active', async () => { render( setupComponent({ ...onlineClusterPayload, @@ -73,7 +73,7 @@ describe('ClusterMenu', () => { { initialEntries: [clusterConnectorsPath(onlineClusterPayload.name)] } ); expect(getMenuItems().length).toEqual(1); - userEvent.click(getMenuItem()); + await userEvent.click(getMenuItem()); expect(getMenuItems().length).toEqual(5); const kafkaConnect = getKafkaConnect(); diff --git a/kafka-ui-react-app/src/components/Nav/__tests__/Nav.spec.tsx b/kafka-ui-react-app/src/components/Nav/__tests__/Nav.spec.tsx index 1ef18720e32..582c3414119 100644 --- a/kafka-ui-react-app/src/components/Nav/__tests__/Nav.spec.tsx +++ b/kafka-ui-react-app/src/components/Nav/__tests__/Nav.spec.tsx @@ -1,29 +1,39 @@ import React from 'react'; -import { - offlineClusterPayload, - onlineClusterPayload, -} from 'redux/reducers/clusters/__test__/fixtures'; import Nav from 'components/Nav/Nav'; import { screen } from '@testing-library/react'; import { render } from 'lib/testHelpers'; +import { Cluster } from 'generated-sources'; +import { useClusters } from 'lib/hooks/api/clusters'; +import { + offlineClusterPayload, + onlineClusterPayload, +} from 'lib/fixtures/clusters'; + +jest.mock('lib/hooks/api/clusters', () => ({ + useClusters: jest.fn(), +})); describe('Nav', () => { + const renderComponent = (payload: Cluster[] = []) => { + (useClusters as jest.Mock).mockImplementation(() => ({ + data: payload, + isSuccess: true, + })); + render(
- - - - - - - - - {versions.map((version) => ( - - ))} - {versions.length === 0 && ( - - - - )} - -
No active Schema
+ true} + renderSubComponent={SchemaVersion} + enableSorting + /> ) : ( )} diff --git a/kafka-ui-react-app/src/components/Schemas/Details/LatestVersion/LatestVersionItem.styled.tsx b/kafka-ui-react-app/src/components/Schemas/Details/LatestVersion/LatestVersionItem.styled.tsx index 076f2ccbd33..5c960e0f48b 100644 --- a/kafka-ui-react-app/src/components/Schemas/Details/LatestVersion/LatestVersionItem.styled.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Details/LatestVersion/LatestVersionItem.styled.tsx @@ -1,11 +1,10 @@ import Heading from 'components/common/heading/Heading.styled'; import React from 'react'; import styled from 'styled-components'; -import theme from 'theme/theme'; export const Wrapper = styled.div` width: 100%; - background-color: ${theme.layout.stuffColor}; + background-color: ${({ theme }) => theme.layout.stuffColor}; padding: 16px; display: flex; justify-content: center; @@ -14,7 +13,7 @@ export const Wrapper = styled.div` max-height: 700px; & > * { - background-color: ${theme.panelColor}; + background-color: ${({ theme }) => theme.default.backgroundColor}; padding: 24px; overflow-y: scroll; } @@ -33,12 +32,16 @@ export const Wrapper = styled.div` gap: 16px; padding-bottom: 16px; } + + p { + color: ${({ theme }) => theme.schema.backgroundColor.p}; + } } `; export const MetaDataLabel = styled((props) => ( ))` - color: ${theme.lastestVersionItem.metaDataLabel.color}; + color: ${({ theme }) => theme.lastestVersionItem.metaDataLabel.color}; width: 110px; `; diff --git a/kafka-ui-react-app/src/components/Schemas/Details/LatestVersion/LatestVersionItem.tsx b/kafka-ui-react-app/src/components/Schemas/Details/LatestVersion/LatestVersionItem.tsx index 4efa47d7fe7..21d443a68c8 100644 --- a/kafka-ui-react-app/src/components/Schemas/Details/LatestVersion/LatestVersionItem.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Details/LatestVersion/LatestVersionItem.tsx @@ -14,7 +14,7 @@ const LatestVersionItem: React.FC = ({ }) => (
- Relevant version + Actual version
@@ -26,6 +26,10 @@ const LatestVersionItem: React.FC = ({ ID

{id}

+
+ Type +

{schemaType}

+
Subject

{subject}

diff --git a/kafka-ui-react-app/src/components/Schemas/Details/SchemaVersion/SchemaVersion.styled.ts b/kafka-ui-react-app/src/components/Schemas/Details/SchemaVersion/SchemaVersion.styled.ts deleted file mode 100644 index fad9ee37849..00000000000 --- a/kafka-ui-react-app/src/components/Schemas/Details/SchemaVersion/SchemaVersion.styled.ts +++ /dev/null @@ -1,13 +0,0 @@ -import styled from 'styled-components'; - -export const Wrapper = styled.tr` - background-color: ${({ theme }) => theme.schema.backgroundColor.tr}; - & > td { - padding: 16px !important; - & > div { - background-color: ${({ theme }) => theme.schema.backgroundColor.div}; - border-radius: 8px; - padding: 24px; - } - } -`; diff --git a/kafka-ui-react-app/src/components/Schemas/Details/SchemaVersion/SchemaVersion.tsx b/kafka-ui-react-app/src/components/Schemas/Details/SchemaVersion/SchemaVersion.tsx index 875627d452c..1470d3c22a9 100644 --- a/kafka-ui-react-app/src/components/Schemas/Details/SchemaVersion/SchemaVersion.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Details/SchemaVersion/SchemaVersion.tsx @@ -1,40 +1,18 @@ import React from 'react'; -import { SchemaSubject } from 'generated-sources'; -import MessageToggleIcon from 'components/common/Icons/MessageToggleIcon'; -import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper'; import EditorViewer from 'components/common/EditorViewer/EditorViewer'; +import { SchemaSubject } from 'generated-sources'; +import { Row } from '@tanstack/react-table'; -import * as S from './SchemaVersion.styled'; - -interface SchemaVersionProps { - version: SchemaSubject; +interface Props { + row: Row; } -const SchemaVersion: React.FC = ({ - version: { version, id, schema, schemaType }, -}) => { - const [isOpen, setIsOpen] = React.useState(false); - const toggleIsOpen = () => setIsOpen(!isOpen); - +const SchemaVersion: React.FC = ({ row }) => { return ( - <> -
- - - - - {isOpen && ( - - - - )} - + ); }; diff --git a/kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx b/kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx index fefcf99801f..3b58c376e93 100644 --- a/kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx @@ -2,10 +2,11 @@ import React from 'react'; import Details from 'components/Schemas/Details/Details'; import { render, WithRoute } from 'lib/testHelpers'; import { clusterSchemaPath } from 'lib/paths'; -import { screen, waitFor } from '@testing-library/dom'; +import { screen } from '@testing-library/dom'; import { schemasInitialState, schemaVersion, + schemaVersionWithNonAsciiChars, } from 'redux/reducers/schemas/__test__/fixtures'; import fetchMock from 'fetch-mock'; import ClusterContext, { @@ -21,6 +22,12 @@ const clusterName = 'testClusterName'; const schemasAPILatestUrl = `/api/clusters/${clusterName}/schemas/${schemaVersion.subject}/latest`; const schemasAPIVersionsUrl = `/api/clusters/${clusterName}/schemas/${schemaVersion.subject}/versions`; +const mockHistoryPush = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockHistoryPush, +})); + const renderComponent = ( initialState: RootState['schemas'] = schemasInitialState, context: ContextProps = contextInitialValue @@ -43,7 +50,7 @@ describe('Details', () => { afterEach(() => fetchMock.reset()); describe('fetch failed', () => { - beforeEach(async () => { + it('renders pageloader', async () => { const schemasAPILatestMock = fetchMock.getOnce(schemasAPILatestUrl, 404); const schemasAPIVersionsMock = fetchMock.getOnce( schemasAPIVersionsUrl, @@ -52,16 +59,8 @@ describe('Details', () => { await act(() => { renderComponent(); }); - - await waitFor(() => { - expect(schemasAPILatestMock.called()).toBeTruthy(); - }); - await waitFor(() => { - expect(schemasAPIVersionsMock.called()).toBeTruthy(); - }); - }); - - it('renders pageloader', () => { + expect(schemasAPILatestMock.called(schemasAPILatestUrl)).toBeTruthy(); + expect(schemasAPIVersionsMock.called(schemasAPIVersionsUrl)).toBeTruthy(); expect(screen.getByRole('progressbar')).toBeInTheDocument(); expect(screen.queryByText(schemaVersion.subject)).not.toBeInTheDocument(); expect(screen.queryByText('Edit Schema')).not.toBeInTheDocument(); @@ -71,7 +70,7 @@ describe('Details', () => { describe('fetch success', () => { describe('has schema versions', () => { - beforeEach(async () => { + it('renders component with schema info', async () => { const schemasAPILatestMock = fetchMock.getOnce( schemasAPILatestUrl, schemaVersion @@ -83,21 +82,37 @@ describe('Details', () => { await act(() => { renderComponent(); }); - await waitFor(() => { - expect(schemasAPILatestMock.called()).toBeTruthy(); - }); - await waitFor(() => { - expect(schemasAPIVersionsMock.called()).toBeTruthy(); - }); - }); - - it('renders component with schema info', () => { + expect(schemasAPILatestMock.called()).toBeTruthy(); + expect(schemasAPIVersionsMock.called()).toBeTruthy(); expect(screen.getByText('Edit Schema')).toBeInTheDocument(); expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); expect(screen.getByRole('table')).toBeInTheDocument(); }); }); + describe('fetch success schema with non ascii characters', () => { + describe('has schema versions', () => { + it('renders component with schema info', async () => { + const schemasAPILatestMock = fetchMock.getOnce( + schemasAPILatestUrl, + schemaVersionWithNonAsciiChars + ); + const schemasAPIVersionsMock = fetchMock.getOnce( + schemasAPIVersionsUrl, + versionPayload + ); + await act(() => { + renderComponent(); + }); + expect(schemasAPILatestMock.called()).toBeTruthy(); + expect(schemasAPIVersionsMock.called()).toBeTruthy(); + expect(screen.getByText('Edit Schema')).toBeInTheDocument(); + expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); + expect(screen.getByRole('table')).toBeInTheDocument(); + }); + }); + }); + describe('empty schema versions', () => { beforeEach(async () => { const schemasAPILatestMock = fetchMock.getOnce( @@ -111,18 +126,13 @@ describe('Details', () => { await act(() => { renderComponent(); }); - await waitFor(() => { - expect(schemasAPILatestMock.called()).toBeTruthy(); - }); - await waitFor(() => { - expect(schemasAPIVersionsMock.called()).toBeTruthy(); - }); + expect(schemasAPILatestMock.called()).toBeTruthy(); + expect(schemasAPIVersionsMock.called()).toBeTruthy(); }); // seems like incorrect behaviour it('renders versions table with 0 items', () => { expect(screen.getByRole('table')).toBeInTheDocument(); - expect(screen.getByText('No active Schema')).toBeInTheDocument(); }); }); }); diff --git a/kafka-ui-react-app/src/components/Schemas/Details/__test__/LatestVersionItem.spec.tsx b/kafka-ui-react-app/src/components/Schemas/Details/__test__/LatestVersionItem.spec.tsx index 9962bd2e966..e4d70548cad 100644 --- a/kafka-ui-react-app/src/components/Schemas/Details/__test__/LatestVersionItem.spec.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Details/__test__/LatestVersionItem.spec.tsx @@ -8,7 +8,7 @@ import { jsonSchema, protoSchema } from './fixtures'; describe('LatestVersionItem', () => { it('renders latest version of json schema', () => { render(); - expect(screen.getByText('Relevant version')).toBeInTheDocument(); + expect(screen.getByText('Actual version')).toBeInTheDocument(); expect(screen.getByText('Latest version')).toBeInTheDocument(); expect(screen.getByText('ID')).toBeInTheDocument(); expect(screen.getByText('Subject')).toBeInTheDocument(); @@ -18,7 +18,7 @@ describe('LatestVersionItem', () => { it('renders latest version of compatibility', () => { render(); - expect(screen.getByText('Relevant version')).toBeInTheDocument(); + expect(screen.getByText('Actual version')).toBeInTheDocument(); expect(screen.getByText('Latest version')).toBeInTheDocument(); expect(screen.getByText('ID')).toBeInTheDocument(); expect(screen.getByText('Subject')).toBeInTheDocument(); diff --git a/kafka-ui-react-app/src/components/Schemas/Details/__test__/SchemaVersion.spec.tsx b/kafka-ui-react-app/src/components/Schemas/Details/__test__/SchemaVersion.spec.tsx index 617c8cc4259..564c5dfa29a 100644 --- a/kafka-ui-react-app/src/components/Schemas/Details/__test__/SchemaVersion.spec.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Details/__test__/SchemaVersion.spec.tsx @@ -1,24 +1,21 @@ import React from 'react'; import SchemaVersion from 'components/Schemas/Details/SchemaVersion/SchemaVersion'; import { render } from 'lib/testHelpers'; -import { screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { SchemaSubject } from 'generated-sources'; +import { Row } from '@tanstack/react-table'; -import { versions } from './fixtures'; +import { jsonSchema } from './fixtures'; -const component = ( -
- - - - {version}{id}
- -
- - - -
-); +const renderComponent = () => { + const row = { + original: jsonSchema, + }; + + return render(} />); +}; describe('SchemaVersion', () => { - it('renders versions', () => { - render(component); - expect(screen.getAllByRole('cell')).toHaveLength(3); - expect(screen.queryByTestId('json-viewer')).not.toBeInTheDocument(); - userEvent.click(screen.getByRole('button')); + it('renders versions', async () => { + renderComponent(); }); }); diff --git a/kafka-ui-react-app/src/components/Schemas/Details/__test__/fixtures.ts b/kafka-ui-react-app/src/components/Schemas/Details/__test__/fixtures.ts index 6443dfd541b..18cca3b1008 100644 --- a/kafka-ui-react-app/src/components/Schemas/Details/__test__/fixtures.ts +++ b/kafka-ui-react-app/src/components/Schemas/Details/__test__/fixtures.ts @@ -2,13 +2,16 @@ import { SchemaSubject, SchemaType } from 'generated-sources'; import { schemaVersion1, schemaVersion2, + schemaVersionWithNonAsciiChars, } from 'redux/reducers/schemas/__test__/fixtures'; -export const versionPayload = [schemaVersion1, schemaVersion2]; +export const versionPayload = [ + schemaVersion1, + schemaVersion2, + schemaVersionWithNonAsciiChars, +]; export const versionEmptyPayload = []; -export const versions = [schemaVersion1, schemaVersion2]; - export const jsonSchema: SchemaSubject = { subject: 'test', version: '15', diff --git a/kafka-ui-react-app/src/components/Schemas/Diff/Diff.styled.ts b/kafka-ui-react-app/src/components/Schemas/Diff/Diff.styled.ts index 18f2ccac259..c5ecef258a0 100644 --- a/kafka-ui-react-app/src/components/Schemas/Diff/Diff.styled.ts +++ b/kafka-ui-react-app/src/components/Schemas/Diff/Diff.styled.ts @@ -1,4 +1,5 @@ import styled from 'styled-components'; +import { Button } from 'components/common/Button/Button'; export const DiffWrapper = styled.div` align-items: stretch; @@ -8,15 +9,37 @@ export const DiffWrapper = styled.div` flex-shrink: 1; min-height: min-content; padding-top: 1.5rem !important; - & - .ace_editor - > .ace_scroller - > .ace_content - > .ace_marker-layer - > .codeMarker { - background: ${({ theme }) => theme.icons.warningIcon}; + + .ace_content { + background-color: ${({ theme }) => theme.default.backgroundColor}; + color: ${({ theme }) => theme.default.color.normal}; + } + .ace_gutter-cell { + background-color: ${({ theme }) => + theme.ksqlDb.query.editor.cell.backgroundColor}; + } + .ace_gutter-layer { + background-color: ${({ theme }) => + theme.ksqlDb.query.editor.layer.backgroundColor}; + color: ${({ theme }) => theme.default.color.normal}; + } + .ace_cursor { + color: ${({ theme }) => theme.ksqlDb.query.editor.cursor}; + } + + .ace_print-margin { + display: none; + } + .ace_variable { + color: ${({ theme }) => theme.ksqlDb.query.editor.variable}; + } + .ace_string { + color: ${({ theme }) => theme.ksqlDb.query.editor.aceString}; + } + .codeMarker { + background-color: ${({ theme }) => theme.ksqlDb.query.editor.codeMarker}; position: absolute; - z-index: 20; + z-index: 2000; } `; @@ -56,3 +79,6 @@ export const DiffTile = styled.div` export const DiffVersionsSelect = styled.div` width: 0.625em; `; +export const BackButton = styled(Button)` + margin: 10px 9px; +`; diff --git a/kafka-ui-react-app/src/components/Schemas/Diff/Diff.tsx b/kafka-ui-react-app/src/components/Schemas/Diff/Diff.tsx index 4c12bc66fa4..05b1373ab60 100644 --- a/kafka-ui-react-app/src/components/Schemas/Diff/Diff.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Diff/Diff.tsx @@ -1,6 +1,10 @@ import React from 'react'; import { SchemaSubject } from 'generated-sources'; -import { clusterSchemaSchemaDiffPath, ClusterSubjectParam } from 'lib/paths'; +import { + clusterSchemaComparePath, + clusterSchemasPath, + ClusterSubjectParam, +} from 'lib/paths'; import PageLoader from 'components/common/PageLoader/PageLoader'; import DiffViewer from 'components/common/DiffViewer/DiffViewer'; import { useNavigate, useLocation } from 'react-router-dom'; @@ -13,8 +17,10 @@ import Select from 'components/common/Select/Select'; import { useAppDispatch } from 'lib/hooks/redux'; import { resetLoaderById } from 'redux/reducers/loader/loaderSlice'; import useAppParams from 'lib/hooks/useAppParams'; +import PageHeading from 'components/common/PageHeading/PageHeading'; import * as S from './Diff.styled'; +import { BackButton } from './Diff.styled'; export interface DiffProps { versions: SchemaSubject[]; @@ -66,109 +72,127 @@ const Diff: React.FC = ({ versions, areVersionsFetched }) => { } = methods; return ( - - {areVersionsFetched ? ( - - - - - ( - { + navigate( + clusterSchemaComparePath(clusterName, subject) + ); + searchParams.set('leftVersion', event.toString()); + searchParams.set( + 'rightVersion', + rightVersion === '' + ? versions[0].version + : rightVersion + ); + navigate({ + search: `?${searchParams.toString()}`, + }); + setLeftVersion(event.toString()); + }} + minWidth="100%" + disabled={isSubmitting} + options={versions.map((type) => ({ + value: type.version, + label: `Version ${type.version}`, + }))} + /> + )} + /> + + + + + ( + { - navigate( - clusterSchemaSchemaDiffPath(clusterName, subject) - ); - searchParams.set( - 'leftVersion', - leftVersion === '' ? versions[0].version : leftVersion - ); - searchParams.set('rightVersion', event.toString()); - navigate({ - search: `?${searchParams.toString()}`, - }); - setRightVersion(event.toString()); - }} - minWidth="100%" - disabled={isSubmitting} - options={versions.map((type) => ({ - value: type.version, - label: `Version ${type.version}`, - }))} - /> - )} - /> - - - - - - - - ) : ( - - )} - + } + onChange={(event) => { + navigate( + clusterSchemaComparePath(clusterName, subject) + ); + searchParams.set( + 'leftVersion', + leftVersion === '' + ? versions[0].version + : leftVersion + ); + searchParams.set('rightVersion', event.toString()); + navigate({ + search: `?${searchParams.toString()}`, + }); + setRightVersion(event.toString()); + }} + minWidth="100%" + disabled={isSubmitting} + options={versions.map((type) => ({ + value: type.version, + label: `Version ${type.version}`, + }))} + /> + )} + /> + + + + + + + + ) : ( + + )} + + ); }; diff --git a/kafka-ui-react-app/src/components/Schemas/Diff/__test__/Diff.spec.tsx b/kafka-ui-react-app/src/components/Schemas/Diff/__test__/Diff.spec.tsx index 418393bb503..2a9429eef1c 100644 --- a/kafka-ui-react-app/src/components/Schemas/Diff/__test__/Diff.spec.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Diff/__test__/Diff.spec.tsx @@ -2,13 +2,14 @@ import React from 'react'; import Diff, { DiffProps } from 'components/Schemas/Diff/Diff'; import { render, WithRoute } from 'lib/testHelpers'; import { screen } from '@testing-library/react'; -import { clusterSchemaSchemaDiffPath } from 'lib/paths'; +import { clusterSchemaComparePath } from 'lib/paths'; +import userEvent from '@testing-library/user-event'; import { versions } from './fixtures'; const defaultClusterName = 'defaultClusterName'; const defaultSubject = 'defaultSubject'; -const defaultPathName = clusterSchemaSchemaDiffPath( +const defaultPathName = clusterSchemaComparePath( defaultClusterName, defaultSubject ); @@ -30,7 +31,7 @@ describe('Diff', () => { pathname = `${pathname}?${searchParams.toString()}`; return render( - + { expect(select).toHaveTextContent(versions[0].version); }); }); + + describe('Back button', () => { + beforeEach(() => { + setupComponent({ + areVersionsFetched: true, + versions, + }); + }); + + it('back button is appear', () => { + const backButton = screen.getAllByRole('button', { name: 'Back' }); + expect(backButton[0]).toBeInTheDocument(); + }); + + it('click on back button', () => { + const backButton = screen.getAllByRole('button', { name: 'Back' }); + userEvent.click(backButton[0]); + expect(screen.queryByRole('Back')).not.toBeInTheDocument(); + }); + }); }); diff --git a/kafka-ui-react-app/src/components/Schemas/Edit/Edit.styled.ts b/kafka-ui-react-app/src/components/Schemas/Edit/Edit.styled.ts index fd1efe29e60..537119a3b8c 100644 --- a/kafka-ui-react-app/src/components/Schemas/Edit/Edit.styled.ts +++ b/kafka-ui-react-app/src/components/Schemas/Edit/Edit.styled.ts @@ -2,7 +2,7 @@ import styled, { css } from 'styled-components'; export const EditWrapper = styled.div` padding: 16px; - padding-top: 0px; + padding-top: 0; & > form { display: flex; flex-direction: column; @@ -44,6 +44,7 @@ export const EditorContainer = styled.div( font-size: 16px; line-height: 24px; padding-bottom: 16px; + color: ${theme.heading.h4}; } ` ); diff --git a/kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx b/kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx index 625da9f88e5..bddb2f173f9 100644 --- a/kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx @@ -1,46 +1,22 @@ import React from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useForm, Controller, FormProvider } from 'react-hook-form'; -import { - CompatibilityLevelCompatibilityEnum, - SchemaType, -} from 'generated-sources'; -import { clusterSchemaPath, ClusterSubjectParam } from 'lib/paths'; -import { NewSchemaSubjectRaw } from 'redux/interfaces'; -import Editor from 'components/common/Editor/Editor'; -import Select from 'components/common/Select/Select'; -import { Button } from 'components/common/Button/Button'; -import { InputLabel } from 'components/common/Input/InputLabel.styled'; -import PageHeading from 'components/common/PageHeading/PageHeading'; +import { ClusterSubjectParam } from 'lib/paths'; import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; import useAppParams from 'lib/hooks/useAppParams'; import { - schemaAdded, - schemasApiClient, fetchLatestSchema, getSchemaLatest, SCHEMA_LATEST_FETCH_ACTION, getAreSchemaLatestFulfilled, - schemaUpdated, } from 'redux/reducers/schemas/schemasSlice'; -import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice'; -import { getResponse } from 'lib/errorHandling'; import PageLoader from 'components/common/PageLoader/PageLoader'; import { resetLoaderById } from 'redux/reducers/loader/loaderSlice'; -import * as S from './Edit.styled'; +import Form from './Form'; const Edit: React.FC = () => { - const navigate = useNavigate(); const dispatch = useAppDispatch(); const { clusterName, subject } = useAppParams(); - const methods = useForm({ mode: 'onChange' }); - const { - formState: { isDirty, isSubmitting, dirtyFields }, - control, - handleSubmit, - } = methods; React.useEffect(() => { dispatch(fetchLatestSchema({ clusterName, subject })); @@ -52,152 +28,10 @@ const Edit: React.FC = () => { const schema = useAppSelector((state) => getSchemaLatest(state)); const isFetched = useAppSelector(getAreSchemaLatestFulfilled); - const formatedSchema = React.useMemo(() => { - return schema?.schemaType === SchemaType.PROTOBUF - ? schema?.schema - : JSON.stringify(JSON.parse(schema?.schema || '{}'), null, '\t'); - }, [schema]); - - const onSubmit = async (props: NewSchemaSubjectRaw) => { - if (!schema) return; - - try { - if (dirtyFields.newSchema || dirtyFields.schemaType) { - const resp = await schemasApiClient.createNewSchema({ - clusterName, - newSchemaSubject: { - ...schema, - schema: props.newSchema || schema.schema, - schemaType: props.schemaType || schema.schemaType, - }, - }); - dispatch(schemaAdded(resp)); - } - - if (dirtyFields.compatibilityLevel) { - await schemasApiClient.updateSchemaCompatibilityLevel({ - clusterName, - subject, - compatibilityLevel: { - compatibility: props.compatibilityLevel, - }, - }); - dispatch( - schemaUpdated({ - ...schema, - compatibilityLevel: props.compatibilityLevel, - }) - ); - } - - navigate(clusterSchemaPath(clusterName, subject)); - } catch (e) { - const err = await getResponse(e as Response); - dispatch(serverErrorAlertAdded(err)); - } - }; - if (!isFetched || !schema) { return ; } - return ( - - - -
-
-
- Type - ( - ({ value: level, label: level }))} - /> - )} - /> -
-
- -
- -

Latest schema

- -
-
-
- -

New schema

- ( - - )} - /> -
- -
-
-
-
-
- ); + return
; }; export default Edit; diff --git a/kafka-ui-react-app/src/components/Schemas/Edit/Form.tsx b/kafka-ui-react-app/src/components/Schemas/Edit/Form.tsx new file mode 100644 index 00000000000..56d7bdc8175 --- /dev/null +++ b/kafka-ui-react-app/src/components/Schemas/Edit/Form.tsx @@ -0,0 +1,224 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useForm, Controller, FormProvider } from 'react-hook-form'; +import { + CompatibilityLevelCompatibilityEnum, + SchemaType, +} from 'generated-sources'; +import { + clusterSchemaPath, + clusterSchemasPath, + ClusterSubjectParam, +} from 'lib/paths'; +import yup from 'lib/yupExtended'; +import { NewSchemaSubjectRaw } from 'redux/interfaces'; +import Editor from 'components/common/Editor/Editor'; +import Select from 'components/common/Select/Select'; +import { Button } from 'components/common/Button/Button'; +import { InputLabel } from 'components/common/Input/InputLabel.styled'; +import PageHeading from 'components/common/PageHeading/PageHeading'; +import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; +import useAppParams from 'lib/hooks/useAppParams'; +import { + schemaAdded, + getSchemaLatest, + getAreSchemaLatestFulfilled, + schemaUpdated, + getAreSchemaLatestRejected, +} from 'redux/reducers/schemas/schemasSlice'; +import PageLoader from 'components/common/PageLoader/PageLoader'; +import { schemasApiClient } from 'lib/api'; +import { showServerError } from 'lib/errorHandling'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { FormError } from 'components/common/Input/Input.styled'; +import { ErrorMessage } from '@hookform/error-message'; + +import * as S from './Edit.styled'; + +const Form: React.FC = () => { + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + + const { clusterName, subject } = useAppParams(); + + const schema = useAppSelector((state) => getSchemaLatest(state)); + const isFetched = useAppSelector(getAreSchemaLatestFulfilled); + const isRejected = useAppSelector(getAreSchemaLatestRejected); + + const formatedSchema = React.useMemo(() => { + return schema?.schemaType === SchemaType.PROTOBUF + ? schema?.schema + : JSON.stringify(JSON.parse(schema?.schema || '{}'), null, '\t'); + }, [schema]); + + const validationSchema = () => + yup.object().shape({ + newSchema: + schema?.schemaType === SchemaType.PROTOBUF + ? yup.string().required() + : yup.string().required().isJsonObject('Schema syntax is not valid'), + }); + const methods = useForm({ + mode: 'onChange', + resolver: yupResolver(validationSchema()), + defaultValues: { + schemaType: schema?.schemaType, + compatibilityLevel: + schema?.compatibilityLevel as CompatibilityLevelCompatibilityEnum, + newSchema: formatedSchema, + }, + }); + + const { + formState: { isDirty, isSubmitting, dirtyFields, errors }, + control, + handleSubmit, + } = methods; + const onSubmit = async (props: NewSchemaSubjectRaw) => { + if (!schema) return; + + try { + if (dirtyFields.compatibilityLevel) { + await schemasApiClient.updateSchemaCompatibilityLevel({ + clusterName, + subject, + compatibilityLevel: { + compatibility: props.compatibilityLevel, + }, + }); + dispatch( + schemaUpdated({ + ...schema, + compatibilityLevel: props.compatibilityLevel, + }) + ); + } + if (dirtyFields.newSchema || dirtyFields.schemaType) { + const resp = await schemasApiClient.createNewSchema({ + clusterName, + newSchemaSubject: { + ...schema, + schema: props.newSchema || schema.schema, + schemaType: props.schemaType || schema.schemaType, + }, + }); + dispatch(schemaAdded(resp)); + } + + navigate(clusterSchemaPath(clusterName, subject)); + } catch (e) { + showServerError(e as Response); + } + }; + + if (isRejected) { + navigate('/404'); + } + + if (!isFetched || !schema) { + return ; + } + return ( + + + + +
+
+ Type + ( + ({ value: level, label: level }))} + /> + )} + /> +
+
+ +
+ +

Latest schema

+ +
+
+
+ +

New schema

+ ( + + )} + /> +
+ + + + +
+
+ +
+
+ ); +}; + +export default Form; diff --git a/kafka-ui-react-app/src/components/Schemas/Edit/__tests__/Edit.spec.tsx b/kafka-ui-react-app/src/components/Schemas/Edit/__tests__/Edit.spec.tsx index ae4e74d7a9e..618a0e6c302 100644 --- a/kafka-ui-react-app/src/components/Schemas/Edit/__tests__/Edit.spec.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Edit/__tests__/Edit.spec.tsx @@ -5,8 +5,9 @@ import { clusterSchemaEditPath } from 'lib/paths'; import { schemasInitialState, schemaVersion, + schemaVersionWithNonAsciiChars, } from 'redux/reducers/schemas/__test__/fixtures'; -import { screen, waitFor } from '@testing-library/dom'; +import { screen } from '@testing-library/dom'; import ClusterContext, { ContextProps, initialValue as contextInitialValue, @@ -41,30 +42,28 @@ const renderComponent = ( describe('Edit', () => { afterEach(() => fetchMock.reset()); - describe('fetch failed', () => { - it('renders page loader', async () => { - const schemasAPILatestMock = fetchMock.getOnce(schemasAPILatestUrl, 404); - await act(() => { - renderComponent(); + describe('fetch success', () => { + describe('has schema versions', () => { + it('renders component with schema info', async () => { + fetchMock.getOnce(schemasAPILatestUrl, schemaVersion); + await act(() => { + renderComponent(); + }); + expect(fetchMock.called(schemasAPILatestUrl)).toBeTruthy(); + expect(screen.getByText('Submit')).toBeInTheDocument(); + expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); }); - await waitFor(() => expect(schemasAPILatestMock.called()).toBeTruthy()); - expect(screen.getByRole('progressbar')).toBeInTheDocument(); - expect(screen.queryByText(schemaVersion.subject)).not.toBeInTheDocument(); - expect(screen.queryByText('Submit')).not.toBeInTheDocument(); }); }); - describe('fetch success', () => { + describe('fetch success schema with non ascii characters', () => { describe('has schema versions', () => { it('renders component with schema info', async () => { - const schemasAPILatestMock = fetchMock.getOnce( - schemasAPILatestUrl, - schemaVersion - ); + fetchMock.getOnce(schemasAPILatestUrl, schemaVersionWithNonAsciiChars); await act(() => { renderComponent(); }); - await waitFor(() => expect(schemasAPILatestMock.called()).toBeTruthy()); + expect(fetchMock.called(schemasAPILatestUrl)).toBeTruthy(); expect(screen.getByText('Submit')).toBeInTheDocument(); expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); }); diff --git a/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.styled.ts b/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.styled.ts index c9e6a07c610..75da5a60bb6 100644 --- a/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.styled.ts +++ b/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.styled.ts @@ -4,4 +4,7 @@ export const Wrapper = styled.div` display: flex; gap: 5px; align-items: center; + & > div { + color: ${({ theme }) => theme.select.label}; + } `; diff --git a/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.tsx b/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.tsx index 77f74351146..002d71a37fd 100644 --- a/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.tsx +++ b/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.tsx @@ -1,37 +1,32 @@ import React from 'react'; -import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; -import Select from 'components/common/Select/Select'; -import { CompatibilityLevelCompatibilityEnum } from 'generated-sources'; -import { getResponse } from 'lib/errorHandling'; +import { + Action, + CompatibilityLevelCompatibilityEnum, + ResourceType, +} from 'generated-sources'; import { useAppDispatch } from 'lib/hooks/redux'; -import usePagination from 'lib/hooks/usePagination'; -import useSearch from 'lib/hooks/useSearch'; import useAppParams from 'lib/hooks/useAppParams'; -import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice'; -import { - fetchSchemas, - schemasApiClient, -} from 'redux/reducers/schemas/schemasSlice'; +import { fetchSchemas } from 'redux/reducers/schemas/schemasSlice'; import { ClusterNameRoute } from 'lib/paths'; +import { schemasApiClient } from 'lib/api'; +import { showServerError } from 'lib/errorHandling'; +import { useConfirm } from 'lib/hooks/useConfirm'; +import { useSearchParams } from 'react-router-dom'; +import { PER_PAGE } from 'lib/constants'; +import { ActionSelect } from 'components/common/ActionComponent'; import * as S from './GlobalSchemaSelector.styled'; const GlobalSchemaSelector: React.FC = () => { const { clusterName } = useAppParams(); const dispatch = useAppDispatch(); - const [searchText] = useSearch(); - const { page, perPage } = usePagination(); + const [searchParams] = useSearchParams(); + const confirm = useConfirm(); const [currentCompatibilityLevel, setCurrentCompatibilityLevel] = React.useState(); - const [nextCompatibilityLevel, setNextCompatibilityLevel] = React.useState< - CompatibilityLevelCompatibilityEnum | undefined - >(); const [isFetching, setIsFetching] = React.useState(false); - const [isUpdating, setIsUpdating] = React.useState(false); - const [isConfirmationVisible, setIsConfirmationVisible] = - React.useState(false); React.useEffect(() => { const fetchData = async () => { @@ -52,30 +47,35 @@ const GlobalSchemaSelector: React.FC = () => { }, [clusterName]); const handleChangeCompatibilityLevel = (level: string | number) => { - setNextCompatibilityLevel(level as CompatibilityLevelCompatibilityEnum); - setIsConfirmationVisible(true); - }; - - const handleUpdateCompatibilityLevel = async () => { - setIsUpdating(true); - if (nextCompatibilityLevel) { - try { - await schemasApiClient.updateGlobalSchemaCompatibilityLevel({ - clusterName, - compatibilityLevel: { compatibility: nextCompatibilityLevel }, - }); - setCurrentCompatibilityLevel(nextCompatibilityLevel); - setNextCompatibilityLevel(undefined); - setIsConfirmationVisible(false); - dispatch( - fetchSchemas({ clusterName, page, perPage, search: searchText }) - ); - } catch (e) { - const err = await getResponse(e as Response); - dispatch(serverErrorAlertAdded(err)); + const nextLevel = level as CompatibilityLevelCompatibilityEnum; + confirm( + <> + Are you sure you want to update the global compatibility level and set + it to {nextLevel}? This may affect the compatibility levels of + the schemas. + , + async () => { + try { + await schemasApiClient.updateGlobalSchemaCompatibilityLevel({ + clusterName, + compatibilityLevel: { + compatibility: nextLevel, + }, + }); + setCurrentCompatibilityLevel(nextLevel); + dispatch( + fetchSchemas({ + clusterName, + page: Number(searchParams.get('page') || 1), + perPage: Number(searchParams.get('perPage') || PER_PAGE), + search: searchParams.get('q') || '', + }) + ); + } catch (e) { + showServerError(e as Response); + } } - } - setIsUpdating(false); + ); }; if (!currentCompatibilityLevel) return null; @@ -83,26 +83,20 @@ const GlobalSchemaSelector: React.FC = () => { return (
Global Compatibility Level:
- @@ -101,17 +129,16 @@ const New: React.FC = () => {
Schema Type * ( + defaultValue={SchemaTypeOptions[0].value as SchemaType} + render={({ field: { name, onChange, value } }) => ( setOffset(value)} - disabled={isTailing} - /> - ) : ( - setTimestamp(date)} - showTimeInput - timeInputLabel="Time:" - dateFormat="MMMM d, yyyy HH:mm" - className="date-picker" - placeholderText="Select timestamp" - disabled={isTailing} - /> - )} - - ({ - label: `Partition #${p.partition.toString()}`, - value: p.partition, - }))} - filterOptions={filterOptions} - value={selectedPartitions} - onChange={setSelectedPartitions} - labelledBy="Select partitions" - disabled={isTailing} - /> - Clear all - {isFetching ? ( - - ) : ( - - )} - - = ({
- - setIsReplicationFactorConfirmationVisible(false)} - onConfirm={replicationFactorSubmit} - > - Are you sure you want to update the replication factor? -
); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZoneContainer.ts b/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZoneContainer.ts deleted file mode 100644 index 722f5144dbb..00000000000 --- a/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZoneContainer.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { connect } from 'react-redux'; -import { RootState } from 'redux/interfaces'; -import { - updateTopicPartitionsCount, - updateTopicReplicationFactor, -} from 'redux/reducers/topics/topicsSlice'; -import { - getTopicPartitionsCountIncreased, - getTopicReplicationFactorUpdated, -} from 'redux/reducers/topics/selectors'; - -import DangerZone from './DangerZone'; - -type OwnProps = { - defaultPartitions: number; - defaultReplicationFactor: number; -}; - -const mapStateToProps = ( - state: RootState, - { defaultPartitions, defaultReplicationFactor }: OwnProps -) => ({ - defaultPartitions, - defaultReplicationFactor, - partitionsCountIncreased: getTopicPartitionsCountIncreased(state), - replicationFactorUpdated: getTopicReplicationFactorUpdated(state), -}); - -const mapDispatchToProps = { - updateTopicPartitionsCount, - updateTopicReplicationFactor, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(DangerZone); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/__test__/DangerZone.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/__test__/DangerZone.spec.tsx index 2099f63305b..b8d5d06dcab 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/__test__/DangerZone.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/__test__/DangerZone.spec.tsx @@ -1,39 +1,43 @@ import React from 'react'; import DangerZone, { - Props, + DangerZoneProps, } from 'components/Topics/Topic/Edit/DangerZone/DangerZone'; -import { act, screen, waitFor, within } from '@testing-library/react'; +import { screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { render, WithRoute } from 'lib/testHelpers'; import { - topicName, - clusterName, -} from 'components/Topics/Topic/Edit/__test__/fixtures'; -import { clusterTopicSendMessagePath } from 'lib/paths'; + useIncreaseTopicPartitionsCount, + useUpdateTopicReplicationFactor, +} from 'lib/hooks/api/topics'; +import { clusterTopicPath } from 'lib/paths'; const defaultPartitions = 3; const defaultReplicationFactor = 3; -const renderComponent = (props?: Partial) => +const clusterName = 'testCluster'; +const topicName = 'testTopic'; + +jest.mock('lib/hooks/api/topics', () => ({ + useIncreaseTopicPartitionsCount: jest.fn(), + useUpdateTopicReplicationFactor: jest.fn(), +})); + +const renderComponent = (props?: Partial) => render( - + , - { initialEntries: [clusterTopicSendMessagePath(clusterName, topicName)] } + { initialEntries: [clusterTopicPath(clusterName, topicName)] } ); -const clickOnDialogSubmitButton = () => { - userEvent.click( +const clickOnDialogSubmitButton = async () => { + await userEvent.click( within(screen.getByRole('dialog')).getByRole('button', { - name: 'Submit', + name: 'Confirm', }) ); }; @@ -41,14 +45,14 @@ const clickOnDialogSubmitButton = () => { const checkDialogThenPressCancel = async () => { const dialog = screen.getByRole('dialog'); expect(screen.getByRole('dialog')).toBeInTheDocument(); - userEvent.click(within(dialog).getByText(/cancel/i)); + await userEvent.click(within(dialog).getByRole('button', { name: 'Cancel' })); await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument() ); }; describe('DangerZone', () => { - it('renders the component', async () => { + it('renders the component', () => { renderComponent(); const numberOfPartitionsEditForm = screen.getByRole('form', { @@ -78,33 +82,33 @@ describe('DangerZone', () => { ).toBeInTheDocument(); }); - it('calls updateTopicPartitionsCount', async () => { - const mockUpdateTopicPartitionsCount = jest.fn(); - renderComponent({ - updateTopicPartitionsCount: mockUpdateTopicPartitionsCount, - }); + it('calls increaseTopicPartitionsCount mutation', async () => { + const mockIncreaseTopicPartitionsCount = jest.fn(); + (useIncreaseTopicPartitionsCount as jest.Mock).mockImplementation(() => ({ + mutateAsync: mockIncreaseTopicPartitionsCount, + })); + renderComponent(); const numberOfPartitionsEditForm = screen.getByRole('form', { name: 'Edit number of partitions', }); - - userEvent.type( + await userEvent.type( within(numberOfPartitionsEditForm).getByRole('spinbutton'), '4' ); - userEvent.click(within(numberOfPartitionsEditForm).getByRole('button')); - - await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); - await waitFor(() => clickOnDialogSubmitButton()); - - expect(mockUpdateTopicPartitionsCount).toHaveBeenCalledTimes(1); + await userEvent.click( + within(numberOfPartitionsEditForm).getByRole('button') + ); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + await clickOnDialogSubmitButton(); + expect(mockIncreaseTopicPartitionsCount).toHaveBeenCalledTimes(1); }); it('calls updateTopicReplicationFactor', async () => { const mockUpdateTopicReplicationFactor = jest.fn(); - renderComponent({ - updateTopicReplicationFactor: mockUpdateTopicReplicationFactor, - }); - + (useUpdateTopicReplicationFactor as jest.Mock).mockImplementation(() => ({ + mutateAsync: mockUpdateTopicReplicationFactor, + })); + renderComponent(); const replicationFactorEditForm = screen.getByRole('form', { name: 'Edit replication factor', }); @@ -117,18 +121,18 @@ describe('DangerZone', () => { within(replicationFactorEditForm).getByRole('button', { name: 'Submit' }) ).toBeInTheDocument(); - userEvent.type( + await userEvent.type( within(replicationFactorEditForm).getByRole('spinbutton'), '4' ); - userEvent.click(within(replicationFactorEditForm).getByRole('button')); + await userEvent.click( + within(replicationFactorEditForm).getByRole('button') + ); - await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); - await waitFor(() => clickOnDialogSubmitButton()); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + await clickOnDialogSubmitButton(); - await waitFor(() => { - expect(mockUpdateTopicReplicationFactor).toHaveBeenCalledTimes(1); - }); + expect(mockUpdateTopicReplicationFactor).toHaveBeenCalledTimes(1); }); it('should view the validation error when partition value is lower than the default passed or empty', async () => { @@ -137,22 +141,20 @@ describe('DangerZone', () => { const partitionInputSubmitBtn = screen.getAllByText(/submit/i)[0]; const value = (defaultPartitions - 4).toString(); expect(partitionInputSubmitBtn).toBeDisabled(); - await act(() => { - userEvent.clear(partitionInput); - userEvent.type(partitionInput, value); - }); + + await userEvent.clear(partitionInput); + await userEvent.type(partitionInput, value); + expect(partitionInput).toHaveValue(+value); expect(partitionInputSubmitBtn).toBeEnabled(); - await act(() => { - userEvent.click(partitionInputSubmitBtn); - }); + + await userEvent.click(partitionInputSubmitBtn); + expect( screen.getByText(/You can only increase the number of partitions!/i) ).toBeInTheDocument(); - userEvent.clear(partitionInput); - await waitFor(() => - expect(screen.getByText(/are required/i)).toBeInTheDocument() - ); + await userEvent.clear(partitionInput); + expect(screen.getByText(/are required/i)).toBeInTheDocument(); }); it('should view the validation error when Replication Facto value is lower than the default passed or empty', async () => { @@ -161,84 +163,13 @@ describe('DangerZone', () => { screen.getByPlaceholderText('Replication Factor'); const replicatorFactorInputSubmitBtn = screen.getAllByText(/submit/i)[1]; - await waitFor(() => userEvent.clear(replicatorFactorInput)); + await userEvent.clear(replicatorFactorInput); expect(replicatorFactorInputSubmitBtn).toBeEnabled(); - userEvent.click(replicatorFactorInputSubmitBtn); - await waitFor(() => - expect(screen.getByText(/are required/i)).toBeInTheDocument() - ); - userEvent.type(replicatorFactorInput, '1'); - await waitFor(() => - expect(screen.queryByText(/are required/i)).not.toBeInTheDocument() - ); - }); - - it('should close any popup if the partitionsCount is Increased ', async () => { - renderComponent({ partitionsCountIncreased: true }); - await waitFor(() => - expect(screen.queryByRole('dialog')).not.toBeInTheDocument() - ); - }); - - it('should close any popup if the replicationFactor is Updated', async () => { - renderComponent({ replicationFactorUpdated: true }); - await waitFor(() => - expect(screen.queryByRole('dialog')).not.toBeInTheDocument() - ); - }); - - it('should already opened Confirmation popup if partitionsCount is Increased', async () => { - const { rerender } = renderComponent(); - const partitionInput = screen.getByPlaceholderText('Number of partitions'); - const partitionInputSubmitBtn = screen.getAllByText(/submit/i)[0]; - - await waitFor(() => { - userEvent.type(partitionInput, '5'); - }); - - userEvent.click(partitionInputSubmitBtn); - await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); - rerender( - - ); - await waitFor(() => - expect(screen.queryByRole('dialog')).not.toBeInTheDocument() - ); - }); - - it('should already opened Confirmation popup if replicationFactor is Increased', async () => { - const { rerender } = renderComponent(); - const replicatorFactorInput = - screen.getByPlaceholderText('Replication Factor'); - const replicatorFactorInputSubmitBtn = screen.getAllByText(/submit/i)[1]; - - await waitFor(() => { - userEvent.type(replicatorFactorInput, '5'); - }); - - userEvent.click(replicatorFactorInputSubmitBtn); - await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); - rerender( - - ); - await waitFor(() => - expect(screen.queryByRole('dialog')).not.toBeInTheDocument() - ); + await userEvent.click(replicatorFactorInputSubmitBtn); + expect(screen.getByText(/are required/i)).toBeInTheDocument(); + await userEvent.type(replicatorFactorInput, '1'); + expect(screen.queryByText(/are required/i)).not.toBeInTheDocument(); }); it('should close the partitions dialog if he cancel button is pressed', async () => { @@ -247,10 +178,8 @@ describe('DangerZone', () => { const partitionInput = screen.getByPlaceholderText('Number of partitions'); const partitionInputSubmitBtn = screen.getAllByText(/submit/i)[0]; - await act(() => { - userEvent.type(partitionInput, '5'); - userEvent.click(partitionInputSubmitBtn); - }); + await userEvent.type(partitionInput, '5'); + await userEvent.click(partitionInputSubmitBtn); await checkDialogThenPressCancel(); }); @@ -261,10 +190,8 @@ describe('DangerZone', () => { screen.getByPlaceholderText('Replication Factor'); const replicatorFactorInputSubmitBtn = screen.getAllByText(/submit/i)[1]; - await act(() => { - userEvent.type(replicatorFactorInput, '5'); - userEvent.click(replicatorFactorInputSubmitBtn); - }); + await userEvent.type(replicatorFactorInput, '5'); + await userEvent.click(replicatorFactorInputSubmitBtn); await checkDialogThenPressCancel(); }); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx index 6de0b52545a..b8a57664997 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx @@ -1,157 +1,103 @@ import React from 'react'; -import { - ClusterName, - TopicFormDataRaw, - TopicName, - TopicConfigByName, - TopicWithDetailedInfo, - TopicFormData, -} from 'redux/interfaces'; +import { TopicConfigByName, TopicFormData } from 'redux/interfaces'; import { useForm, FormProvider } from 'react-hook-form'; import TopicForm from 'components/Topics/shared/Form/TopicForm'; import { RouteParamsClusterTopic } from 'lib/paths'; import { useNavigate } from 'react-router-dom'; import { yupResolver } from '@hookform/resolvers/yup'; import { topicFormValidationSchema } from 'lib/yupExtended'; -import { TOPIC_CUSTOM_PARAMS_PREFIX, TOPIC_CUSTOM_PARAMS } from 'lib/constants'; -import styled from 'styled-components'; -import PageHeading from 'components/common/PageHeading/PageHeading'; -import { useAppSelector } from 'lib/hooks/redux'; -import { getFullTopic } from 'redux/reducers/topics/selectors'; import useAppParams from 'lib/hooks/useAppParams'; - -import DangerZoneContainer from './DangerZone/DangerZoneContainer'; - -export interface Props { - isFetched: boolean; - isTopicUpdated: boolean; - fetchTopicConfig: (payload: { - clusterName: ClusterName; - topicName: TopicName; - }) => void; - updateTopic: (payload: { - clusterName: ClusterName; - topicName: TopicName; - form: TopicFormDataRaw; - }) => void; -} - -const EditWrapperStyled = styled.div` - display: flex; - justify-content: center; - & > * { - width: 800px; - } -`; - -export const DEFAULTS = { +import topicParamsTransformer from 'components/Topics/Topic/Edit/topicParamsTransformer'; +import { MILLISECONDS_IN_WEEK } from 'lib/constants'; +import { + useTopicConfig, + useTopicDetails, + useUpdateTopic, +} from 'lib/hooks/api/topics'; +import DangerZone from 'components/Topics/Topic/Edit/DangerZone/DangerZone'; +import { ConfigSource } from 'generated-sources'; + +export const TOPIC_EDIT_FORM_DEFAULT_PROPS = { partitions: 1, replicationFactor: 1, minInSyncReplicas: 1, cleanupPolicy: 'delete', retentionBytes: -1, + retentionMs: MILLISECONDS_IN_WEEK, maxMessageBytes: 1000012, + customParams: [], }; -const topicParams = (topic: TopicWithDetailedInfo | undefined) => { - if (!topic) { - return DEFAULTS; - } - - const { name, replicationFactor } = topic; - - return { - ...DEFAULTS, - name, - partitions: topic.partitionCount || DEFAULTS.partitions, - replicationFactor, - [TOPIC_CUSTOM_PARAMS_PREFIX]: topic.config - ?.filter( - (el) => - el.value !== el.defaultValue && - Object.keys(TOPIC_CUSTOM_PARAMS).includes(el.name) - ) - .map((el) => ({ name: el.name, value: el.value })), - }; -}; - -let formInit = false; - -const Edit: React.FC = ({ - isFetched, - isTopicUpdated, - fetchTopicConfig, - updateTopic, -}) => { +const Edit: React.FC = () => { const { clusterName, topicName } = useAppParams(); + const { data: topic } = useTopicDetails({ clusterName, topicName }); + const { data: topicConfig } = useTopicConfig({ clusterName, topicName }); + const updateTopic = useUpdateTopic({ clusterName, topicName }); - const topic = useAppSelector((state) => getFullTopic(state, topicName)); - - const defaultValues = React.useMemo(() => topicParams(topic), [topic]); + const defaultValues = topicParamsTransformer(topic, topicConfig); const methods = useForm({ defaultValues, resolver: yupResolver(topicFormValidationSchema), + mode: 'onChange', }); - const [isSubmitting, setIsSubmitting] = React.useState(false); const navigate = useNavigate(); - React.useEffect(() => { - fetchTopicConfig({ clusterName, topicName }); - }, [fetchTopicConfig, clusterName, topicName]); - - React.useEffect(() => { - if (isSubmitting && isTopicUpdated) { - navigate('../'); - } - }, [isSubmitting, isTopicUpdated, clusterName, navigate]); - - if (!isFetched || !topic || !topic.config) { - return null; - } - - if (!formInit) { - methods.reset(defaultValues); - formInit = true; - } - const config: TopicConfigByName = { byName: {}, }; - topic.config.forEach((param) => { + topicConfig?.forEach((param) => { config.byName[param.name] = param; }); - - const onSubmit = async (data: TopicFormDataRaw) => { - updateTopic({ clusterName, topicName, form: data }); - setIsSubmitting(true); // Keep this action after updateTopic to prevent redirect before update. + const onSubmit = async (data: TopicFormData) => { + const filteredDirtyDefaultEntries = Object.entries(data).filter( + ([key, val]) => { + const isDirty = + String(val) !== + String(defaultValues[key as keyof typeof defaultValues]); + + const isDefaultConfig = + config.byName[key]?.source === ConfigSource.DEFAULT_CONFIG; + + // if it is changed should be sent or if it was Dynamic + return isDirty || !isDefaultConfig; + } + ); + + const newData = Object.fromEntries(filteredDirtyDefaultEntries); + try { + await updateTopic.mutateAsync(newData); + navigate('../'); + } catch (e) { + // do nothing + } }; return ( <> - - -
- - - - {topic && ( - - )} -
-
+ + + + {topic && ( + + )} ); }; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/EditContainer.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Edit/EditContainer.tsx deleted file mode 100644 index 5ff9dcc6759..00000000000 --- a/kafka-ui-react-app/src/components/Topics/Topic/Edit/EditContainer.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { connect } from 'react-redux'; -import { RootState } from 'redux/interfaces'; -import { - updateTopic, - fetchTopicConfig, -} from 'redux/reducers/topics/topicsSlice'; -import { - getTopicConfigFetched, - getTopicUpdated, -} from 'redux/reducers/topics/selectors'; - -import Edit from './Edit'; - -const mapStateToProps = (state: RootState) => ({ - isFetched: getTopicConfigFetched(state), - isTopicUpdated: getTopicUpdated(state), -}); - -const mapDispatchToProps = { - fetchTopicConfig, - updateTopic, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(Edit); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx index b658ac8019e..e7c3e288c65 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx @@ -1,13 +1,18 @@ import React from 'react'; -import Edit, { DEFAULTS, Props } from 'components/Topics/Topic/Edit/Edit'; -import { act, screen } from '@testing-library/react'; +import Edit from 'components/Topics/Topic/Edit/Edit'; +import { screen } from '@testing-library/react'; import { render, WithRoute } from 'lib/testHelpers'; import userEvent from '@testing-library/user-event'; import { clusterTopicEditPath } from 'lib/paths'; -import { TopicsState, TopicWithDetailedInfo } from 'redux/interfaces'; -import { getTopicStateFixtures } from 'redux/reducers/topics/__test__/fixtures'; +import { + useTopicConfig, + useTopicDetails, + useUpdateTopic, +} from 'lib/hooks/api/topics'; +import { internalTopicPayload, topicConfigPayload } from 'lib/fixtures/topics'; -import { topicName, clusterName, topicWithInfo } from './fixtures'; +const clusterName = 'testCluster'; +const topicName = 'testTopic'; const mockNavigate = jest.fn(); jest.mock('react-router-dom', () => ({ @@ -15,140 +20,61 @@ jest.mock('react-router-dom', () => ({ useNavigate: () => mockNavigate, })); -const renderComponent = ( - props: Partial = {}, - topic: TopicWithDetailedInfo | null = topicWithInfo -) => { - let topics: TopicsState | undefined; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, +})); + +jest.mock('components/Topics/Topic/Edit/DangerZone/DangerZone', () => () => ( + <>DangerZone +)); + +jest.mock('lib/hooks/api/topics', () => ({ + useTopicDetails: jest.fn(), + useTopicConfig: jest.fn(), + useUpdateTopic: jest.fn(), +})); - if (topic === null) { - topics = undefined; - } else { - topics = getTopicStateFixtures([topic]); - } +const updateTopicMock = jest.fn(); +const renderComponent = () => { + const path = clusterTopicEditPath(clusterName, topicName); return render( - + , - { - initialEntries: [clusterTopicEditPath(clusterName, topicName)], - preloadedState: { topics }, - } + { initialEntries: [path] } ); }; describe('Edit Component', () => { - afterEach(() => {}); - - it('renders the Edit Component', () => { + beforeEach(() => { + (useTopicDetails as jest.Mock).mockImplementation(() => ({ + data: internalTopicPayload, + })); + (useTopicConfig as jest.Mock).mockImplementation(() => ({ + data: topicConfigPayload, + })); + (useUpdateTopic as jest.Mock).mockImplementation(() => ({ + isLoading: false, + mutateAsync: updateTopicMock, + })); renderComponent(); - - expect( - screen.getByRole('heading', { name: `Edit ${topicName}` }) - ).toBeInTheDocument(); - expect( - screen.getByRole('heading', { name: `Danger Zone` }) - ).toBeInTheDocument(); - }); - - it('should check Edit component renders null is not rendered when topic is not passed', () => { - renderComponent({}, { ...topicWithInfo, config: undefined }); - expect( - screen.queryByRole('heading', { name: `Edit ${topicName}` }) - ).not.toBeInTheDocument(); - expect( - screen.queryByRole('heading', { name: `Danger Zone` }) - ).not.toBeInTheDocument(); }); - it('should check Edit component renders null is not isFetched is false', () => { - renderComponent({ isFetched: false }); - expect( - screen.queryByRole('heading', { name: `Edit ${topicName}` }) - ).not.toBeInTheDocument(); - expect( - screen.queryByRole('heading', { name: `Danger Zone` }) - ).not.toBeInTheDocument(); + it('renders DangerZone component', () => { + expect(screen.getByText(`DangerZone`)).toBeInTheDocument(); }); - it('should check Edit component renders null is not topic config is not passed is false', () => { - const modifiedTopic = { ...topicWithInfo }; - modifiedTopic.config = undefined; - renderComponent({}, modifiedTopic); - expect( - screen.queryByRole('heading', { name: `Edit ${topicName}` }) - ).not.toBeInTheDocument(); - expect( - screen.queryByRole('heading', { name: `Danger Zone` }) - ).not.toBeInTheDocument(); - }); - - describe('Edit Component with its topic default and modified values', () => { - it('should check the default partitions value in the DangerZone', async () => { - renderComponent({}, { ...topicWithInfo, partitionCount: 0 }); - // cause topic selector will return falsy - expect( - screen.queryByRole('heading', { name: `Edit ${topicName}` }) - ).not.toBeInTheDocument(); - expect( - screen.queryByRole('heading', { name: `Danger Zone` }) - ).not.toBeInTheDocument(); - }); - - it('should check the default partitions value in the DangerZone', async () => { - renderComponent({}, { ...topicWithInfo, replicationFactor: undefined }); - expect(screen.getByPlaceholderText('Replication Factor')).toHaveValue( - DEFAULTS.replicationFactor - ); - }); - }); - - describe('Submit Case of the Edit Component', () => { - it('should check the submit functionality when topic updated is false', async () => { - const updateTopicMock = jest.fn(); - - renderComponent({ updateTopic: updateTopicMock }, undefined); - - const btn = screen.getAllByText(/submit/i)[0]; - expect(btn).toBeEnabled(); - - await act(() => { - userEvent.type( - screen.getByPlaceholderText('Min In Sync Replicas'), - '1' - ); - userEvent.click(btn); - }); - expect(updateTopicMock).toHaveBeenCalledTimes(1); - expect(mockNavigate).not.toHaveBeenCalled(); - }); - - it('should check the submit functionality when topic updated is true', async () => { - const updateTopicMock = jest.fn(); - - renderComponent( - { updateTopic: updateTopicMock, isTopicUpdated: true }, - undefined - ); - - const btn = screen.getAllByText(/submit/i)[0]; - - await act(() => { - userEvent.type( - screen.getByPlaceholderText('Min In Sync Replicas'), - '1' - ); - userEvent.click(btn); - }); - expect(updateTopicMock).toHaveBeenCalledTimes(1); - expect(mockNavigate).toHaveBeenLastCalledWith('../'); + it('submits form correctly', async () => { + renderComponent(); + const btn = screen.getAllByText(/Update topic/i)[0]; + const field = screen.getByRole('spinbutton', { + name: 'Min In Sync Replicas Min In Sync Replicas', }); + await userEvent.type(field, '1'); + await userEvent.click(btn); + expect(updateTopicMock).toHaveBeenCalledTimes(1); + expect(mockNavigate).toHaveBeenCalledWith('../'); }); }); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/fixtures.ts b/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/fixtures.ts deleted file mode 100644 index 4d4d562b4c5..00000000000 --- a/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/fixtures.ts +++ /dev/null @@ -1,553 +0,0 @@ -import { CleanUpPolicy, ConfigSource, TopicConfig } from 'generated-sources'; -import { TopicWithDetailedInfo } from 'redux/interfaces/topic'; - -export const clusterName = 'testCluster'; -export const topicName = 'testTopic'; - -export const config: TopicConfig[] = [ - { - name: 'compression.type', - value: 'producer', - defaultValue: 'producer', - source: ConfigSource.DYNAMIC_TOPIC_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'compression.type', - value: 'producer', - source: ConfigSource.DYNAMIC_TOPIC_CONFIG, - }, - { - name: 'compression.type', - value: 'producer', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'confluent.value.schema.validation', - value: 'false', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [], - }, - { - name: 'leader.replication.throttled.replicas', - value: '', - defaultValue: '', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [], - }, - { - name: 'confluent.key.subject.name.strategy', - value: 'io.confluent.kafka.serializers.subject.TopicNameStrategy', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [], - }, - { - name: 'message.downconversion.enable', - value: 'true', - defaultValue: 'true', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'log.message.downconversion.enable', - value: 'true', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'min.insync.replicas', - value: '1', - defaultValue: '1', - source: ConfigSource.DYNAMIC_TOPIC_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'min.insync.replicas', - value: '1', - source: ConfigSource.DYNAMIC_TOPIC_CONFIG, - }, - { - name: 'min.insync.replicas', - value: '1', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'segment.jitter.ms', - value: '0', - defaultValue: '0', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [], - }, - { - name: 'cleanup.policy', - value: 'delete', - defaultValue: 'delete', - source: ConfigSource.DYNAMIC_TOPIC_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'cleanup.policy', - value: 'delete', - source: ConfigSource.DYNAMIC_TOPIC_CONFIG, - }, - { - name: 'log.cleanup.policy', - value: 'delete', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'flush.ms', - value: '9223372036854775807', - defaultValue: '9223372036854775807', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [], - }, - { - name: 'confluent.tier.local.hotset.ms', - value: '86400000', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'confluent.tier.local.hotset.ms', - value: '86400000', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'follower.replication.throttled.replicas', - value: '', - defaultValue: '', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [], - }, - { - name: 'confluent.tier.local.hotset.bytes', - value: '-1', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'confluent.tier.local.hotset.bytes', - value: '-1', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'confluent.value.subject.name.strategy', - value: 'io.confluent.kafka.serializers.subject.TopicNameStrategy', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [], - }, - { - name: 'segment.bytes', - value: '1073741824', - defaultValue: '1073741824', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'log.segment.bytes', - value: '1073741824', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'retention.ms', - value: '604800000', - defaultValue: '604800000', - source: ConfigSource.DYNAMIC_TOPIC_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'retention.ms', - value: '604800000', - source: ConfigSource.DYNAMIC_TOPIC_CONFIG, - }, - ], - }, - { - name: 'flush.messages', - value: '9223372036854775807', - defaultValue: '9223372036854775807', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'log.flush.interval.messages', - value: '9223372036854775807', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'confluent.tier.enable', - value: 'false', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'confluent.tier.enable', - value: 'false', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'confluent.tier.segment.hotset.roll.min.bytes', - value: '104857600', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'confluent.tier.segment.hotset.roll.min.bytes', - value: '104857600', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'confluent.segment.speculative.prefetch.enable', - value: 'false', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'confluent.segment.speculative.prefetch.enable', - value: 'false', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'message.format.version', - value: '2.7-IV2', - defaultValue: '2.7-IV2', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'log.message.format.version', - value: '2.7-IV2', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'max.compaction.lag.ms', - value: '9223372036854775807', - defaultValue: '9223372036854775807', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'log.cleaner.max.compaction.lag.ms', - value: '9223372036854775807', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'file.delete.delay.ms', - value: '60000', - defaultValue: '60000', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'log.segment.delete.delay.ms', - value: '60000', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'max.message.bytes', - value: '1000012', - defaultValue: '1000012', - source: ConfigSource.DYNAMIC_TOPIC_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'max.message.bytes', - value: '1000012', - source: ConfigSource.DYNAMIC_TOPIC_CONFIG, - }, - { - name: 'message.max.bytes', - value: '1048588', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'min.compaction.lag.ms', - value: '0', - defaultValue: '0', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'log.cleaner.min.compaction.lag.ms', - value: '0', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'message.timestamp.type', - value: 'CreateTime', - defaultValue: 'CreateTime', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'log.message.timestamp.type', - value: 'CreateTime', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'preallocate', - value: 'false', - defaultValue: 'false', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'log.preallocate', - value: 'false', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'confluent.placement.constraints', - value: '', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [], - }, - { - name: 'min.cleanable.dirty.ratio', - value: '0.5', - defaultValue: '0.5', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'log.cleaner.min.cleanable.ratio', - value: '0.5', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'index.interval.bytes', - value: '4096', - defaultValue: '4096', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'log.index.interval.bytes', - value: '4096', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'unclean.leader.election.enable', - value: 'false', - defaultValue: 'false', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'unclean.leader.election.enable', - value: 'false', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'retention.bytes', - value: '-1', - defaultValue: '-1', - source: ConfigSource.DYNAMIC_TOPIC_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'retention.bytes', - value: '-1', - source: ConfigSource.DYNAMIC_TOPIC_CONFIG, - }, - { - name: 'log.retention.bytes', - value: '-1', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'delete.retention.ms', - value: '86400001', - defaultValue: '86400000', - source: ConfigSource.DYNAMIC_TOPIC_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'delete.retention.ms', - value: '86400001', - source: ConfigSource.DYNAMIC_TOPIC_CONFIG, - }, - { - name: 'log.cleaner.delete.retention.ms', - value: '86400000', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'confluent.prefer.tier.fetch.ms', - value: '-1', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'confluent.prefer.tier.fetch.ms', - value: '-1', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'confluent.key.schema.validation', - value: 'false', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [], - }, - { - name: 'segment.ms', - value: '604800000', - defaultValue: '604800000', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [], - }, - { - name: 'message.timestamp.difference.max.ms', - value: '9223372036854775807', - defaultValue: '9223372036854775807', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'log.message.timestamp.difference.max.ms', - value: '9223372036854775807', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, - { - name: 'segment.index.bytes', - value: '10485760', - defaultValue: '10485760', - source: ConfigSource.DEFAULT_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'log.index.size.max.bytes', - value: '10485760', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, -]; - -export const partitions = [ - { - partition: 0, - leader: 2, - replicas: [ - { - broker: 2, - leader: false, - inSync: true, - }, - ], - offsetMax: 0, - offsetMin: 0, - }, -]; - -export const topicWithInfo: TopicWithDetailedInfo = { - name: topicName, - internal: false, - partitionCount: 1, - replicationFactor: 1, - replicas: 1, - inSyncReplicas: 1, - segmentSize: 0, - segmentCount: 1, - underReplicatedPartitions: 0, - cleanUpPolicy: CleanUpPolicy.DELETE, - partitions, - config, -}; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/topicParamsTransformer.spec.ts b/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/topicParamsTransformer.spec.ts new file mode 100644 index 00000000000..d1b2d003834 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/topicParamsTransformer.spec.ts @@ -0,0 +1,59 @@ +import topicParamsTransformer, { + getValue, +} from 'components/Topics/Topic/Edit/topicParamsTransformer'; +import { externalTopicPayload, topicConfigPayload } from 'lib/fixtures/topics'; +import { TOPIC_EDIT_FORM_DEFAULT_PROPS } from 'components/Topics/Topic/Edit/Edit'; + +const defaultValue = 3232326; +describe('getValue', () => { + it('returns value when field exists', () => { + expect(getValue(topicConfigPayload, 'min.insync.replicas')).toEqual(1); + }); + it('returns default value when field does not exists', () => { + expect(getValue(topicConfigPayload, 'min.max.mid', defaultValue)).toEqual( + defaultValue + ); + }); +}); + +describe('topicParamsTransformer', () => { + it('returns default values when config payload is not defined', () => { + expect(topicParamsTransformer(externalTopicPayload)).toEqual( + TOPIC_EDIT_FORM_DEFAULT_PROPS + ); + }); + it('returns default values when topic payload is not defined', () => { + expect(topicParamsTransformer(undefined, topicConfigPayload)).toEqual( + TOPIC_EDIT_FORM_DEFAULT_PROPS + ); + }); + it('returns transformed config', () => { + expect( + topicParamsTransformer(externalTopicPayload, topicConfigPayload) + ).toEqual({ + ...TOPIC_EDIT_FORM_DEFAULT_PROPS, + name: externalTopicPayload.name, + }); + }); + it('returns default partitions config', () => { + expect( + topicParamsTransformer( + { ...externalTopicPayload, partitionCount: undefined }, + topicConfigPayload + ).partitions + ).toEqual(TOPIC_EDIT_FORM_DEFAULT_PROPS.partitions); + }); + it('returns empty list of custom params', () => { + expect( + topicParamsTransformer(externalTopicPayload, topicConfigPayload) + .customParams + ).toEqual([]); + }); + it('returns list of custom params', () => { + expect( + topicParamsTransformer(externalTopicPayload, [ + { ...topicConfigPayload[0], value: 'SuperCustom' }, + ]).customParams + ).toEqual([{ name: 'compression.type', value: 'SuperCustom' }]); + }); +}); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/topicParamsTransformer.ts b/kafka-ui-react-app/src/components/Topics/Topic/Edit/topicParamsTransformer.ts new file mode 100644 index 00000000000..910f31a5fd2 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Edit/topicParamsTransformer.ts @@ -0,0 +1,45 @@ +import { + MILLISECONDS_IN_WEEK, + TOPIC_CUSTOM_PARAMS, + TOPIC_CUSTOM_PARAMS_PREFIX, +} from 'lib/constants'; +import { TOPIC_EDIT_FORM_DEFAULT_PROPS } from 'components/Topics/Topic/Edit/Edit'; +import { getCleanUpPolicyValue } from 'components/Topics/shared/Form/TopicForm'; +import { Topic, TopicConfig } from 'generated-sources'; + +export const getValue = ( + config: TopicConfig[], + fieldName: string, + defaultValue?: number +) => + Number(config.find(({ name }) => name === fieldName)?.value) || defaultValue; + +const topicParamsTransformer = (topic?: Topic, config?: TopicConfig[]) => { + if (!config || !topic) { + return TOPIC_EDIT_FORM_DEFAULT_PROPS; + } + + const customParams = config.reduce((acc, { name, value, defaultValue }) => { + if (value === defaultValue) return acc; + if (!TOPIC_CUSTOM_PARAMS[name]) return acc; + return [...acc, { name, value }]; + }, [] as { name: string; value?: string }[]); + + return { + ...TOPIC_EDIT_FORM_DEFAULT_PROPS, + name: topic.name, + replicationFactor: topic.replicationFactor, + partitions: + topic.partitionCount || TOPIC_EDIT_FORM_DEFAULT_PROPS.partitions, + cleanupPolicy: + getCleanUpPolicyValue(topic.cleanUpPolicy) || + TOPIC_EDIT_FORM_DEFAULT_PROPS.cleanupPolicy, + maxMessageBytes: getValue(config, 'max.message.bytes', 1000012), + minInSyncReplicas: getValue(config, 'min.insync.replicas', 1), + retentionBytes: getValue(config, 'retention.bytes', -1), + retentionMs: getValue(config, 'retention.ms', MILLISECONDS_IN_WEEK), + + [TOPIC_CUSTOM_PARAMS_PREFIX]: customParams, + }; +}; +export default topicParamsTransformer; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/AddEditFilterContainer.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/AddEditFilterContainer.tsx similarity index 86% rename from kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/AddEditFilterContainer.tsx rename to kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/AddEditFilterContainer.tsx index fcb0da429d1..557db159ba7 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/AddEditFilterContainer.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/AddEditFilterContainer.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import * as S from 'components/Topics/Topic/Details/Messages/Filters/Filters.styled'; +import * as S from 'components/Topics/Topic/Messages/Filters/Filters.styled'; import { InputLabel } from 'components/common/Input/InputLabel.styled'; import Input from 'components/common/Input/Input'; import { FormProvider, Controller, useForm } from 'react-hook-form'; import { ErrorMessage } from '@hookform/error-message'; import { Button } from 'components/common/Button/Button'; import { FormError } from 'components/common/Input/Input.styled'; -import { AddMessageFilters } from 'components/Topics/Topic/Details/Messages/Filters/AddFilter'; +import { AddMessageFilters } from 'components/Topics/Topic/Messages/Filters/AddFilter'; import Editor from 'components/common/Editor/Editor'; import { yupResolver } from '@hookform/resolvers/yup'; import yup from 'lib/yupExtended'; @@ -51,8 +51,12 @@ const AddEditFilterContainer: React.FC = ({ const onSubmit = React.useCallback( (values: AddMessageFilters) => { - submitCallback?.(values); - reset({ name: '', code: '', saveFilter: false }); + try { + submitCallback?.(values); + reset({ name: '', code: '', saveFilter: false }); + } catch (e) { + // do nothing + } }, [isAdd, reset, submitCallback] ); @@ -85,14 +89,10 @@ const AddEditFilterContainer: React.FC = ({ {isAdd && ( - - - Save this filter - + + + Save this filter + )}
Display name diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/AddFilter.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/AddFilter.tsx new file mode 100644 index 00000000000..035d98c3a39 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/AddFilter.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import * as S from 'components/Topics/Topic/Messages/Filters/Filters.styled'; +import { MessageFilters } from 'components/Topics/Topic/Messages/Filters/Filters'; +import { FilterEdit } from 'components/Topics/Topic/Messages/Filters/FilterModal'; +import SavedFilters from 'components/Topics/Topic/Messages/Filters/SavedFilters'; +import SavedIcon from 'components/common/Icons/SavedIcon'; +import QuestionIcon from 'components/common/Icons/QuestionIcon'; +import useBoolean from 'lib/hooks/useBoolean'; +import { showAlert } from 'lib/errorHandling'; + +import AddEditFilterContainer from './AddEditFilterContainer'; +import InfoModal from './InfoModal'; + +export interface FilterModalProps { + toggleIsOpen(): void; + filters: MessageFilters[]; + addFilter(values: MessageFilters): void; + deleteFilter(index: number): void; + activeFilterHandler(activeFilter: MessageFilters, index: number): void; + toggleEditModal(): void; + editFilter(value: FilterEdit): void; + isSavedFiltersOpen: boolean; + onClickSavedFilters(newValue: boolean): void; + activeFilter?: MessageFilters; +} + +export interface AddMessageFilters extends MessageFilters { + saveFilter: boolean; +} + +const AddFilter: React.FC = ({ + toggleIsOpen, + filters, + addFilter, + deleteFilter, + activeFilterHandler, + toggleEditModal, + editFilter, + isSavedFiltersOpen, + onClickSavedFilters, + activeFilter, +}) => { + const { value: isOpen, toggle } = useBoolean(); + + const onSubmit = React.useCallback( + async (values: AddMessageFilters) => { + const isFilterExists = filters.some( + (filter) => filter.name === values.name + ); + + if (isFilterExists) { + showAlert('error', { + id: '', + title: 'Validation Error', + message: 'Filter with the same name already exists', + }); + return; + } + + const data = { ...values }; + if (data.saveFilter) { + addFilter(data); + } else { + // other case is not applying the filter + const dataCodeLabel = + data.code.length > 16 ? `${data.code.slice(0, 16)}...` : data.code; + data.name = data.name || dataCodeLabel; + + activeFilterHandler(data, -1); + toggleIsOpen(); + } + }, + [activeFilterHandler, addFilter, toggleIsOpen] + ); + return ( + <> + + Add filter +
+ + + + {isOpen && } +
+
+ {isSavedFiltersOpen ? ( + onClickSavedFilters(!onClickSavedFilters)} + filters={filters} + onEdit={(index: number, filter: MessageFilters) => { + toggleEditModal(); + editFilter({ index, filter }); + }} + activeFilter={activeFilter} + /> + ) : ( + <> + onClickSavedFilters(!isSavedFiltersOpen)} + > + Saved Filters + + + + )} + + ); +}; + +export default AddFilter; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/EditFilter.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/EditFilter.tsx similarity index 78% rename from kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/EditFilter.tsx rename to kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/EditFilter.tsx index 15c9fdfd7af..04f9c47ee93 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/EditFilter.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/EditFilter.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters/Filters'; -import { FilterEdit } from 'components/Topics/Topic/Details/Messages/Filters/FilterModal'; +import { MessageFilters } from 'components/Topics/Topic/Messages/Filters/Filters'; +import { FilterEdit } from 'components/Topics/Topic/Messages/Filters/FilterModal'; import AddEditFilterContainer from './AddEditFilterContainer'; import * as S from './Filters.styled'; @@ -22,7 +22,7 @@ const EditFilter: React.FC = ({ }; return ( <> - Edit saved filter + Edit filter toggleEditModal()} submitBtnText="Save" diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/FilterModal.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/FilterModal.tsx new file mode 100644 index 00000000000..0ada1587856 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/FilterModal.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import * as S from 'components/Topics/Topic/Messages/Filters/Filters.styled'; +import { + ActiveMessageFilter, + MessageFilters, +} from 'components/Topics/Topic/Messages/Filters/Filters'; +import AddFilter from 'components/Topics/Topic/Messages/Filters/AddFilter'; +import EditFilter from 'components/Topics/Topic/Messages/Filters/EditFilter'; + +export interface FilterModalProps { + toggleIsOpen(): void; + filters: MessageFilters[]; + addFilter(values: MessageFilters): void; + deleteFilter(index: number): void; + activeFilterHandler(activeFilter: MessageFilters, index: number): void; + editSavedFilter(filter: FilterEdit): void; + activeFilter: ActiveMessageFilter; + quickEditMode?: boolean; +} + +export interface FilterEdit { + index: number; + filter: MessageFilters; +} + +const FilterModal: React.FC = ({ + toggleIsOpen, + filters, + addFilter, + deleteFilter, + activeFilterHandler, + editSavedFilter, + activeFilter, + quickEditMode = false, +}) => { + const [isInEditMode, setIsInEditMode] = + React.useState(quickEditMode); + const [isSavedFiltersOpen, setIsSavedFiltersOpen] = + React.useState(false); + + const toggleEditModal = () => { + setIsInEditMode(!isInEditMode); + }; + + const [editFilter, setEditFilter] = React.useState(() => { + const { index, name, code } = activeFilter; + return quickEditMode + ? { index, filter: { name, code } } + : { index: -1, filter: { name: '', code: '' } }; + }); + const editFilterHandler = (value: FilterEdit) => { + setEditFilter(value); + setIsInEditMode(!isInEditMode); + }; + + const toggleEditModalHandler = quickEditMode ? toggleIsOpen : toggleEditModal; + + return ( + + {isInEditMode ? ( + + ) : ( + setIsSavedFiltersOpen(!isSavedFiltersOpen)} + activeFilter={activeFilter} + /> + )} + + ); +}; + +export default FilterModal; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/Filters.styled.ts b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/Filters.styled.ts new file mode 100644 index 00000000000..7ec8fbde072 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/Filters.styled.ts @@ -0,0 +1,426 @@ +import Input from 'components/common/Input/Input'; +import Select from 'components/common/Select/Select'; +import styled, { css } from 'styled-components'; +import DatePicker from 'react-datepicker'; +import EditIcon from 'components/common/Icons/EditIcon'; +import closeIcon from 'components/common/Icons/CloseIcon'; + +interface SavedFilterProps { + selected: boolean; +} +interface MessageLoadingProps { + isLive: boolean; +} + +interface MessageLoadingSpinnerProps { + isFetching: boolean; +} + +export const FiltersWrapper = styled.div` + display: flex; + flex-direction: column; + padding-left: 16px; + padding-right: 16px; + + & > div:first-child { + display: flex; + justify-content: space-between; + padding-top: 2px; + align-items: flex-end; + } +`; + +export const FilterInputs = styled.div` + display: flex; + gap: 8px; + align-items: flex-end; + width: 90%; + flex-wrap: wrap; +`; + +export const SeekTypeSelectorWrapper = styled.div` + display: flex; + & .select-wrapper { + width: 40% !important; + & > select { + border-radius: 4px 0 0 4px !important; + } + } +`; + +export const OffsetSelector = styled(Input)` + border-radius: 0 4px 4px 0 !important; + &::placeholder { + color: ${({ theme }) => theme.input.color.normal}; + } +`; + +export const DatePickerInput = styled(DatePicker)` + height: 32px; + border: 1px ${({ theme }) => theme.select.borderColor.normal} solid; + border-left: none; + border-radius: 0 4px 4px 0; + font-size: 14px; + width: 100%; + padding-left: 12px; + background-color: ${({ theme }) => theme.input.backgroundColor.normal}; + color: ${({ theme }) => theme.input.color.normal}; + &::placeholder { + color: ${({ theme }) => theme.input.color.normal}; + } + + background-image: url('data:image/svg+xml,%3Csvg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M1 1L5 5L9 1" stroke="%23454F54"/%3E%3C/svg%3E%0A') !important; + background-repeat: no-repeat !important; + background-position-x: 96% !important; + background-position-y: 55% !important; + appearance: none !important; + + &:hover { + cursor: pointer; + } + &:focus { + outline: none; + } +`; + +export const FiltersMetrics = styled.div` + display: flex; + justify-content: flex-end; + align-items: center; + gap: 22px; + padding-top: 16px; + padding-bottom: 16px; +`; +export const Message = styled.div` + font-size: 14px; + color: ${({ theme }) => theme.metrics.filters.color.normal}; +`; +export const Metric = styled.div` + color: ${({ theme }) => theme.metrics.filters.color.normal}; + font-size: 12px; + display: flex; +`; + +export const MetricsIcon = styled.div` + color: ${({ theme }) => theme.metrics.filters.color.icon}; + padding-right: 6px; + height: 12px; +`; + +export const ClearAll = styled.div` + color: ${({ theme }) => theme.metrics.filters.color.normal}; + font-size: 12px; + cursor: pointer; + line-height: 32px; + margin-left: 8px; +`; + +export const ButtonContainer = styled.div` + width: 100%; + display: flex; + justify-content: center; + margin-top: 20px; +`; + +export const ListItem = styled.li` + font-size: 12px; + font-weight: 400; + margin: 4px 0; + line-height: 1.5; + color: ${({ theme }) => theme.table.td.color.normal}; +`; + +export const InfoParagraph = styled.div` + font-size: 12px; + font-weight: 400; + line-height: 1.5; + margin-bottom: 10px; + color: ${({ theme }) => theme.table.td.color.normal}; +`; + +export const MessageFilterModal = styled.div` + height: auto; + width: 560px; + border-radius: 8px; + background: ${({ theme }) => theme.modal.backgroundColor}; + position: absolute; + left: 25%; + border: 1px solid ${({ theme }) => theme.modal.border.contrast}; + box-shadow: ${({ theme }) => theme.modal.shadow}; + padding: 16px; + z-index: 1; +`; + +export const InfoModal = styled.div` + height: auto; + width: 560px; + border-radius: 8px; + background: ${({ theme }) => theme.modal.backgroundColor}; + position: absolute; + left: 25%; + border: 1px solid ${({ theme }) => theme.modal.border.contrast}; + box-shadow: ${({ theme }) => theme.modal.shadow}; + padding: 32px; + z-index: 1; +`; + +export const QuestionIconContainer = styled.button` + cursor: pointer; + padding: 0; + background: none; + border: none; +`; + +export const FilterTitle = styled.h3` + line-height: 32px; + font-size: 20px; + margin-bottom: 40px; + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + color: ${({ theme }) => theme.modal.color}; + &:after { + content: ''; + width: calc(100% + 32px); + height: 1px; + position: absolute; + top: 40px; + left: -16px; + display: inline-block; + background-color: ${({ theme }) => theme.modal.border.top}; + } +`; + +export const CreatedFilter = styled.p` + margin: 25px 0 10px; + font-size: 14px; + line-height: 20px; + color: ${({ theme }) => theme.savedFilter.color}; +`; + +export const NoSavedFilter = styled.p` + color: ${({ theme }) => theme.savedFilter.color}; +`; +export const SavedFiltersContainer = styled.div` + overflow-y: auto; + height: 195px; + justify-content: space-around; + padding-left: 10px; +`; + +export const SavedFilterName = styled.div` + font-size: 14px; + line-height: 20px; + color: ${({ theme }) => theme.savedFilter.filterName}; +`; + +export const FilterButtonWrapper = styled.div` + display: flex; + justify-content: flex-end; + margin-top: 10px; + gap: 10px; + padding-top: 16px; + position: relative; + &:before { + content: ''; + width: calc(100% + 32px); + height: 1px; + position: absolute; + top: 0; + left: -16px; + display: inline-block; + background-color: ${({ theme }) => theme.modal.border.bottom}; + } +`; + +export const ActiveSmartFilterWrapper = styled.div` + padding: 8px 0 5px; + display: flex; + gap: 10px; + align-items: center; + justify-content: flex-start; +`; + +export const DeleteSavedFilter = styled.div.attrs({ role: 'deleteIcon' })` + margin-top: 2px; + cursor: pointer; + color: ${({ theme }) => theme.icons.deleteIcon}; +`; + +export const FilterEdit = styled.div` + font-weight: 500; + font-size: 14px; + line-height: 20px; +`; + +export const FilterOptions = styled.div` + display: none; + width: 50px; + justify-content: space-between; + color: ${({ theme }) => theme.editFilter.textColor}; +`; + +export const SavedFilter = styled.div.attrs({ + role: 'savedFilter', +})` + display: flex; + justify-content: space-between; + padding-right: 5px; + height: 32px; + align-items: center; + cursor: pointer; + border-top: 1px solid ${({ theme }) => theme.panelColor.borderTop}; + &:hover ${FilterOptions} { + display: flex; + } + &:hover { + background: ${({ theme }) => theme.layout.stuffColor}; + } + background: ${({ selected, theme }) => + selected ? theme.layout.stuffColor : theme.modal.backgroundColor}; +`; + +export const ActiveSmartFilter = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + height: 32px; + color: ${({ theme }) => theme.activeFilter.color}; + background: ${({ theme }) => theme.activeFilter.backgroundColor}; + border-radius: 4px; + font-size: 14px; + line-height: 20px; +`; + +export const EditSmartFilterIcon = styled.div( + ({ theme: { icons } }) => css` + color: ${icons.editIcon.normal}; + display: flex; + align-items: center; + justify-content: center; + height: 32px; + width: 32px; + cursor: pointer; + border-left: 1px solid ${icons.editIcon.border}; + + &:hover { + ${EditIcon} { + fill: ${icons.editIcon.hover}; + } + } + + &:active { + ${EditIcon} { + fill: ${icons.editIcon.active}; + } + } + ` +); + +export const SmartFilterName = styled.div` + padding: 0 8px; + min-width: 32px; +`; + +export const DeleteSmartFilterIcon = styled.div( + ({ theme: { icons } }) => css` + color: ${icons.closeIcon.normal}; + display: flex; + align-items: center; + justify-content: center; + height: 32px; + width: 32px; + cursor: pointer; + border-left: 1px solid ${icons.closeIcon.border}; + + svg { + height: 14px; + width: 14px; + } + + &:hover { + ${closeIcon} { + fill: ${icons.closeIcon.hover}; + } + } + + &:active { + ${closeIcon} { + fill: ${icons.closeIcon.active}; + } + } + ` +); + +export const MessageLoading = styled.div.attrs({ + role: 'contentLoader', +})` + color: ${({ theme }) => theme.heading.h3.color}; + font-size: ${({ theme }) => theme.heading.h3.fontSize}; + display: ${({ isLive }) => (isLive ? 'flex' : 'none')}; + justify-content: space-around; + width: 250px; +`; + +export const StopLoading = styled.div` + color: ${({ theme }) => theme.pageLoader.borderColor}; + font-size: ${({ theme }) => theme.heading.h3.fontSize}; + cursor: pointer; +`; + +export const MessageLoadingSpinner = styled.div` + display: ${({ isFetching }) => (isFetching ? 'block' : 'none')}; + border: 3px solid ${({ theme }) => theme.pageLoader.borderColor}; + border-bottom: 3px solid ${({ theme }) => theme.pageLoader.borderBottomColor}; + border-radius: 50%; + width: 20px; + height: 20px; + animation: spin 1.3s linear infinite; + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } +`; + +export const SavedFiltersTextContainer = styled.div.attrs({ + role: 'savedFilterText', +})` + display: flex; + align-items: center; + cursor: pointer; + margin-bottom: 15px; +`; + +const textStyle = css` + font-size: 14px; + color: ${({ theme }) => theme.editFilter.textColor}; + font-weight: 500; +`; + +export const SavedFiltersText = styled.div` + ${textStyle}; + margin-left: 7px; +`; + +export const BackToCustomText = styled.div` + ${textStyle}; + cursor: pointer; +`; + +export const SeekTypeSelect = styled(Select)` + border-top-right-radius: 0; + border-bottom-right-radius: 0; + user-select: none; +`; + +export const Serdes = styled.div` + display: flex; + gap: 24px; + padding: 8px 0; +`; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/Filters.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/Filters.tsx new file mode 100644 index 00000000000..347623d2226 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/Filters.tsx @@ -0,0 +1,643 @@ +import 'react-datepicker/dist/react-datepicker.css'; + +import { + MessageFilterType, + Partition, + SeekDirection, + SeekType, + SerdeUsage, + TopicMessage, + TopicMessageConsuming, + TopicMessageEvent, + TopicMessageEventTypeEnum, +} from 'generated-sources'; +import React, { useContext } from 'react'; +import omitBy from 'lodash/omitBy'; +import { useNavigate, useLocation, useSearchParams } from 'react-router-dom'; +import MultiSelect from 'components/common/MultiSelect/MultiSelect.styled'; +import { Option } from 'react-multi-select-component'; +import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted'; +import { BASE_PARAMS } from 'lib/constants'; +import Select from 'components/common/Select/Select'; +import { Button } from 'components/common/Button/Button'; +import Search from 'components/common/Search/Search'; +import FilterModal, { + FilterEdit, +} from 'components/Topics/Topic/Messages/Filters/FilterModal'; +import { SeekDirectionOptions } from 'components/Topics/Topic/Messages/Messages'; +import TopicMessagesContext from 'components/contexts/TopicMessagesContext'; +import useBoolean from 'lib/hooks/useBoolean'; +import { RouteParamsClusterTopic } from 'lib/paths'; +import useAppParams from 'lib/hooks/useAppParams'; +import PlusIcon from 'components/common/Icons/PlusIcon'; +import EditIcon from 'components/common/Icons/EditIcon'; +import CloseIcon from 'components/common/Icons/CloseIcon'; +import ClockIcon from 'components/common/Icons/ClockIcon'; +import ArrowDownIcon from 'components/common/Icons/ArrowDownIcon'; +import FileIcon from 'components/common/Icons/FileIcon'; +import { useTopicDetails } from 'lib/hooks/api/topics'; +import { InputLabel } from 'components/common/Input/InputLabel.styled'; +import { getSerdeOptions } from 'components/Topics/Topic/SendMessage/utils'; +import { useSerdes } from 'lib/hooks/api/topicMessages'; + +import * as S from './Filters.styled'; +import { + filterOptions, + getOffsetFromSeekToParam, + getSelectedPartitionsFromSeekToParam, + getTimestampFromSeekToParam, +} from './utils'; + +type Query = Record; + +export interface FiltersProps { + phaseMessage?: string; + meta: TopicMessageConsuming; + isFetching: boolean; + messageEventType?: string; + addMessage(content: { message: TopicMessage; prepend: boolean }): void; + resetMessages(): void; + updatePhase(phase: string): void; + updateMeta(meta: TopicMessageConsuming): void; + setIsFetching(status: boolean): void; + setMessageType(messageType: string): void; +} + +export interface MessageFilters { + name: string; + code: string; +} + +export interface ActiveMessageFilter { + index: number; + name: string; + code: string; +} + +const PER_PAGE = 100; + +export const SeekTypeOptions = [ + { value: SeekType.OFFSET, label: 'Offset' }, + { value: SeekType.TIMESTAMP, label: 'Timestamp' }, +]; + +const Filters: React.FC = ({ + phaseMessage, + meta: { elapsedMs, bytesConsumed, messagesConsumed, filterApplyErrors }, + isFetching, + addMessage, + resetMessages, + updatePhase, + updateMeta, + setIsFetching, + setMessageType, + messageEventType, +}) => { + const { clusterName, topicName } = useAppParams(); + const location = useLocation(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + + const page = searchParams.get('page'); + + const { data: topic } = useTopicDetails({ clusterName, topicName }); + + const partitions = topic?.partitions || []; + + const { seekDirection, isLive, changeSeekDirection } = + useContext(TopicMessagesContext); + + const { value: isOpen, toggle } = useBoolean(); + + const { value: isQuickEditOpen, toggle: toggleQuickEdit } = useBoolean(); + + const source = React.useRef(null); + + const [selectedPartitions, setSelectedPartitions] = React.useState( + getSelectedPartitionsFromSeekToParam(searchParams, partitions) + ); + + const [currentSeekType, setCurrentSeekType] = React.useState( + SeekTypeOptions.find( + (ele) => ele.value === (searchParams.get('seekType') as SeekType) + ) !== undefined + ? (searchParams.get('seekType') as SeekType) + : SeekType.OFFSET + ); + const [offset, setOffset] = React.useState( + getOffsetFromSeekToParam(searchParams) + ); + + const [timestamp, setTimestamp] = React.useState( + getTimestampFromSeekToParam(searchParams) + ); + const [keySerde, setKeySerde] = React.useState( + searchParams.get('keySerde') || '' + ); + const [valueSerde, setValueSerde] = React.useState( + searchParams.get('valueSerde') || '' + ); + + const [savedFilters, setSavedFilters] = React.useState( + JSON.parse(localStorage.getItem('savedFilters') ?? '[]') + ); + + let storageActiveFilter = localStorage.getItem('activeFilter'); + storageActiveFilter = + storageActiveFilter ?? JSON.stringify({ name: '', code: '', index: -1 }); + + const [activeFilter, setActiveFilter] = React.useState( + JSON.parse(storageActiveFilter) + ); + + const [queryType, setQueryType] = React.useState( + activeFilter.name + ? MessageFilterType.GROOVY_SCRIPT + : MessageFilterType.STRING_CONTAINS + ); + const [query, setQuery] = React.useState(searchParams.get('q') || ''); + const [isTailing, setIsTailing] = React.useState(isLive); + + const isSeekTypeControlVisible = React.useMemo( + () => selectedPartitions.length > 0, + [selectedPartitions] + ); + + const isSubmitDisabled = React.useMemo(() => { + if (isSeekTypeControlVisible) { + return ( + (currentSeekType === SeekType.TIMESTAMP && !timestamp) || isTailing + ); + } + + return false; + }, [isSeekTypeControlVisible, currentSeekType, timestamp, isTailing]); + + const partitionMap = React.useMemo( + () => + partitions.reduce>( + (acc, partition) => ({ + ...acc, + [partition.partition]: partition, + }), + {} + ), + [partitions] + ); + + const handleClearAllFilters = () => { + setCurrentSeekType(SeekType.OFFSET); + setOffset(''); + setTimestamp(null); + setQuery(''); + changeSeekDirection(SeekDirection.FORWARD); + getSelectedPartitionsFromSeekToParam(searchParams, partitions); + setSelectedPartitions( + partitions.map((partition: Partition) => { + return { + value: partition.partition, + label: `Partition #${partition.partition.toString()}`, + }; + }) + ); + }; + + const handleFiltersSubmit = (currentOffset: string) => { + const nextAttempt = Number(searchParams.get('attempt') || 0) + 1; + const props: Query = { + q: + queryType === MessageFilterType.GROOVY_SCRIPT + ? activeFilter.code + : query, + filterQueryType: queryType, + attempt: nextAttempt, + limit: PER_PAGE, + page: page || 0, + seekDirection, + keySerde: keySerde || searchParams.get('keySerde') || '', + valueSerde: valueSerde || searchParams.get('valueSerde') || '', + }; + + if (isSeekTypeControlVisible) { + switch (seekDirection) { + case SeekDirection.FORWARD: + props.seekType = SeekType.BEGINNING; + break; + case SeekDirection.BACKWARD: + case SeekDirection.TAILING: + props.seekType = SeekType.LATEST; + break; + default: + props.seekType = currentSeekType; + } + + if (offset && currentSeekType === SeekType.OFFSET) { + props.seekType = SeekType.OFFSET; + } + + if (timestamp && currentSeekType === SeekType.TIMESTAMP) { + props.seekType = SeekType.TIMESTAMP; + } + + const isSeekTypeWithSeekTo = + props.seekType === SeekType.TIMESTAMP || + props.seekType === SeekType.OFFSET; + + if ( + selectedPartitions.length !== partitions.length || + isSeekTypeWithSeekTo + ) { + // not everything in the partition is selected + props.seekTo = selectedPartitions.map(({ value }) => { + const offsetProperty = + seekDirection === SeekDirection.FORWARD ? 'offsetMin' : 'offsetMax'; + const offsetBasedSeekTo = + currentOffset || partitionMap[value][offsetProperty]; + const seekToOffset = + currentSeekType === SeekType.OFFSET + ? offsetBasedSeekTo + : timestamp?.getTime(); + + return `${value}::${seekToOffset || '0'}`; + }); + } + } + + const newProps = omitBy(props, (v) => v === undefined || v === ''); + const qs = Object.keys(newProps) + .map((key) => `${key}=${encodeURIComponent(newProps[key] as string)}`) + .join('&'); + navigate({ + search: `?${qs}`, + }); + }; + + const handleSSECancel = () => { + if (!source.current) return; + setIsFetching(false); + source.current.close(); + }; + + const addFilter = (newFilter: MessageFilters) => { + const filters = [...savedFilters]; + filters.push(newFilter); + setSavedFilters(filters); + localStorage.setItem('savedFilters', JSON.stringify(filters)); + }; + const deleteFilter = (index: number) => { + const filters = [...savedFilters]; + if (activeFilter.name && activeFilter.index === index) { + localStorage.removeItem('activeFilter'); + setActiveFilter({ name: '', code: '', index: -1 }); + setQueryType(MessageFilterType.STRING_CONTAINS); + } + filters.splice(index, 1); + localStorage.setItem('savedFilters', JSON.stringify(filters)); + setSavedFilters(filters); + }; + const deleteActiveFilter = () => { + setActiveFilter({ name: '', code: '', index: -1 }); + localStorage.removeItem('activeFilter'); + setQueryType(MessageFilterType.STRING_CONTAINS); + }; + const activeFilterHandler = ( + newActiveFilter: MessageFilters, + index: number + ) => { + localStorage.setItem( + 'activeFilter', + JSON.stringify({ index, ...newActiveFilter }) + ); + setActiveFilter({ index, ...newActiveFilter }); + setQueryType(MessageFilterType.GROOVY_SCRIPT); + }; + + const composeMessageFilter = (filter: FilterEdit): ActiveMessageFilter => ({ + index: filter.index, + name: filter.filter.name, + code: filter.filter.code, + }); + + const storeAsActiveFilter = (filter: FilterEdit) => { + const messageFilter = JSON.stringify(composeMessageFilter(filter)); + localStorage.setItem('activeFilter', messageFilter); + }; + + const editSavedFilter = (filter: FilterEdit) => { + const filters = [...savedFilters]; + filters[filter.index] = filter.filter; + if (activeFilter.name && activeFilter.index === filter.index) { + setActiveFilter(composeMessageFilter(filter)); + storeAsActiveFilter(filter); + } + localStorage.setItem('savedFilters', JSON.stringify(filters)); + setSavedFilters(filters); + }; + + const editCurrentFilter = (filter: FilterEdit) => { + if (filter.index < 0) { + setActiveFilter(composeMessageFilter(filter)); + storeAsActiveFilter(filter); + } else { + editSavedFilter(filter); + } + }; + // eslint-disable-next-line consistent-return + React.useEffect(() => { + if (location.search?.length !== 0) { + const url = `${BASE_PARAMS.basePath}/api/clusters/${encodeURIComponent( + clusterName + )}/topics/${topicName}/messages${location.search}`; + const sse = new EventSource(url); + + source.current = sse; + setIsFetching(true); + + sse.onopen = () => { + resetMessages(); + setIsFetching(true); + }; + sse.onmessage = ({ data }) => { + const { type, message, phase, consuming }: TopicMessageEvent = + JSON.parse(data); + switch (type) { + case TopicMessageEventTypeEnum.MESSAGE: + if (message) { + addMessage({ + message, + prepend: isLive, + }); + } + break; + case TopicMessageEventTypeEnum.PHASE: + if (phase?.name) { + updatePhase(phase.name); + } + break; + case TopicMessageEventTypeEnum.CONSUMING: + if (consuming) updateMeta(consuming); + break; + case TopicMessageEventTypeEnum.DONE: + if (consuming && type) { + setMessageType(type); + updateMeta(consuming); + } + break; + default: + } + }; + + sse.onerror = () => { + setIsFetching(false); + sse.close(); + }; + + return () => { + setIsFetching(false); + sse.close(); + }; + } + }, [ + clusterName, + topicName, + seekDirection, + location, + addMessage, + resetMessages, + setIsFetching, + updateMeta, + updatePhase, + ]); + React.useEffect(() => { + if (location.search?.length === 0) { + handleFiltersSubmit(offset); + } + }, [ + seekDirection, + queryType, + activeFilter, + currentSeekType, + timestamp, + query, + location, + ]); + React.useEffect(() => { + handleFiltersSubmit(offset); + }, [ + seekDirection, + queryType, + activeFilter, + currentSeekType, + timestamp, + query, + seekDirection, + page, + ]); + + React.useEffect(() => { + setIsTailing(isLive); + }, [isLive]); + + const { data: serdes = {} } = useSerdes({ + clusterName, + topicName, + use: SerdeUsage.DESERIALIZE, + }); + + return ( + +
+ +
+ Seek Type + + setCurrentSeekType(option as SeekType)} + value={currentSeekType} + selectSize="M" + minWidth="100px" + options={SeekTypeOptions} + disabled={isTailing} + /> + + {currentSeekType === SeekType.OFFSET ? ( + setOffset(value)} + disabled={isTailing} + /> + ) : ( + setTimestamp(date)} + showTimeInput + timeInputLabel="Time:" + dateFormat="MMM d, yyyy HH:mm" + placeholderText="Select timestamp" + disabled={isTailing} + /> + )} + +
+
+ Partitions + ({ + label: `Partition #${p.partition.toString()}`, + value: p.partition, + }))} + filterOptions={filterOptions} + value={selectedPartitions} + onChange={setSelectedPartitions} + labelledBy="Select partitions" + disabled={isTailing} + /> +
+
+ Key Serde + setValueSerde(option as string)} + options={getSerdeOptions(serdes.value || [])} + value={searchParams.get('valueSerde') as string} + minWidth="170px" + selectSize="M" + disabled={isTailing} + /> +
+ Clear all + +
+ setField(target?.value)} + /> + {errors.includes('field') && 'Field is required'} +
+
+ Json path + setPath(target?.value)} + /> + + {errors.includes('path') && 'Json path is required'} + +
+ + + + + + ); +}; + +export default PreviewModal; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/FiltersContainer.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/FiltersContainer.spec.tsx new file mode 100644 index 00000000000..127a602f85e --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/FiltersContainer.spec.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import FiltersContainer from 'components/Topics/Topic/Messages/Filters/FiltersContainer'; +import { screen } from '@testing-library/react'; +import { render } from 'lib/testHelpers'; + +jest.mock('components/Topics/Topic/Messages/Filters/Filters', () => () => ( +
mock-Filters
+)); + +describe('FiltersContainer', () => { + it('renders Filters component', () => { + render(); + expect(screen.getByText('mock-Filters')).toBeInTheDocument(); + }); +}); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/Message.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/Message.spec.tsx new file mode 100644 index 00000000000..c96b395f4c7 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/Message.spec.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import { TopicMessage, TopicMessageTimestampTypeEnum } from 'generated-sources'; +import Message, { + PreviewFilter, + Props, +} from 'components/Topics/Topic/Messages/Message'; +import { screen } from '@testing-library/react'; +import { render } from 'lib/testHelpers'; +import userEvent from '@testing-library/user-event'; +import { formatTimestamp } from 'lib/dateTimeHelpers'; + +const messageContentText = 'messageContentText'; + +const keyTest = '{"payload":{"subreddit":"learnprogramming"}}'; +const contentTest = + '{"payload":{"author":"DwaywelayTOP","archived":false,"name":"t3_11jshwd","id":"11jshwd"}}'; +jest.mock( + 'components/Topics/Topic/Messages/MessageContent/MessageContent', + () => () => + ( + + {messageContentText} + + ) +); + +describe('Message component', () => { + const mockMessage: TopicMessage = { + timestamp: new Date(), + timestampType: TopicMessageTimestampTypeEnum.CREATE_TIME, + offset: 0, + key: 'test-key', + partition: 6, + content: '{"data": "test"}', + headers: { header: 'test' }, + }; + const mockKeyFilters: PreviewFilter = { + field: 'sub', + path: '$.payload.subreddit', + }; + const mockContentFilters: PreviewFilter = { + field: 'author', + path: '$.payload.author', + }; + const renderComponent = ( + props: Partial = { + message: mockMessage, + keyFilters: [], + contentFilters: [], + } + ) => + render( + + + + +
+ ); + + it('shows the data in the table row', () => { + renderComponent(); + expect(screen.getByText(mockMessage.content as string)).toBeInTheDocument(); + expect(screen.getByText(mockMessage.key as string)).toBeInTheDocument(); + expect( + screen.getByText(formatTimestamp(mockMessage.timestamp)) + ).toBeInTheDocument(); + expect(screen.getByText(mockMessage.offset.toString())).toBeInTheDocument(); + expect( + screen.getByText(mockMessage.partition.toString()) + ).toBeInTheDocument(); + }); + + it('check the useDataSaver functionality', () => { + const props = { message: { ...mockMessage } }; + delete props.message.content; + renderComponent(props); + expect( + screen.queryByText(mockMessage.content as string) + ).not.toBeInTheDocument(); + }); + + it('should check the dropdown being visible during hover', async () => { + renderComponent(); + const text = 'Save as a file'; + const trElement = screen.getByRole('row'); + expect(screen.queryByText(text)).not.toBeInTheDocument(); + + await userEvent.hover(trElement); + expect(screen.getByText(text)).toBeInTheDocument(); + + await userEvent.unhover(trElement); + expect(screen.queryByText(text)).not.toBeInTheDocument(); + }); + + it('should check open Message Content functionality', async () => { + renderComponent(); + const messageToggleIcon = screen.getByRole('button', { hidden: true }); + expect(screen.queryByText(messageContentText)).not.toBeInTheDocument(); + await userEvent.click(messageToggleIcon); + expect(screen.getByText(messageContentText)).toBeInTheDocument(); + }); + + it('should check if Preview filter showing for key', () => { + const props = { + message: { ...mockMessage, key: keyTest as string }, + keyFilters: [mockKeyFilters], + }; + renderComponent(props); + const keyFiltered = screen.getByText('sub: "learnprogramming"'); + expect(keyFiltered).toBeInTheDocument(); + }); + + it('should check if Preview filter showing for Value', () => { + const props = { + message: { ...mockMessage, content: contentTest as string }, + contentFilters: [mockContentFilters], + }; + renderComponent(props); + const keyFiltered = screen.getByText('author: "DwaywelayTOP"'); + expect(keyFiltered).toBeInTheDocument(); + }); +}); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/Messages.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/Messages.spec.tsx new file mode 100644 index 00000000000..172dc810135 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/Messages.spec.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import { render, EventSourceMock, WithRoute } from 'lib/testHelpers'; +import Messages, { + SeekDirectionOptions, + SeekDirectionOptionsObj, +} from 'components/Topics/Topic/Messages/Messages'; +import { SeekDirection, SeekType } from 'generated-sources'; +import userEvent from '@testing-library/user-event'; +import { clusterTopicMessagesPath } from 'lib/paths'; +import { useSerdes } from 'lib/hooks/api/topicMessages'; +import { serdesPayload } from 'lib/fixtures/topicMessages'; +import { useTopicDetails } from 'lib/hooks/api/topics'; +import { externalTopicPayload } from 'lib/fixtures/topics'; + +jest.mock('lib/hooks/api/topicMessages', () => ({ + useSerdes: jest.fn(), +})); + +jest.mock('lib/hooks/api/topics', () => ({ + useTopicDetails: jest.fn(), +})); + +describe('Messages', () => { + const searchParams = `?filterQueryType=STRING_CONTAINS&attempt=0&limit=100&seekDirection=${SeekDirection.FORWARD}&seekType=${SeekType.OFFSET}&seekTo=0::9`; + const renderComponent = (param: string = searchParams) => { + const query = new URLSearchParams(param).toString(); + const path = `${clusterTopicMessagesPath()}?${query}`; + return render( + + + , + { + initialEntries: [path], + } + ); + }; + + beforeEach(() => { + Object.defineProperty(window, 'EventSource', { + value: EventSourceMock, + }); + (useSerdes as jest.Mock).mockImplementation(() => ({ + data: serdesPayload, + })); + (useTopicDetails as jest.Mock).mockImplementation(() => ({ + data: externalTopicPayload, + })); + }); + describe('component rendering default behavior with the search params', () => { + beforeEach(() => { + renderComponent(); + }); + it('should check default seekDirection if it actually take the value from the url', () => { + expect(screen.getAllByRole('listbox')[3]).toHaveTextContent( + SeekDirectionOptionsObj[SeekDirection.FORWARD].label + ); + }); + + it('should check the SeekDirection select changes with live option', async () => { + const seekDirectionSelect = screen.getAllByRole('listbox')[3]; + const seekDirectionOption = screen.getAllByRole('option')[3]; + + expect(seekDirectionOption).toHaveTextContent( + SeekDirectionOptionsObj[SeekDirection.FORWARD].label + ); + + const labelValue1 = SeekDirectionOptions[1].label; + await userEvent.click(seekDirectionSelect); + await userEvent.selectOptions(seekDirectionSelect, [labelValue1]); + expect(seekDirectionOption).toHaveTextContent(labelValue1); + + const labelValue0 = SeekDirectionOptions[0].label; + await userEvent.click(seekDirectionSelect); + await userEvent.selectOptions(seekDirectionSelect, [labelValue0]); + expect(seekDirectionOption).toHaveTextContent(labelValue0); + + const liveOptionConf = SeekDirectionOptions[2]; + const labelValue2 = liveOptionConf.label; + await userEvent.click(seekDirectionSelect); + + const options = screen.getAllByRole('option'); + const liveModeLi = options.find( + (option) => option.getAttribute('value') === liveOptionConf.value + ); + expect(liveModeLi).toBeInTheDocument(); + if (!liveModeLi) return; // to make TS happy + await userEvent.selectOptions(seekDirectionSelect, [liveModeLi]); + expect(seekDirectionOption).toHaveTextContent(labelValue2); + + await waitFor(() => { + expect(screen.getByRole('contentLoader')).toBeInTheDocument(); + }); + }); + }); + + describe('Component rendering with custom Url search params', () => { + it('reacts to a change of seekDirection in the url which make the select pick up different value', () => { + renderComponent( + searchParams.replace(SeekDirection.FORWARD, SeekDirection.BACKWARD) + ); + expect(screen.getAllByRole('listbox')[3]).toHaveTextContent( + SeekDirectionOptionsObj[SeekDirection.BACKWARD].label + ); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/MessagesTable.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/MessagesTable.spec.tsx new file mode 100644 index 00000000000..7b1e80f8c27 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/MessagesTable.spec.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { render } from 'lib/testHelpers'; +import MessagesTable from 'components/Topics/Topic/Messages/MessagesTable'; +import { SeekDirection, SeekType, TopicMessage } from 'generated-sources'; +import TopicMessagesContext, { + ContextProps, +} from 'components/contexts/TopicMessagesContext'; +import { + topicMessagePayload, + topicMessagesMetaPayload, +} from 'redux/reducers/topicMessages/__test__/fixtures'; + +const mockTopicsMessages: TopicMessage[] = [{ ...topicMessagePayload }]; + +const mockNavigate = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, +})); + +describe('MessagesTable', () => { + const searchParams = new URLSearchParams({ + filterQueryType: 'STRING_CONTAINS', + attempt: '0', + limit: '100', + seekDirection: SeekDirection.FORWARD, + seekType: SeekType.OFFSET, + seekTo: '0::9', + }); + const contextValue: ContextProps = { + isLive: false, + seekDirection: SeekDirection.FORWARD, + changeSeekDirection: jest.fn(), + }; + + const renderComponent = ( + params: URLSearchParams = searchParams, + ctx: ContextProps = contextValue, + messages: TopicMessage[] = [], + isFetching?: boolean, + path?: string + ) => { + const customPath = path || params.toString(); + return render( + + + , + { + initialEntries: [`/messages?${customPath}`], + preloadedState: { + topicMessages: { + messages, + meta: { + ...topicMessagesMetaPayload, + }, + isFetching: !!isFetching, + }, + }, + } + ); + }; + + describe('Default props Setup for MessagesTable component', () => { + beforeEach(() => { + renderComponent(); + }); + + it('should check the render', () => { + expect(screen.getByRole('table')).toBeInTheDocument(); + }); + + it('should check preview buttons', async () => { + const previewButtons = await screen.findAllByRole('button', { + name: 'Preview', + }); + expect(previewButtons).toHaveLength(2); + }); + + it('should show preview modal with validation', async () => { + await userEvent.click(screen.getAllByText('Preview')[0]); + expect(screen.getByPlaceholderText('Field')).toHaveValue(''); + expect(screen.getByPlaceholderText('Json Path')).toHaveValue(''); + }); + + it('should check the if no elements is rendered in the table', () => { + expect(screen.getByText(/No messages found/i)).toBeInTheDocument(); + }); + }); + + describe('Custom Setup with different props value', () => { + it('should check if next button and previous is disabled isLive Param', () => { + renderComponent(searchParams, { ...contextValue, isLive: true }); + expect(screen.queryByText(/next/i)).toBeDisabled(); + expect(screen.queryByText(/back/i)).toBeDisabled(); + }); + + it('should check the display of the loader element', () => { + renderComponent( + searchParams, + { ...contextValue, isLive: true }, + [], + true + ); + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + }); + }); + + describe('should render Messages table with data', () => { + beforeEach(() => { + renderComponent(searchParams, { ...contextValue }, mockTopicsMessages); + }); + + it('should check the rendering of the messages', () => { + expect(screen.queryByText(/No messages found/i)).not.toBeInTheDocument(); + expect( + screen.getByText(mockTopicsMessages[0].content as string) + ).toBeInTheDocument(); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/PreviewModal.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/PreviewModal.spec.tsx new file mode 100644 index 00000000000..fbea81562ee --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/PreviewModal.spec.tsx @@ -0,0 +1,112 @@ +import userEvent from '@testing-library/user-event'; +import { act, screen } from '@testing-library/react'; +import { render } from 'lib/testHelpers'; +import React from 'react'; +import { PreviewFilter } from 'components/Topics/Topic/Messages/Message'; +import { serdesPayload } from 'lib/fixtures/topicMessages'; +import { useSerdes } from 'lib/hooks/api/topicMessages'; +import PreviewModal, { + InfoModalProps, +} from 'components/Topics/Topic/Messages/PreviewModal'; + +jest.mock('components/common/Icons/CloseIcon', () => () => 'mock-CloseIcon'); + +jest.mock('lib/hooks/api/topicMessages', () => ({ + useSerdes: jest.fn(), +})); + +beforeEach(async () => { + (useSerdes as jest.Mock).mockImplementation(() => ({ + data: serdesPayload, + })); +}); + +const toggleInfoModal = jest.fn(); +const mockValues: PreviewFilter[] = [ + { + field: '', + path: '', + }, +]; + +const renderComponent = (props?: Partial) => { + render( + + ); +}; + +describe('PreviewModal component', () => { + it('closes PreviewModal', async () => { + renderComponent(); + await userEvent.click(screen.getByRole('button', { name: 'Close' })); + expect(toggleInfoModal).toHaveBeenCalledTimes(1); + }); + + it('return if empty inputs', async () => { + renderComponent(); + await userEvent.click(screen.getByRole('button', { name: 'Save' })); + expect(screen.getByText('Json path is required')).toBeInTheDocument(); + expect(screen.getByText('Field is required')).toBeInTheDocument(); + }); + + describe('Input elements', () => { + const fieldValue = 'type'; + const pathValue = 'schema.type'; + + beforeEach(async () => { + await act(() => { + renderComponent(); + }); + }); + + it('field input', async () => { + const fieldInput = screen.getByPlaceholderText('Field'); + expect(fieldInput).toHaveValue(''); + await userEvent.type(fieldInput, fieldValue); + expect(fieldInput).toHaveValue(fieldValue); + }); + + it('path input', async () => { + const pathInput = screen.getByPlaceholderText('Json Path'); + expect(pathInput).toHaveValue(''); + await userEvent.type(pathInput, pathValue); + expect(pathInput).toHaveValue(pathValue.toString()); + }); + }); + + describe('edit and remove functionality', () => { + const fieldValue = 'type new'; + const pathValue = 'schema.type.new'; + + it('remove values', async () => { + const setFilters = jest.fn(); + await act(() => { + renderComponent({ setFilters }); + }); + await userEvent.click(screen.getByRole('button', { name: 'Cancel' })); + expect(setFilters).toHaveBeenCalledTimes(1); + }); + + it('edit values', async () => { + const setFilters = jest.fn(); + const toggleIsOpen = jest.fn(); + await act(() => { + renderComponent({ setFilters }); + }); + userEvent.click(screen.getByRole('button', { name: 'Edit' })); + const fieldInput = screen.getByPlaceholderText('Field'); + userEvent.type(fieldInput, fieldValue); + const pathInput = screen.getByPlaceholderText('Json Path'); + userEvent.type(pathInput, pathValue); + userEvent.click(screen.getByRole('button', { name: 'Save' })); + await act(() => { + renderComponent({ setFilters, toggleIsOpen }); + }); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/utils.spec.ts b/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/utils.spec.ts new file mode 100644 index 00000000000..97dd0ec7bdd --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/utils.spec.ts @@ -0,0 +1,120 @@ +import { Option } from 'react-multi-select-component'; +import { + filterOptions, + getOffsetFromSeekToParam, + getTimestampFromSeekToParam, + getSelectedPartitionsFromSeekToParam, +} from 'components/Topics/Topic/Messages/Filters/utils'; +import { SeekType, Partition } from 'generated-sources'; + +const options: Option[] = [ + { + value: 0, + label: 'Partition #0', + }, + { + value: 1, + label: 'Partition #1', + }, + { + value: 11, + label: 'Partition #11', + }, + { + value: 21, + label: 'Partition #21', + }, +]; + +let paramsString; +let searchParams = new URLSearchParams(paramsString); + +describe('utils', () => { + describe('filterOptions', () => { + it('returns options if no filter is defined', () => { + expect(filterOptions(options, '')).toEqual(options); + }); + + it('returns filtered options', () => { + expect(filterOptions(options, '11')).toEqual([options[2]]); + }); + }); + + describe('getOffsetFromSeekToParam', () => { + beforeEach(() => { + paramsString = 'seekTo=0::123,1::123,2::0'; + searchParams = new URLSearchParams(paramsString); + }); + + it('returns nothing when param "seekType" is equal BEGGINING', () => { + searchParams.set('seekType', SeekType.BEGINNING); + expect(getOffsetFromSeekToParam(searchParams)).toEqual(''); + }); + + it('returns nothing when param "seekType" is equal TIMESTAMP', () => { + searchParams.set('seekType', SeekType.TIMESTAMP); + expect(getOffsetFromSeekToParam(searchParams)).toEqual(''); + }); + + it('returns correct messages list when param "seekType" is equal OFFSET', () => { + searchParams.set('seekType', SeekType.OFFSET); + expect(getOffsetFromSeekToParam(searchParams)).toEqual('123'); + }); + + it('returns 0 when param "seekTo" is not defined and param "seekType" is equal OFFSET', () => { + searchParams.set('seekType', SeekType.OFFSET); + searchParams.delete('seekTo'); + expect(getOffsetFromSeekToParam(searchParams)).toEqual('0'); + }); + }); + + describe('getTimestampFromSeekToParam', () => { + beforeEach(() => { + paramsString = `seekTo=0::1627333200000,1::1627333200000`; + searchParams = new URLSearchParams(paramsString); + }); + + it('returns null when param "seekType" is equal BEGGINING', () => { + searchParams.set('seekType', SeekType.BEGINNING); + expect(getTimestampFromSeekToParam(searchParams)).toEqual(null); + }); + it('returns null when param "seekType" is equal OFFSET', () => { + searchParams.set('seekType', SeekType.OFFSET); + expect(getTimestampFromSeekToParam(searchParams)).toEqual(null); + }); + it('returns correct messages list when param "seekType" is equal TIMESTAMP', () => { + searchParams.set('seekType', SeekType.TIMESTAMP); + expect(getTimestampFromSeekToParam(searchParams)).toEqual( + new Date(1627333200000) + ); + }); + it('returns default timestamp when param "seekTo" is empty and param "seekType" is equal TIMESTAMP', () => { + searchParams.set('seekType', SeekType.TIMESTAMP); + searchParams.delete('seekTo'); + expect(getTimestampFromSeekToParam(searchParams)).toEqual(new Date(0)); + }); + }); + + describe('getSelectedPartitionsFromSeekToParam', () => { + const part: Partition[] = [{ partition: 42, offsetMin: 0, offsetMax: 100 }]; + + it('returns parsed partition from params when partition list includes selected partition', () => { + searchParams.set('seekTo', '42::0'); + expect(getSelectedPartitionsFromSeekToParam(searchParams, part)).toEqual([ + { label: 'Partition #42', value: 42 }, + ]); + }); + it('returns parsed partition from params when partition list NOT includes selected partition', () => { + searchParams.set('seekTo', '24::0'); + expect(getSelectedPartitionsFromSeekToParam(searchParams, part)).toEqual( + [] + ); + }); + it('returns partitions when param "seekTo" is not defined', () => { + searchParams.delete('seekTo'); + expect(getSelectedPartitionsFromSeekToParam(searchParams, part)).toEqual([ + { label: 'Partition #42', value: 42 }, + ]); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/getDefaultSerdeName.ts b/kafka-ui-react-app/src/components/Topics/Topic/Messages/getDefaultSerdeName.ts new file mode 100644 index 00000000000..a5235e9ac5b --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/getDefaultSerdeName.ts @@ -0,0 +1,13 @@ +import { SerdeDescription } from 'generated-sources'; +import { getPreferredDescription } from 'components/Topics/Topic/SendMessage/utils'; + +export const getDefaultSerdeName = (serdes: SerdeDescription[]) => { + const preffered = getPreferredDescription(serdes); + if (preffered) { + return preffered.name || ''; + } + if (serdes.length > 0) { + return serdes[0].name || ''; + } + return ''; +}; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Overview/ActionsCell.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Overview/ActionsCell.tsx new file mode 100644 index 00000000000..19e03a1411c --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Overview/ActionsCell.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Action, Partition, ResourceType } from 'generated-sources'; +import { CellContext } from '@tanstack/react-table'; +import ClusterContext from 'components/contexts/ClusterContext'; +import { RouteParamsClusterTopic } from 'lib/paths'; +import useAppParams from 'lib/hooks/useAppParams'; +import { Dropdown } from 'components/common/Dropdown'; +import { useClearTopicMessages, useTopicDetails } from 'lib/hooks/api/topics'; +import { ActionDropdownItem } from 'components/common/ActionComponent'; + +const ActionsCell: React.FC> = ({ row }) => { + const { clusterName, topicName } = useAppParams(); + const { data } = useTopicDetails({ clusterName, topicName }); + const { isReadOnly } = React.useContext(ClusterContext); + const { partition } = row.original; + + const clearMessages = useClearTopicMessages(clusterName, [partition]); + + const clearTopicMessagesHandler = async () => { + await clearMessages.mutateAsync(topicName); + }; + const disabled = + data?.internal || isReadOnly || data?.cleanUpPolicy !== 'DELETE'; + return ( + + + Clear Messages + + + ); +}; + +export default ActionsCell; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Overview/Overview.styled.ts b/kafka-ui-react-app/src/components/Topics/Topic/Overview/Overview.styled.ts new file mode 100644 index 00000000000..d274a1ba9d9 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Overview/Overview.styled.ts @@ -0,0 +1,22 @@ +import styled from 'styled-components'; + +export const Replica = styled.span.attrs({ 'aria-label': 'replica-info' })<{ + leader?: boolean; + outOfSync?: boolean; +}>` + color: ${({ leader, outOfSync, theme }) => { + if (outOfSync) return theme.topicMetaData.outOfSync.color; + if (leader) return theme.topicMetaData.liderReplica.color; + return null; + }}; + + font-weight: ${({ outOfSync }) => (outOfSync ? '500' : null)}; + + &:after { + content: ', '; + } + + &:last-child::after { + content: ''; + } +`; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Overview/Overview.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Overview/Overview.tsx new file mode 100644 index 00000000000..c639ac2b625 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Overview/Overview.tsx @@ -0,0 +1,157 @@ +import React from 'react'; +import type { Partition, Replica } from 'generated-sources'; +import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted'; +import Table from 'components/common/NewTable'; +import * as Metrics from 'components/common/Metrics'; +import { Tag } from 'components/common/Tag/Tag.styled'; +import { RouteParamsClusterTopic } from 'lib/paths'; +import useAppParams from 'lib/hooks/useAppParams'; +import { useTopicDetails } from 'lib/hooks/api/topics'; +import { ColumnDef } from '@tanstack/react-table'; + +import * as S from './Overview.styled'; +import ActionsCell from './ActionsCell'; + +const Overview: React.FC = () => { + const { clusterName, topicName } = useAppParams(); + const { data } = useTopicDetails({ clusterName, topicName }); + + const messageCount = React.useMemo( + () => + (data?.partitions || []).reduce((memo, partition) => { + return memo + partition.offsetMax - partition.offsetMin; + }, 0), + [data] + ); + const newData = React.useMemo(() => { + if (!data?.partitions) return []; + + return data.partitions.map((items: Partition) => { + return { + ...items, + messageCount: items.offsetMax - items.offsetMin, + }; + }); + }, [data?.partitions]); + + const columns = React.useMemo[]>( + () => [ + { + header: 'Partition ID', + enableSorting: false, + accessorKey: 'partition', + }, + { + header: 'Replicas', + enableSorting: false, + + accessorKey: 'replicas', + cell: ({ getValue }) => { + const replicas = getValue(); + if (replicas === undefined || replicas.length === 0) { + return 0; + } + return replicas?.map(({ broker, leader, inSync }: Replica) => ( + + {broker} + + )); + }, + }, + { + header: 'First Offset', + enableSorting: false, + accessorKey: 'offsetMin', + }, + { header: 'Next Offset', enableSorting: false, accessorKey: 'offsetMax' }, + { + header: 'Message Count', + enableSorting: false, + accessorKey: `messageCount`, + }, + { + header: '', + enableSorting: false, + accessorKey: 'actions', + cell: ActionsCell, + }, + ], + [] + ); + return ( + <> + + + + {data?.partitionCount} + + + {data?.replicationFactor} + + + {data?.underReplicatedPartitions === 0 ? ( + + {data?.underReplicatedPartitions} + + ) : ( + + {data?.underReplicatedPartitions} + + )} + + + {data?.inSyncReplicas && + data?.replicas && + data?.inSyncReplicas < data?.replicas ? ( + {data?.inSyncReplicas} + ) : ( + data?.inSyncReplicas + )} + of {data?.replicas} + + + {data?.internal ? 'Internal' : 'External'} + + + + + + {data?.segmentCount} + + + {data?.cleanUpPolicy || 'Unknown'} + + + {messageCount} + + + + + + ); +}; + +export default Overview; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Overview/__test__/Overview.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Overview/__test__/Overview.spec.tsx new file mode 100644 index 00000000000..bf762a002b3 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Overview/__test__/Overview.spec.tsx @@ -0,0 +1,161 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; +import { render, WithRoute } from 'lib/testHelpers'; +import Overview from 'components/Topics/Topic/Overview/Overview'; +import { theme } from 'theme/theme'; +import { CleanUpPolicy, Topic } from 'generated-sources'; +import ClusterContext from 'components/contexts/ClusterContext'; +import userEvent from '@testing-library/user-event'; +import { clusterTopicPath } from 'lib/paths'; +import { Replica } from 'components/Topics/Topic/Overview/Overview.styled'; +import { useClearTopicMessages, useTopicDetails } from 'lib/hooks/api/topics'; +import { + externalTopicPayload, + internalTopicPayload, +} from 'lib/fixtures/topics'; + +const clusterName = 'local'; +const topicName = 'topic'; +const defaultContextValues = { + isReadOnly: false, + hasKafkaConnectConfigured: true, + hasSchemaRegistryConfigured: true, + isTopicDeletionAllowed: true, +}; + +jest.mock('lib/hooks/api/topics', () => ({ + useTopicDetails: jest.fn(), + useClearTopicMessages: jest.fn(), +})); + +const clearTopicMessage = jest.fn(); + +describe('Overview', () => { + const renderComponent = ( + topic: Topic = externalTopicPayload, + context = defaultContextValues + ) => { + (useTopicDetails as jest.Mock).mockImplementation(() => ({ + data: topic, + })); + (useClearTopicMessages as jest.Mock).mockImplementation(() => ({ + mutateAsync: clearTopicMessage, + })); + const path = clusterTopicPath(clusterName, topicName); + return render( + + + + + , + { initialEntries: [path] } + ); + }; + + it('at least one replica was rendered', () => { + renderComponent(); + expect(screen.getByLabelText('replica-info')).toBeInTheDocument(); + }); + + it('renders replica cell with props', () => { + render(); + const element = screen.getByLabelText('replica-info'); + expect(element).toBeInTheDocument(); + expect(element).toHaveStyleRule( + 'color', + theme.topicMetaData.liderReplica.color + ); + }); + + describe('when replicas out of sync', () => { + it('should be the appropriate color', () => { + render(); + const element = screen.getByLabelText('replica-info'); + expect(element).toBeInTheDocument(); + expect(element).toHaveStyleRule( + 'color', + theme.topicMetaData.outOfSync.color + ); + expect(element).toHaveStyleRule('font-weight', '500'); + }); + }); + + describe('when it has internal flag', () => { + it('renders the Action button for Topic', () => { + renderComponent({ + ...externalTopicPayload, + cleanUpPolicy: CleanUpPolicy.DELETE, + }); + expect(screen.getAllByLabelText('Dropdown Toggle').length).toEqual(1); + }); + + it('does not render Partitions', () => { + renderComponent({ ...externalTopicPayload, partitions: [] }); + expect(screen.getByText('No Partitions found')).toBeInTheDocument(); + }); + }); + + describe('should render circular alert', () => { + it('should be in document', () => { + renderComponent(); + const circles = screen.getAllByRole('circle'); + expect(circles.length).toEqual(2); + }); + + it('should be the appropriate color', () => { + renderComponent({ + ...externalTopicPayload, + underReplicatedPartitions: 0, + inSyncReplicas: 1, + replicas: 2, + }); + const circles = screen.getAllByRole('circle'); + expect(circles[0]).toHaveStyle( + `fill: ${theme.circularAlert.color.success}` + ); + expect(circles[1]).toHaveStyle( + `fill: ${theme.circularAlert.color.error}` + ); + }); + }); + + describe('when Clear Messages is clicked', () => { + it('should when Clear Messages is clicked', async () => { + renderComponent({ + ...externalTopicPayload, + cleanUpPolicy: CleanUpPolicy.DELETE, + }); + + const clearMessagesButton = screen.getByText('Clear Messages'); + await userEvent.click(clearMessagesButton); + expect(clearTopicMessage).toHaveBeenCalledTimes(1); + }); + }); + + describe('when the table partition dropdown appearance', () => { + it('should check if the dropdown is disabled when it is readOnly', () => { + renderComponent( + { + ...externalTopicPayload, + }, + { ...defaultContextValues, isReadOnly: true } + ); + expect(screen.getByLabelText('Dropdown Toggle')).toBeDisabled(); + }); + + it('should check if the dropdown is disabled when it is internal', () => { + renderComponent({ + ...internalTopicPayload, + }); + expect(screen.getByLabelText('Dropdown Toggle')).toBeDisabled(); + }); + + it('should check if the dropdown is disabled when cleanUpPolicy is not DELETE', () => { + renderComponent({ + ...externalTopicPayload, + cleanUpPolicy: CleanUpPolicy.COMPACT, + }); + expect(screen.getByLabelText('Dropdown Toggle')).toBeDisabled(); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.styled.tsx b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.styled.tsx index c4c32d5d464..d2750abf7d1 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.styled.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.styled.tsx @@ -2,6 +2,35 @@ import styled from 'styled-components'; export const Wrapper = styled.div` display: block; - padding: 1.25rem; border-radius: 6px; `; + +export const Columns = styled.div` + margin: -0.75rem; + margin-bottom: 0.75rem; + display: flex; + flex-direction: column; + padding: 0.75rem; + gap: 8px; + + @media screen and (min-width: 769px) { + display: flex; + } +`; +export const Flex = styled.div` + display: flex; + flex-direction: row; + gap: 8px; + @media screen and (max-width: 1200px) { + flex-direction: column; + } +`; +export const FlexItem = styled.div` + width: 18rem; + @media screen and (max-width: 1450px) { + width: 50%; + } + @media screen and (max-width: 1200px) { + width: 100%; + } +`; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx index 567e094e8aa..bef7a4dddb5 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx @@ -1,202 +1,206 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { useForm, Controller } from 'react-hook-form'; -import { useNavigate } from 'react-router-dom'; -import { - clusterTopicMessagesRelativePath, - RouteParamsClusterTopic, -} from 'lib/paths'; -import jsf from 'json-schema-faker'; -import { messagesApiClient } from 'redux/reducers/topicMessages/topicMessagesSlice'; -import { - fetchTopicMessageSchema, - fetchTopicDetails, -} from 'redux/reducers/topics/topicsSlice'; -import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; -import { alertAdded } from 'redux/reducers/alerts/alertsSlice'; -import { now } from 'lodash'; +import { RouteParamsClusterTopic } from 'lib/paths'; import { Button } from 'components/common/Button/Button'; import Editor from 'components/common/Editor/Editor'; -import PageLoader from 'components/common/PageLoader/PageLoader'; -import { - getMessageSchemaByTopicName, - getPartitionsByTopicName, - getTopicMessageSchemaFetched, -} from 'redux/reducers/topics/selectors'; import Select, { SelectOption } from 'components/common/Select/Select'; +import Switch from 'components/common/Switch/Switch'; import useAppParams from 'lib/hooks/useAppParams'; +import { showAlert } from 'lib/errorHandling'; +import { useSendMessage, useTopicDetails } from 'lib/hooks/api/topics'; +import { InputLabel } from 'components/common/Input/InputLabel.styled'; +import { useSerdes } from 'lib/hooks/api/topicMessages'; +import { SerdeUsage } from 'generated-sources'; -import validateMessage from './validateMessage'; import * as S from './SendMessage.styled'; +import { + getDefaultValues, + getPartitionOptions, + getSerdeOptions, + validateBySchema, +} from './utils'; -type FieldValues = Partial<{ +interface FormType { key: string; content: string; headers: string; - partition: number | string; -}>; + partition: number; + keySerde: string; + valueSerde: string; + keepContents: boolean; +} -const SendMessage: React.FC = () => { - const dispatch = useAppDispatch(); +const SendMessage: React.FC<{ closeSidebar: () => void }> = ({ + closeSidebar, +}) => { const { clusterName, topicName } = useAppParams(); - const navigate = useNavigate(); - - jsf.option('fillProperties', false); - jsf.option('alwaysFakeOptionals', true); - - React.useEffect(() => { - dispatch(fetchTopicMessageSchema({ clusterName, topicName })); - }, [clusterName, dispatch, topicName]); - - const messageSchema = useAppSelector((state) => - getMessageSchemaByTopicName(state, topicName) - ); - const partitions = useAppSelector((state) => - getPartitionsByTopicName(state, topicName) - ); - const schemaIsFetched = useAppSelector(getTopicMessageSchemaFetched); - const selectPartitionOptions: Array = partitions.map((p) => { - const value = String(p.partition); - return { value, label: value }; + const { data: topic } = useTopicDetails({ clusterName, topicName }); + const { data: serdes = {} } = useSerdes({ + clusterName, + topicName, + use: SerdeUsage.SERIALIZE, }); + const sendMessage = useSendMessage({ clusterName, topicName }); - const keyDefaultValue = React.useMemo(() => { - if (!schemaIsFetched || !messageSchema) { - return undefined; - } - return JSON.stringify( - jsf.generate(JSON.parse(messageSchema.key.schema)), - null, - '\t' - ); - }, [messageSchema, schemaIsFetched]); - - const contentDefaultValue = React.useMemo(() => { - if (!schemaIsFetched || !messageSchema) { - return undefined; - } - return JSON.stringify( - jsf.generate(JSON.parse(messageSchema.value.schema)), - null, - '\t' - ); - }, [messageSchema, schemaIsFetched]); - + const defaultValues = React.useMemo(() => getDefaultValues(serdes), [serdes]); + const partitionOptions: SelectOption[] = React.useMemo( + () => getPartitionOptions(topic?.partitions || []), + [topic] + ); const { handleSubmit, - formState: { isSubmitting, isDirty }, + formState: { isSubmitting }, control, - reset, - } = useForm({ + setValue, + } = useForm({ mode: 'onChange', defaultValues: { - key: keyDefaultValue, - content: contentDefaultValue, - headers: undefined, - partition: undefined, + ...defaultValues, + partition: Number(partitionOptions[0].value), + keepContents: false, }, }); - useEffect(() => { - reset({ - key: keyDefaultValue, - content: contentDefaultValue, - }); - }, [keyDefaultValue, contentDefaultValue, reset]); + const submit = async ({ + keySerde, + valueSerde, + key, + content, + headers, + partition, + keepContents, + }: FormType) => { + let errors: string[] = []; - const onSubmit = async (data: { - key: string; - content: string; - headers: string; - partition: number; - }) => { - if (messageSchema) { - const { partition, key, content } = data; - const errors = validateMessage(key, content, messageSchema); - if (data.headers) { - try { - JSON.parse(data.headers); - } catch (error) { - errors.push('Wrong header format'); - } - } - if (errors.length > 0) { - const errorsHtml = errors.map((e) => `
  • ${e}
  • `).join(''); - dispatch( - alertAdded({ - id: `${clusterName}-${topicName}-createTopicMessageError`, - type: 'error', - title: 'Validation Error', - message: `
      ${errorsHtml}
    `, - createdAt: now(), - }) - ); - return; - } - const headers = data.headers ? JSON.parse(data.headers) : undefined; + if (keySerde) { + const selectedKeySerde = serdes.key?.find((k) => k.name === keySerde); + errors = validateBySchema(key, selectedKeySerde?.schema, 'key'); + } + + if (valueSerde) { + const selectedValue = serdes.value?.find((v) => v.name === valueSerde); + errors = [ + ...errors, + ...validateBySchema(content, selectedValue?.schema, 'content'), + ]; + } + + let parsedHeaders; + if (headers) { try { - await messagesApiClient.sendTopicMessages({ - clusterName, - topicName, - createTopicMessage: { - key: !key ? null : key, - content: !content ? null : content, - headers, - partition, - }, - }); - dispatch(fetchTopicDetails({ clusterName, topicName })); - } catch (e) { - dispatch( - alertAdded({ - id: `${clusterName}-${topicName}-sendTopicMessagesError`, - type: 'error', - title: `Error in sending a message to ${topicName}`, - message: e?.message, - createdAt: now(), - }) - ); + parsedHeaders = JSON.parse(headers); + } catch (error) { + errors.push('Wrong header format'); + } + } + + if (errors.length > 0) { + showAlert('error', { + id: `${clusterName}-${topicName}-createTopicMessageError`, + title: 'Validation Error', + message: ( +
      + {errors.map((e) => ( +
    • {e}
    • + ))} +
    + ), + }); + return; + } + try { + await sendMessage.mutateAsync({ + key: key || null, + content: content || null, + headers: parsedHeaders, + partition: partition || 0, + keySerde, + valueSerde, + }); + if (!keepContents) { + setValue('key', defaultValues.key || ''); + setValue('content', defaultValues.content || ''); + closeSidebar(); } - navigate(`../${clusterTopicMessagesRelativePath}`); + } catch (e) { + // do nothing } }; - if (!schemaIsFetched) { - return ; - } return ( -
    -
    -
    - + + + + Partition ( + render={({ field: { name, onChange, value } }) => ( + )} + /> + + + Value Serde + ( +
    ; +}; + +export default Settings; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Settings/__test__/Settings.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Settings/__test__/Settings.spec.tsx new file mode 100644 index 00000000000..c2aa9a93e5d --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Settings/__test__/Settings.spec.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { render, WithRoute } from 'lib/testHelpers'; +import { screen } from '@testing-library/react'; +import Settings from 'components/Topics/Topic/Settings/Settings'; +import { clusterTopicSettingsPath } from 'lib/paths'; +import { topicConfigPayload } from 'lib/fixtures/topics'; +import { useTopicConfig } from 'lib/hooks/api/topics'; + +const clusterName = 'Cluster_Name'; +const topicName = 'Topic_Name'; + +jest.mock('lib/hooks/api/topics', () => ({ + useTopicConfig: jest.fn(), +})); + +const getName = () => screen.getByText('compression.type'); +const getValue = () => screen.getByText('producer'); + +describe('Settings', () => { + const renderComponent = () => { + const path = clusterTopicSettingsPath(clusterName, topicName); + return render( + + + , + { initialEntries: [path] } + ); + }; + + beforeEach(() => { + (useTopicConfig as jest.Mock).mockImplementation(() => ({ + data: topicConfigPayload, + })); + renderComponent(); + }); + + it('renders without CustomValue', () => { + expect(getName()).toBeInTheDocument(); + expect(getName()).toHaveStyle('font-weight: 400'); + expect(getValue()).toBeInTheDocument(); + expect(getValue()).toHaveStyle('font-weight: 400'); + }); +}); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Indicators/SizeStats.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Indicators/SizeStats.tsx new file mode 100644 index 00000000000..a74a0bd24ed --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Indicators/SizeStats.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import * as Metrics from 'components/common/Metrics'; +import { TopicAnalysisSizeStats } from 'generated-sources'; +import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted'; + +const SizeStats: React.FC<{ + stats: TopicAnalysisSizeStats; + title: string; +}> = ({ + stats: { sum, min, max, avg, prctl50, prctl75, prctl95, prctl99, prctl999 }, + title, +}) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +export default SizeStats; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Indicators/Total.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Indicators/Total.tsx new file mode 100644 index 00000000000..65e5892b41b --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Indicators/Total.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import * as Metrics from 'components/common/Metrics'; +import { TopicAnalysisStats } from 'generated-sources'; +import { formatTimestamp } from 'lib/dateTimeHelpers'; + +const Total: React.FC = ({ + totalMsgs, + minOffset, + maxOffset, + minTimestamp, + maxTimestamp, + nullKeys, + nullValues, + approxUniqKeys, + approxUniqValues, +}) => { + return ( + + {totalMsgs} + + {`${minOffset} - ${maxOffset}`} + + + {`${formatTimestamp(minTimestamp)} - ${formatTimestamp(maxTimestamp)}`} + + {nullKeys} + + {approxUniqKeys} + + {nullValues} + + {approxUniqValues} + + + ); +}; + +export default Total; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Metrics.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Metrics.tsx new file mode 100644 index 00000000000..69da71c31bf --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Metrics.tsx @@ -0,0 +1,131 @@ +import React, { useEffect, useState } from 'react'; +import { + useAnalyzeTopic, + useCancelTopicAnalysis, + useTopicAnalysis, +} from 'lib/hooks/api/topics'; +import useAppParams from 'lib/hooks/useAppParams'; +import { RouteParamsClusterTopic } from 'lib/paths'; +import * as Informers from 'components/common/Metrics'; +import ProgressBar from 'components/common/ProgressBar/ProgressBar'; +import { + List, + Label, +} from 'components/common/PropertiesList/PropertiesList.styled'; +import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted'; +import { calculateTimer, formatTimestamp } from 'lib/dateTimeHelpers'; +import { Action, ResourceType } from 'generated-sources'; +import { ActionButton } from 'components/common/ActionComponent'; + +import * as S from './Statistics.styles'; +import Total from './Indicators/Total'; +import SizeStats from './Indicators/SizeStats'; +import PartitionTable from './PartitionTable'; + +const Metrics: React.FC = () => { + const params = useAppParams(); + + const [isAnalyzing, setIsAnalyzing] = useState(true); + const analyzeTopic = useAnalyzeTopic(params); + const cancelTopicAnalysis = useCancelTopicAnalysis(params); + + const { data } = useTopicAnalysis(params, isAnalyzing); + + useEffect(() => { + if (data && !data.progress) { + setIsAnalyzing(false); + } + }, [data]); + + if (!data) { + return null; + } + + if (data.progress) { + return ( + + + {Math.floor(data.progress.completenessPercent || 0)}% + + + + + { + await cancelTopicAnalysis.mutateAsync(); + setIsAnalyzing(true); + }} + buttonType="secondary" + buttonSize="M" + permission={{ + resource: ResourceType.TOPIC, + action: Action.MESSAGES_READ, + value: params.topicName, + }} + > + Stop Analysis + + + + + {formatTimestamp(data.progress.startedAt, { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + })} + + + {calculateTimer(data.progress.startedAt as number)} + + {data.progress.msgsScanned} + + + + + + + ); + } + + if (!data.result) { + return null; + } + + const totalStats = data.result.totalStats || {}; + const partitionStats = data.result.partitionStats || []; + + return ( + <> + + {formatTimestamp(data?.result?.finishedAt)} + { + await analyzeTopic.mutateAsync(); + setIsAnalyzing(true); + }} + buttonType="primary" + buttonSize="S" + permission={{ + resource: ResourceType.TOPIC, + action: Action.MESSAGES_READ, + value: params.topicName, + }} + > + Restart Analysis + + + + + {totalStats.keySize && ( + + )} + {totalStats.valueSize && ( + + )} + + + + ); +}; + +export default Metrics; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Statistics/PartitionInfoRow.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/PartitionInfoRow.tsx new file mode 100644 index 00000000000..51c1f85c2f1 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/PartitionInfoRow.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { Row } from '@tanstack/react-table'; +import Heading from 'components/common/heading/Heading.styled'; +import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted'; +import { + List, + Label, +} from 'components/common/PropertiesList/PropertiesList.styled'; +import { TopicAnalysisStats } from 'generated-sources'; +import { formatTimestamp } from 'lib/dateTimeHelpers'; + +import * as S from './Statistics.styles'; + +const PartitionInfoRow: React.FC<{ row: Row }> = ({ + row, +}) => { + const { + totalMsgs, + minTimestamp, + maxTimestamp, + nullKeys, + nullValues, + approxUniqKeys, + approxUniqValues, + keySize, + valueSize, + } = row.original; + return ( + +
    + Partition stats + + + {totalMsgs} + + + + {formatTimestamp(minTimestamp)} + + {formatTimestamp(maxTimestamp)} + + {nullKeys} + + {nullValues} + + {approxUniqKeys} + + {approxUniqValues} + +
    +
    + Keys sizes + + + + + + + + + + + + + + + + + + + + +
    +
    + Values sizes + + + + + + + + + + + + + + + + + + + + +
    +
    + ); +}; + +export default PartitionInfoRow; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Statistics/PartitionTable.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/PartitionTable.tsx new file mode 100644 index 00000000000..b019f0bc422 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/PartitionTable.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { TopicAnalysisStats } from 'generated-sources'; +import { ColumnDef } from '@tanstack/react-table'; +import Table from 'components/common/NewTable'; + +import PartitionInfoRow from './PartitionInfoRow'; + +const PartitionTable: React.FC<{ data: TopicAnalysisStats[] }> = ({ data }) => { + const columns = React.useMemo[]>( + () => [ + { + header: 'Partition ID', + accessorKey: 'partition', + }, + { + header: 'Total Messages', + accessorKey: 'totalMsgs', + }, + { + header: 'Min Offset', + accessorKey: 'minOffset', + }, + { header: 'Max Offset', accessorKey: 'maxOffset' }, + ], + [] + ); + + return ( +
    true} + renderSubComponent={PartitionInfoRow} + enableSorting + /> + ); +}; + +export default PartitionTable; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Statistics.styles.ts b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Statistics.styles.ts new file mode 100644 index 00000000000..ca98cdcbfd0 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Statistics.styles.ts @@ -0,0 +1,51 @@ +import { List } from 'components/common/PropertiesList/PropertiesList.styled'; +import styled from 'styled-components'; + +export const ProgressContainer = styled.div` + padding: 1.5rem 1rem; + background: ${({ theme }) => theme.code.backgroundColor}; + justify-content: center; + align-items: center; + display: flex; + flex-direction: column; + height: 300px; + text-align: center; + + ${List} { + opacity: 0.5; + } +`; + +export const ActionsBar = styled.div` + display: flex; + justify-content: end; + gap: 8px; + padding: 10px 20px; + align-items: center; +`; + +export const CreatedAt = styled.div` + font-size: 12px; + line-height: 1.5; + color: ${({ theme }) => theme.statictics.createdAtColor}; +`; + +export const PartitionInfo = styled.div` + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + column-gap: 24px; +`; + +export const ProgressBarWrapper = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + width: 280px; +`; + +export const ProgressPct = styled.span` + font-size: 15px; + font-weight: bold; + line-height: 1.5; + color: ${({ theme }) => theme.statictics.progressPctColor}; +`; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Statistics.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Statistics.tsx new file mode 100644 index 00000000000..fd275028ba8 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Statistics.tsx @@ -0,0 +1,50 @@ +/* eslint-disable react/no-unstable-nested-components */ +import React from 'react'; +import { useAnalyzeTopic } from 'lib/hooks/api/topics'; +import useAppParams from 'lib/hooks/useAppParams'; +import { RouteParamsClusterTopic } from 'lib/paths'; +import { QueryErrorResetBoundary } from '@tanstack/react-query'; +import { ErrorBoundary } from 'react-error-boundary'; +import { Action, ResourceType } from 'generated-sources'; +import { ActionButton } from 'components/common/ActionComponent'; + +import * as S from './Statistics.styles'; +import Metrics from './Metrics'; + +const Statistics: React.FC = () => { + const params = useAppParams(); + const analyzeTopic = useAnalyzeTopic(params); + + return ( + + {({ reset }) => ( + ( + + { + await analyzeTopic.mutateAsync(); + resetErrorBoundary(); + }} + buttonType="primary" + buttonSize="M" + permission={{ + resource: ResourceType.TOPIC, + action: Action.MESSAGES_READ, + value: params.topicName, + }} + > + Start Analysis + + + )} + > + + + )} + + ); +}; + +export default Statistics; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Statistics/__test__/Metrics.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/__test__/Metrics.spec.tsx new file mode 100644 index 00000000000..096a0af4c19 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/__test__/Metrics.spec.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import { render, WithRoute } from 'lib/testHelpers'; +import Statistics from 'components/Topics/Topic/Statistics/Statistics'; +import { clusterTopicStatisticsPath } from 'lib/paths'; +import { + useTopicAnalysis, + useCancelTopicAnalysis, + useAnalyzeTopic, +} from 'lib/hooks/api/topics'; +import { topicStatsPayload } from 'lib/fixtures/topics'; +import userEvent from '@testing-library/user-event'; + +const clusterName = 'local'; +const topicName = 'topic'; + +jest.mock('lib/hooks/api/topics', () => ({ + ...jest.requireActual('lib/hooks/api/topics'), + useTopicAnalysis: jest.fn(), + useCancelTopicAnalysis: jest.fn(), + useAnalyzeTopic: jest.fn(), +})); + +describe('Metrics', () => { + const renderComponent = () => { + const path = clusterTopicStatisticsPath(clusterName, topicName); + return render( + + + , + { initialEntries: [path] } + ); + }; + + describe('when analysis is in progress', () => { + const cancelMock = jest.fn(); + + beforeEach(() => { + (useCancelTopicAnalysis as jest.Mock).mockImplementation(() => ({ + mutateAsync: cancelMock, + })); + (useTopicAnalysis as jest.Mock).mockImplementation(() => ({ + data: { + progress: { + ...topicStatsPayload.progress, + completenessPercent: undefined, + }, + result: undefined, + }, + })); + renderComponent(); + }); + + it('renders Stop Analysis button', async () => { + const btn = screen.getByRole('button', { name: 'Stop Analysis' }); + expect(btn).toBeInTheDocument(); + await userEvent.click(btn); + expect(cancelMock).toHaveBeenCalled(); + }); + + it('renders Progress bar', () => { + const progressbar = screen.getByRole('progressbar'); + expect(progressbar).toBeInTheDocument(); + expect(progressbar).toHaveStyleRule('width', '0%'); + }); + + it('calculate Timer ', () => { + expect(screen.getByText('Passed since start')).toBeInTheDocument(); + }); + }); + + describe('when analysis is completed', () => { + const restartMock = jest.fn(); + beforeEach(() => { + (useTopicAnalysis as jest.Mock).mockImplementation(() => ({ + data: { ...topicStatsPayload, progress: undefined }, + })); + (useAnalyzeTopic as jest.Mock).mockImplementation(() => ({ + mutateAsync: restartMock, + })); + renderComponent(); + }); + it('renders metrics', async () => { + const btn = screen.getByRole('button', { name: 'Restart Analysis' }); + expect(btn).toBeInTheDocument(); + expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); + expect(screen.getAllByRole('group').length).toEqual(3); + expect(screen.getByRole('table')).toBeInTheDocument(); + }); + it('renders restarts analisis', async () => { + const btn = screen.getByRole('button', { name: 'Restart Analysis' }); + await waitFor(() => userEvent.click(btn)); + expect(restartMock).toHaveBeenCalled(); + }); + it('renders expandable table', async () => { + expect(screen.getByRole('table')).toBeInTheDocument(); + const rows = screen.getAllByRole('row'); + expect(rows.length).toEqual(3); + const btns = screen.getAllByRole('button', { name: 'Expand row' }); + expect(btns.length).toEqual(2); + expect(screen.queryByText('Partition stats')).not.toBeInTheDocument(); + + await userEvent.click(btns[0]); + expect(screen.getAllByText('Partition stats').length).toEqual(1); + await userEvent.click(btns[1]); + expect(screen.getAllByText('Partition stats').length).toEqual(2); + }); + }); + + it('returns empty container', () => { + (useTopicAnalysis as jest.Mock).mockImplementation(() => ({ + data: undefined, + })); + renderComponent(); + expect(screen.queryByRole('table')).not.toBeInTheDocument(); + expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); + }); + it('returns empty container', () => { + (useTopicAnalysis as jest.Mock).mockImplementation(() => ({ + data: {}, + })); + renderComponent(); + expect(screen.queryByRole('table')).not.toBeInTheDocument(); + expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); + }); +}); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Statistics/__test__/Statistics.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/__test__/Statistics.spec.tsx new file mode 100644 index 00000000000..2cc592c9816 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/__test__/Statistics.spec.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import { render, WithRoute } from 'lib/testHelpers'; +import Statistics from 'components/Topics/Topic/Statistics/Statistics'; +import { clusterTopicStatisticsPath } from 'lib/paths'; +import { useTopicAnalysis, useAnalyzeTopic } from 'lib/hooks/api/topics'; +import userEvent from '@testing-library/user-event'; + +const clusterName = 'local'; +const topicName = 'topic'; + +jest.mock('lib/hooks/api/topics', () => ({ + ...jest.requireActual('lib/hooks/api/topics'), + useTopicAnalysis: jest.fn(), + useAnalyzeTopic: jest.fn(), +})); + +describe('Statistics', () => { + const renderComponent = () => { + const path = clusterTopicStatisticsPath(clusterName, topicName); + return render( + + + , + { initialEntries: [path] } + ); + }; + const startMock = jest.fn(); + it('renders Metrics component', async () => { + (useTopicAnalysis as jest.Mock).mockImplementation(() => ({ + data: { result: 1 }, + })); + + renderComponent(); + await expect(screen.getByText('Restart Analysis')).toBeInTheDocument(); + expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); + }); + it('renders Start Analysis button', async () => { + jest.spyOn(console, 'error').mockImplementation(() => undefined); + (useAnalyzeTopic as jest.Mock).mockImplementation(() => ({ + mutateAsync: startMock, + })); + renderComponent(); + const btn = screen.getByRole('button', { name: 'Start Analysis' }); + expect(btn).toBeInTheDocument(); + await waitFor(() => userEvent.click(btn)); + expect(startMock).toHaveBeenCalled(); + jest.clearAllMocks(); + }); +}); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Topic.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Topic.tsx index 14fb518a752..5a639f0c48d 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Topic.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Topic.tsx @@ -1,57 +1,245 @@ -import React from 'react'; -import { Routes, Route } from 'react-router-dom'; -import { ClusterName, TopicName } from 'redux/interfaces'; -import EditContainer from 'components/Topics/Topic/Edit/EditContainer'; -import DetailsContainer from 'components/Topics/Topic/Details/DetailsContainer'; -import PageLoader from 'components/common/PageLoader/PageLoader'; +import React, { Suspense } from 'react'; +import { NavLink, Route, Routes, useNavigate } from 'react-router-dom'; import { + clusterTopicConsumerGroupsRelativePath, clusterTopicEditRelativePath, - clusterTopicSendMessageRelativePath, + clusterTopicMessagesRelativePath, + clusterTopicSettingsRelativePath, + clusterTopicsPath, + clusterTopicStatisticsRelativePath, RouteParamsClusterTopic, } from 'lib/paths'; +import ClusterContext from 'components/contexts/ClusterContext'; +import PageHeading from 'components/common/PageHeading/PageHeading'; +import { + ActionButton, + ActionNavLink, + ActionDropdownItem, +} from 'components/common/ActionComponent'; +import Navbar from 'components/common/Navigation/Navbar.styled'; +import { useAppDispatch } from 'lib/hooks/redux'; import useAppParams from 'lib/hooks/useAppParams'; +import { Dropdown, DropdownItemHint } from 'components/common/Dropdown'; +import { + useClearTopicMessages, + useDeleteTopic, + useRecreateTopic, + useTopicDetails, +} from 'lib/hooks/api/topics'; +import { resetTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice'; +import { Action, CleanUpPolicy, ResourceType } from 'generated-sources'; +import PageLoader from 'components/common/PageLoader/PageLoader'; +import SlidingSidebar from 'components/common/SlidingSidebar'; +import useBoolean from 'lib/hooks/useBoolean'; +import Messages from './Messages/Messages'; +import Overview from './Overview/Overview'; +import Settings from './Settings/Settings'; +import TopicConsumerGroups from './ConsumerGroups/TopicConsumerGroups'; +import Statistics from './Statistics/Statistics'; +import Edit from './Edit/Edit'; import SendMessage from './SendMessage/SendMessage'; -interface TopicProps { - isTopicFetching: boolean; - resetTopicMessages: () => void; - fetchTopicDetails: (payload: { - clusterName: ClusterName; - topicName: TopicName; - }) => void; -} - -const Topic: React.FC = ({ - isTopicFetching, - fetchTopicDetails, - resetTopicMessages, -}) => { +const Topic: React.FC = () => { + const dispatch = useAppDispatch(); + const { + value: isSidebarOpen, + setFalse: closeSidebar, + setTrue: openSidebar, + } = useBoolean(false); const { clusterName, topicName } = useAppParams(); - React.useEffect(() => { - fetchTopicDetails({ clusterName, topicName }); - }, [fetchTopicDetails, clusterName, topicName]); + const navigate = useNavigate(); + const deleteTopic = useDeleteTopic(clusterName); + const recreateTopic = useRecreateTopic({ clusterName, topicName }); + const { data } = useTopicDetails({ clusterName, topicName }); + + const { isReadOnly, isTopicDeletionAllowed } = + React.useContext(ClusterContext); + + const deleteTopicHandler = async () => { + await deleteTopic.mutateAsync(topicName); + navigate(clusterTopicsPath(clusterName)); + }; React.useEffect(() => { return () => { - resetTopicMessages(); + dispatch(resetTopicMessages()); }; }, []); + const clearMessages = useClearTopicMessages(clusterName); + const clearTopicMessagesHandler = async () => { + await clearMessages.mutateAsync(topicName); + }; + const canCleanup = data?.cleanUpPolicy === CleanUpPolicy.DELETE; + return ( + <> + + + Produce Message + + + navigate(clusterTopicEditRelativePath)} + permission={{ + resource: ResourceType.TOPIC, + action: Action.EDIT, + value: topicName, + }} + > + Edit settings + + Pay attention! This operation has +
    + especially important consequences. +
    +
    - if (isTopicFetching) { - return ; - } + + Clear messages + + Clearing messages is only allowed for topics +
    + with DELETE policy +
    +
    - return ( - - } /> - } /> - } - /> - + + Are you sure want to recreate {topicName} topic? + + } + danger + permission={{ + resource: ResourceType.TOPIC, + action: [Action.MESSAGES_READ, Action.CREATE, Action.DELETE], + value: topicName, + }} + > + Recreate Topic + + + Are you sure want to remove {topicName} topic? + + } + disabled={!isTopicDeletionAllowed} + danger + permission={{ + resource: ResourceType.TOPIC, + action: Action.DELETE, + value: topicName, + }} + > + Remove Topic + {!isTopicDeletionAllowed && ( + + The topic deletion is restricted at the broker +
    + configuration level (delete.topic.enable = false) +
    + )} +
    +
    +
    + + (isActive ? 'is-active' : '')} + end + > + Overview + + (isActive ? 'is-active' : '')} + permission={{ + resource: ResourceType.TOPIC, + action: Action.MESSAGES_READ, + value: topicName, + }} + > + Messages + + (isActive ? 'is-active' : '')} + > + Consumers + + (isActive ? 'is-active' : '')} + > + Settings + + (isActive ? 'is-active' : '')} + > + Statistics + + + }> + + } /> + } + /> + } + /> + } + /> + } + /> + } /> + + + + }> + + + + ); }; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/TopicContainer.tsx b/kafka-ui-react-app/src/components/Topics/Topic/TopicContainer.tsx deleted file mode 100644 index 58e4522d7c0..00000000000 --- a/kafka-ui-react-app/src/components/Topics/Topic/TopicContainer.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { connect } from 'react-redux'; -import { RootState } from 'redux/interfaces'; -import { fetchTopicDetails } from 'redux/reducers/topics/topicsSlice'; -import { resetTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice'; -import { getIsTopicDetailsFetching } from 'redux/reducers/topics/selectors'; - -import Topic from './Topic'; - -const mapStateToProps = (state: RootState) => ({ - isTopicFetching: getIsTopicDetailsFetching(state), -}); - -const mapDispatchToProps = { - fetchTopicDetails, - resetTopicMessages, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(Topic); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/__test__/Topic.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/__test__/Topic.spec.tsx new file mode 100644 index 00000000000..4ec45c3a58f --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/__test__/Topic.spec.tsx @@ -0,0 +1,272 @@ +import React from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import ClusterContext from 'components/contexts/ClusterContext'; +import Details from 'components/Topics/Topic/Topic'; +import { render, WithRoute } from 'lib/testHelpers'; +import { + clusterTopicConsumerGroupsPath, + clusterTopicEditRelativePath, + clusterTopicMessagesPath, + clusterTopicPath, + clusterTopicSettingsPath, + clusterTopicsPath, + clusterTopicStatisticsPath, + getNonExactPath, +} from 'lib/paths'; +import { CleanUpPolicy, Topic } from 'generated-sources'; +import { externalTopicPayload } from 'lib/fixtures/topics'; +import { + useClearTopicMessages, + useDeleteTopic, + useRecreateTopic, + useTopicDetails, +} from 'lib/hooks/api/topics'; +import { useAppDispatch } from 'lib/hooks/redux'; + +const mockNavigate = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, +})); +jest.mock('lib/hooks/api/topics', () => ({ + useTopicDetails: jest.fn(), + useDeleteTopic: jest.fn(), + useRecreateTopic: jest.fn(), + useClearTopicMessages: jest.fn(), +})); + +const unwrapMock = jest.fn(); +const clearTopicMessages = jest.fn(); + +jest.mock('lib/hooks/redux', () => ({ + ...jest.requireActual('lib/hooks/redux'), + useAppDispatch: jest.fn(), +})); + +jest.mock('components/Topics/Topic/Overview/Overview', () => () => ( + <>OverviewMock +)); +jest.mock('components/Topics/Topic/Messages/Messages', () => () => ( + <>MessagesMock +)); +jest.mock('components/Topics/Topic/SendMessage/SendMessage', () => () => ( + <>SendMessageMock +)); +jest.mock('components/Topics/Topic/Settings/Settings', () => () => ( + <>SettingsMock +)); +jest.mock( + 'components/Topics/Topic/ConsumerGroups/TopicConsumerGroups', + () => () => <>ConsumerGroupsMock +); +jest.mock('components/Topics/Topic/Statistics/Statistics', () => () => ( + <>StatisticsMock +)); + +const mockDelete = jest.fn(); +const mockRecreate = jest.fn(); +const mockClusterName = 'local'; +const topic: Topic = { + ...externalTopicPayload, + cleanUpPolicy: CleanUpPolicy.DELETE, +}; +const defaultPath = clusterTopicPath(mockClusterName, topic.name); + +describe('Details', () => { + const renderComponent = (isReadOnly = false, path = defaultPath) => { + render( + + +
    + + , + { initialEntries: [path] } + ); + }; + + beforeEach(async () => { + (useTopicDetails as jest.Mock).mockImplementation(() => ({ + data: topic, + })); + (useDeleteTopic as jest.Mock).mockImplementation(() => ({ + mutateAsync: mockDelete, + })); + (useRecreateTopic as jest.Mock).mockImplementation(() => ({ + mutateAsync: mockRecreate, + })); + (useClearTopicMessages as jest.Mock).mockImplementation(() => ({ + mutateAsync: clearTopicMessages, + })); + (useAppDispatch as jest.Mock).mockImplementation(() => () => ({ + unwrap: unwrapMock, + })); + }); + describe('Action Bar', () => { + describe('when it has readonly flag', () => { + it('renders disabled the Action button', () => { + renderComponent(true); + expect( + screen.getByRole('button', { name: 'Produce Message' }) + ).toBeDisabled(); + }); + }); + + describe('when remove topic modal is open', () => { + beforeEach(async () => { + renderComponent(); + const openModalButton = screen.getAllByText('Remove Topic')[0]; + await userEvent.click(openModalButton); + }); + + it('calls deleteTopic on confirm', async () => { + const submitButton = screen.getAllByRole('button', { + name: 'Confirm', + })[0]; + await userEvent.click(submitButton); + expect(mockDelete).toHaveBeenCalledWith(topic.name); + }); + it('closes the modal when cancel button is clicked', async () => { + const cancelButton = screen.getAllByText('Cancel')[0]; + await waitFor(() => userEvent.click(cancelButton)); + expect(cancelButton).not.toBeInTheDocument(); + }); + }); + + describe('when clear messages modal is open', () => { + beforeEach(async () => { + await renderComponent(); + const confirmButton = screen.getAllByText('Clear messages')[0]; + await userEvent.click(confirmButton); + }); + + it('it calls clearTopicMessages on confirm', async () => { + const submitButton = screen.getAllByRole('button', { + name: 'Confirm', + })[0]; + await waitFor(() => userEvent.click(submitButton)); + expect(clearTopicMessages).toHaveBeenCalledTimes(1); + }); + + it('closes the modal when cancel button is clicked', async () => { + const cancelButton = screen.getAllByText('Cancel')[0]; + await waitFor(() => userEvent.click(cancelButton)); + + expect(cancelButton).not.toBeInTheDocument(); + }); + }); + + describe('when edit settings is clicked', () => { + it('redirects to the edit page', async () => { + renderComponent(); + const button = screen.getAllByText('Edit settings')[0]; + await userEvent.click(button); + expect(mockNavigate).toHaveBeenCalledWith(clusterTopicEditRelativePath); + }); + }); + + it('redirects to the correct route if topic is deleted', async () => { + renderComponent(); + const deleteTopicButton = screen.getByText(/Remove topic/i); + await waitFor(() => userEvent.click(deleteTopicButton)); + const submitDeleteButton = screen.getByRole('button', { + name: 'Confirm', + }); + await userEvent.click(submitDeleteButton); + expect(mockNavigate).toHaveBeenCalledWith( + clusterTopicsPath(mockClusterName) + ); + }); + + it('shows a confirmation popup on deleting topic messages', async () => { + renderComponent(); + const clearMessagesButton = screen.getAllByText(/Clear messages/i)[0]; + await userEvent.click(clearMessagesButton); + + expect( + screen.getByText(/Are you sure want to clear topic messages?/i) + ).toBeInTheDocument(); + }); + + it('shows a confirmation popup on recreating topic', async () => { + renderComponent(); + const recreateTopicButton = screen.getByText(/Recreate topic/i); + await userEvent.click(recreateTopicButton); + expect( + screen.getByText(/Are you sure want to recreate topic?/i) + ).toBeInTheDocument(); + }); + + it('is calling recreation function after click on Submit button', async () => { + renderComponent(); + const recreateTopicButton = screen.getByText(/Recreate topic/i); + await userEvent.click(recreateTopicButton); + const confirmBtn = screen.getByRole('button', { name: /Confirm/i }); + + await waitFor(() => userEvent.click(confirmBtn)); + expect(mockRecreate).toBeCalledTimes(1); + }); + + it('closes popup confirmation window after click on Cancel button', async () => { + renderComponent(); + const recreateTopicButton = screen.getByText(/Recreate topic/i); + await userEvent.click(recreateTopicButton); + const cancelBtn = screen.getByRole('button', { name: /cancel/i }); + await userEvent.click(cancelBtn); + expect( + screen.queryByText(/Are you sure want to recreate topic?/i) + ).not.toBeInTheDocument(); + }); + }); + + describe('Internal routing', () => { + const itExpectsCorrectPageRendered = ( + path: string, + tab: string, + selector: string + ) => { + renderComponent(false, path); + expect(screen.getByText(tab)).toHaveClass('is-active'); + expect(screen.getByText(selector)).toBeInTheDocument(); + }; + + it('renders Overview tab by default', () => { + itExpectsCorrectPageRendered(defaultPath, 'Overview', 'OverviewMock'); + }); + it('renders Messages tabs', () => { + itExpectsCorrectPageRendered( + clusterTopicMessagesPath(), + 'Messages', + 'MessagesMock' + ); + }); + it('renders Consumers tab', () => { + itExpectsCorrectPageRendered( + clusterTopicConsumerGroupsPath(), + 'Consumers', + 'ConsumerGroupsMock' + ); + }); + it('renders Settings tab', () => { + itExpectsCorrectPageRendered( + clusterTopicSettingsPath(), + 'Settings', + 'SettingsMock' + ); + }); + it('renders Statistics tab', () => { + itExpectsCorrectPageRendered( + clusterTopicStatisticsPath(), + 'Statistics', + 'StatisticsMock' + ); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/__tests__/Topic.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/__tests__/Topic.spec.tsx deleted file mode 100644 index 84880fe3e0f..00000000000 --- a/kafka-ui-react-app/src/components/Topics/Topic/__tests__/Topic.spec.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react'; -import { render, WithRoute } from 'lib/testHelpers'; -import { screen } from '@testing-library/react'; -import Topic from 'components/Topics/Topic/Topic'; -import { - clusterTopicPath, - clusterTopicEditPath, - clusterTopicSendMessagePath, - getNonExactPath, -} from 'lib/paths'; - -const topicText = { - edit: 'Edit Container', - send: 'Send Message', - detail: 'Details Container', - loading: 'Loading', -}; - -jest.mock('components/Topics/Topic/Edit/EditContainer', () => () => ( -
    {topicText.edit}
    -)); -jest.mock('components/Topics/Topic/SendMessage/SendMessage', () => () => ( -
    {topicText.send}
    -)); -jest.mock('components/Topics/Topic/Details/DetailsContainer', () => () => ( -
    {topicText.detail}
    -)); -jest.mock('components/common/PageLoader/PageLoader', () => () => ( -
    {topicText.loading}
    -)); - -describe('Topic Component', () => { - const resetTopicMessages = jest.fn(); - const fetchTopicDetailsMock = jest.fn(); - - const renderComponent = (pathname: string, topicFetching: boolean) => - render( - - - , - { initialEntries: [pathname] } - ); - - afterEach(() => { - resetTopicMessages.mockClear(); - fetchTopicDetailsMock.mockClear(); - }); - - it('renders Edit page', () => { - renderComponent(clusterTopicEditPath('local', 'myTopicName'), false); - expect(screen.getByText(topicText.edit)).toBeInTheDocument(); - }); - - it('renders Send Message page', () => { - renderComponent(clusterTopicSendMessagePath('local', 'myTopicName'), false); - expect(screen.getByText(topicText.send)).toBeInTheDocument(); - }); - - it('renders Details Container page', () => { - renderComponent(clusterTopicPath('local', 'myTopicName'), false); - expect(screen.getByText(topicText.detail)).toBeInTheDocument(); - }); - - it('renders Page loader', () => { - renderComponent(clusterTopicPath('local', 'myTopicName'), true); - expect(screen.getByText(topicText.loading)).toBeInTheDocument(); - }); - - it('fetches topicDetails', () => { - renderComponent(clusterTopicPath('local', 'myTopicName'), false); - expect(fetchTopicDetailsMock).toHaveBeenCalledTimes(1); - }); - - it('resets topic messages after unmount', () => { - const component = renderComponent( - clusterTopicPath('local', 'myTopicName'), - false - ); - component.unmount(); - expect(resetTopicMessages).toHaveBeenCalledTimes(1); - }); -}); diff --git a/kafka-ui-react-app/src/components/Topics/Topics.tsx b/kafka-ui-react-app/src/components/Topics/Topics.tsx index 6e1b7e21043..d6557a85807 100644 --- a/kafka-ui-react-app/src/components/Topics/Topics.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topics.tsx @@ -6,44 +6,23 @@ import { getNonExactPath, RouteParams, } from 'lib/paths'; -import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route'; +import SuspenseQueryComponent from 'components/common/SuspenseQueryComponent/SuspenseQueryComponent'; -import ListContainer from './List/ListContainer'; -import TopicContainer from './Topic/TopicContainer'; import New from './New/New'; +import ListPage from './List/ListPage'; +import Topic from './Topic/Topic'; const Topics: React.FC = () => ( - - - - } - /> - - - - } - /> - - - - } - /> + } /> + } /> + } /> - - + + + } /> diff --git a/kafka-ui-react-app/src/components/Topics/__tests__/Topics.spec.tsx b/kafka-ui-react-app/src/components/Topics/__tests__/Topics.spec.tsx index b699c5ff093..0e077445fc6 100644 --- a/kafka-ui-react-app/src/components/Topics/__tests__/Topics.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/__tests__/Topics.spec.tsx @@ -10,14 +10,14 @@ import { getNonExactPath, } from 'lib/paths'; -const listContainer = 'listContainer'; -const topicContainer = 'topicContainer'; -const newCopyContainer = 'newCopyContainer'; +const listContainer = 'My List Page'; +const topicContainer = 'My Topic Details Page'; +const newCopyContainer = 'My New/Copy Page'; -jest.mock('components/Topics/List/ListContainer', () => () => ( +jest.mock('components/Topics/List/ListPage', () => () => (
    {listContainer}
    )); -jest.mock('components/Topics/Topic/TopicContainer', () => () => ( +jest.mock('components/Topics/Topic/Topic', () => () => (
    {topicContainer}
    )); jest.mock('components/Topics/New/New', () => () => ( @@ -27,14 +27,13 @@ jest.mock('components/Topics/New/New', () => () => ( describe('Topics Component', () => { const clusterName = 'clusterName'; const topicName = 'topicName'; - const setUpComponent = (path: string) => { - return render( + const setUpComponent = (path: string) => + render( , { initialEntries: [path] } ); - }; it('should check if the page is Topics List rendered', () => { setUpComponent(clusterTopicsPath(clusterName)); diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamField.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamField.tsx index 72f28c5c461..95d3bcb15c9 100644 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamField.tsx +++ b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamField.tsx @@ -2,18 +2,20 @@ import React, { useRef } from 'react'; import { ErrorMessage } from '@hookform/error-message'; import { TOPIC_CUSTOM_PARAMS } from 'lib/constants'; import { FieldArrayWithId, useFormContext, Controller } from 'react-hook-form'; -import { TopicFormData } from 'redux/interfaces'; +import { TopicConfigParams, TopicFormData } from 'redux/interfaces'; import { InputLabel } from 'components/common/Input/InputLabel.styled'; import { FormError } from 'components/common/Input/Input.styled'; import Select from 'components/common/Select/Select'; import Input from 'components/common/Input/Input'; import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper'; -import CloseIcon from 'components/common/Icons/CloseIcon'; +import CloseCircleIcon from 'components/common/Icons/CloseCircleIcon'; import * as C from 'components/Topics/shared/Form/TopicForm.styled'; +import { ConfigSource } from 'generated-sources'; import * as S from './CustomParams.styled'; export interface Props { + config?: TopicConfigParams; isDisabled: boolean; index: number; existingFields: string[]; @@ -27,6 +29,7 @@ const CustomParamField: React.FC = ({ isDisabled, index, remove, + config, existingFields, setExistingFields, }) => { @@ -44,7 +47,10 @@ const CustomParamField: React.FC = ({ .map((option) => ({ value: option, label: option, - disabled: existingFields.includes(option), + disabled: + (config && + config[option]?.source !== ConfigSource.DYNAMIC_TOPIC_CONFIG) || + existingFields.includes(option), })); React.useEffect(() => { @@ -67,7 +73,7 @@ const CustomParamField: React.FC = ({ return (
    - Custom Parameter + Custom Parameter * = ({ } title={`Delete customParam field ${index}`} > - + diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.tsx index f3ac7666b2b..bd000b00f3f 100644 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.tsx +++ b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.tsx @@ -1,17 +1,23 @@ import React from 'react'; -import { TopicFormData } from 'redux/interfaces'; +import { TopicConfigParams, TopicFormData } from 'redux/interfaces'; import { useFieldArray, useFormContext, useWatch } from 'react-hook-form'; import { Button } from 'components/common/Button/Button'; import { TOPIC_CUSTOM_PARAMS_PREFIX } from 'lib/constants'; +import PlusIcon from 'components/common/Icons/PlusIcon'; import CustomParamField from './CustomParamField'; import * as S from './CustomParams.styled'; export interface CustomParamsProps { + config?: TopicConfigParams; isSubmitting: boolean; + isEditing?: boolean; } -const CustomParams: React.FC = ({ isSubmitting }) => { +const CustomParams: React.FC = ({ + isSubmitting, + config, +}) => { const { control } = useFormContext(); const { fields, append, remove } = useFieldArray({ control, @@ -40,9 +46,10 @@ const CustomParams: React.FC = ({ isSubmitting }) => { return ( - {controlledFields.map((field, idx) => ( + {controlledFields?.map((field, idx) => ( = ({ isSubmitting }) => { type="button" buttonSize="M" buttonType="secondary" - onClick={() => append({ name: '', value: '' })} + onClick={() => + append({ name: '', value: '' }, { shouldFocus: false }) + } > - + Add Custom Parameter
    diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__test__/CustomParamField.spec.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__test__/CustomParamField.spec.tsx index d94812d73aa..8ff3b88dafa 100644 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__test__/CustomParamField.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__test__/CustomParamField.spec.tsx @@ -57,7 +57,7 @@ describe('CustomParamsField', () => { }); describe('core functionality works', () => { - it('click on button triggers remove', () => { + it('click on button triggers remove', async () => { setupComponent({ field, isDisabled, @@ -66,11 +66,11 @@ describe('CustomParamsField', () => { existingFields, setExistingFields, }); - userEvent.click(screen.getByRole('button')); + await userEvent.click(screen.getByRole('button')); expect(remove).toHaveBeenCalledTimes(1); }); - it('pressing space on button triggers remove', () => { + it('pressing space on button triggers remove', async () => { setupComponent({ field, isDisabled, @@ -79,7 +79,7 @@ describe('CustomParamsField', () => { existingFields, setExistingFields, }); - userEvent.type(screen.getByRole('button'), SPACE_KEY); + await userEvent.type(screen.getByRole('button'), SPACE_KEY); // userEvent.type triggers remove two times as at first it clicks on element and then presses space expect(remove).toHaveBeenCalledTimes(2); }); diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__test__/CustomParams.spec.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__test__/CustomParams.spec.tsx index 09c58be6811..1a185637a65 100644 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__test__/CustomParams.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__test__/CustomParams.spec.tsx @@ -11,10 +11,10 @@ import { TOPIC_CUSTOM_PARAMS } from 'lib/constants'; import { defaultValues } from './fixtures'; const selectOption = async (listbox: HTMLElement, option: string) => { - await act(() => { - userEvent.click(listbox); + await act(async () => { + await userEvent.click(listbox); }); - userEvent.click(screen.getByText(option)); + await userEvent.click(screen.getByText(option)); }; const expectOptionIsSelected = (listbox: HTMLElement, option: string) => { @@ -28,7 +28,9 @@ const expectOptionAvailability = async ( option: string, disabled: boolean ) => { - await act(() => userEvent.click(listbox)); + await act(async () => { + await userEvent.click(listbox); + }); const selectedOptions = within(listbox).getAllByText(option).reverse(); // its either two or one nodes, we only need last one const selectedOption = selectedOptions[0]; @@ -43,7 +45,9 @@ const expectOptionAvailability = async ( 'cursor', disabled ? 'not-allowed' : 'pointer' ); - await act(() => userEvent.click(listbox)); + await act(async () => { + await userEvent.click(listbox); + }); }; const renderComponent = (props: CustomParamsProps, defaults = {}) => { @@ -85,7 +89,9 @@ describe('CustomParams', () => { beforeEach(async () => { renderComponent({ isSubmitting: false }); button = screen.getByRole('button'); - await act(() => userEvent.click(button)); + await act(async () => { + await userEvent.click(button); + }); }); it('button click creates custom param fieldset', async () => { @@ -120,8 +126,12 @@ describe('CustomParams', () => { }); it('multiple button clicks create multiple fieldsets', async () => { - await act(() => userEvent.click(button)); - await act(() => userEvent.click(button)); + await act(async () => { + await userEvent.click(button); + }); + await act(async () => { + await userEvent.click(button); + }); const listboxes = screen.getAllByRole('listbox'); expect(listboxes.length).toBe(3); @@ -131,7 +141,9 @@ describe('CustomParams', () => { }); it("can't select already selected option", async () => { - await act(() => userEvent.click(button)); + await act(async () => { + await userEvent.click(button); + }); const listboxes = screen.getAllByRole('listbox'); @@ -144,8 +156,12 @@ describe('CustomParams', () => { }); it('when fieldset with selected custom property type is deleted disabled options update correctly', async () => { - await act(() => userEvent.click(button)); - await act(() => userEvent.click(button)); + await act(async () => { + await userEvent.click(button); + }); + await act(async () => { + await userEvent.click(button); + }); const listboxes = screen.getAllByRole('listbox'); @@ -172,7 +188,9 @@ describe('CustomParams', () => { const deleteSecondFieldsetButton = screen.getByTitle( 'Delete customParam field 1' ); - await act(() => userEvent.click(deleteSecondFieldsetButton)); + await act(async () => { + await userEvent.click(deleteSecondFieldsetButton); + }); expect(secondListbox).not.toBeInTheDocument(); await expectOptionAvailability( diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/TimeToRetain.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/TimeToRetain.tsx index daa515ae7bf..93925181a6e 100644 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/TimeToRetain.tsx +++ b/kafka-ui-react-app/src/components/Topics/shared/Form/TimeToRetain.tsx @@ -39,7 +39,7 @@ const TimeToRetain: React.FC = ({ isSubmitting }) => { = ({ inputName, text, value }) => { setValue(inputName, value)} + onClick={() => + setValue(inputName, value, { + shouldDirty: true, + }) + } > {text} diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/TimeToRetainBtns.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/TimeToRetainBtns.tsx index 53376876e4b..80d421b4a41 100644 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/TimeToRetainBtns.tsx +++ b/kafka-ui-react-app/src/components/Topics/shared/Form/TimeToRetainBtns.tsx @@ -18,23 +18,27 @@ const TimeToRetainBtnsWrapper = styled.div` const TimeToRetainBtns: React.FC = ({ name }) => ( - + diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.styled.ts b/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.styled.ts index fc46d8d9397..9dbe1d0aeef 100644 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.styled.ts +++ b/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.styled.ts @@ -1,4 +1,5 @@ import styled from 'styled-components'; +import Input from 'components/common/Input/Input'; export const Column = styled.div` display: flex; @@ -13,6 +14,11 @@ export const NameField = styled.div` export const CustomParamsHeading = styled.h4` font-weight: 500; + color: ${({ theme }) => theme.heading.h4}; +`; + +export const MessageSizeInput = styled(Input)` + min-width: 195px; `; export const Label = styled.div` @@ -29,15 +35,21 @@ export const Label = styled.div` export const Button = styled.button<{ isActive: boolean }>` background-color: ${({ theme, ...props }) => props.isActive - ? theme.button.primary.backgroundColor.active - : theme.button.primary.backgroundColor.normal}; - height: 32px; - width: 46px; - border: 1px solid - ${({ theme, ...props }) => - props.isActive ? theme.button.border.active : theme.button.primary.color}; + ? theme.chips.backgroundColor.active + : theme.chips.backgroundColor.normal}; + color: ${({ theme, ...props }) => + props.isActive ? theme.chips.color.active : theme.chips.color.normal}; + height: 24px; + padding: 0 5px; + min-width: 51px; + border: none; border-radius: 6px; &:hover { cursor: pointer; } `; + +export const ButtonWrapper = styled.div` + display: flex; + gap: 10px; +`; diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx index bd0fb16beb6..677890cb721 100644 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx +++ b/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useFormContext, Controller } from 'react-hook-form'; import { NOT_SET, BYTES_IN_GB } from 'lib/constants'; -import { TopicName } from 'redux/interfaces'; +import { ClusterName, TopicConfigParams, TopicName } from 'redux/interfaces'; import { ErrorMessage } from '@hookform/error-message'; import Select, { SelectOption } from 'components/common/Select/Select'; import Input from 'components/common/Input/Input'; @@ -9,16 +9,21 @@ import { Button } from 'components/common/Button/Button'; import { InputLabel } from 'components/common/Input/InputLabel.styled'; import { FormError } from 'components/common/Input/Input.styled'; import { StyledForm } from 'components/common/Form/Form.styled'; +import { clusterTopicPath } from 'lib/paths'; +import { useNavigate } from 'react-router-dom'; +import useAppParams from 'lib/hooks/useAppParams'; import CustomParams from './CustomParams/CustomParams'; import TimeToRetain from './TimeToRetain'; import * as S from './TopicForm.styled'; export interface Props { + config?: TopicConfigParams; topicName?: TopicName; partitionCount?: number; replicationFactor?: number; inSyncReplicas?: number; + retentionBytes?: number; cleanUpPolicy?: string; isEditing?: boolean; isSubmitting: boolean; @@ -31,6 +36,17 @@ const CleanupPolicyOptions: Array = [ { value: 'compact,delete', label: 'Compact,Delete' }, ]; +export const getCleanUpPolicyValue = (cleanUpPolicy?: string) => { + if (!cleanUpPolicy) return undefined; + + return CleanupPolicyOptions.find((option: SelectOption) => { + return ( + option.value.toString().replace(/,/g, '_') === + cleanUpPolicy?.toLowerCase() + ); + })?.value.toString(); +}; + const RetentionBytesOptions: Array = [ { value: NOT_SET, label: 'Not Set' }, { value: BYTES_IN_GB, label: '1 GB' }, @@ -40,25 +56,36 @@ const RetentionBytesOptions: Array = [ ]; const TopicForm: React.FC = ({ + config, + retentionBytes, topicName, isEditing, isSubmitting, onSubmit, - partitionCount, - replicationFactor, - inSyncReplicas, cleanUpPolicy, }) => { const { control, - formState: { errors }, + formState: { errors, isDirty, isValid }, + reset, } = useFormContext(); + const navigate = useNavigate(); + const { clusterName } = useAppParams<{ clusterName: ClusterName }>(); const getCleanUpPolicy = - CleanupPolicyOptions.find((option: SelectOption) => { - return option.value === cleanUpPolicy?.toLowerCase(); - })?.value || CleanupPolicyOptions[0].value; + getCleanUpPolicyValue(cleanUpPolicy) || CleanupPolicyOptions[0].value; + + const getRetentionBytes = + RetentionBytesOptions.find((option: SelectOption) => { + return option.value === retentionBytes; + })?.value || RetentionBytesOptions[0].value; + + const onCancel = () => { + reset(); + navigate(clusterTopicPath(clusterName, topicName)); + }; + return ( - +
    @@ -66,9 +93,11 @@ const TopicForm: React.FC = ({ Topic Name * @@ -76,85 +105,91 @@ const TopicForm: React.FC = ({ - {!isEditing && ( - + + {!isEditing && (
    - Number of partitions * + Number of Partitions *
    -
    - - Replication Factor * - - - - - -
    -
    - )} + )} + +
    + + Cleanup policy + + ( + - +
    -
    - - Cleanup policy - - ( - + + + +
    + )}
    @@ -180,7 +215,7 @@ const TopicForm: React.FC = ({ id="topicFormRetentionBytes" aria-labelledby="topicFormRetentionBytesLabel" name={name} - value={RetentionBytesOptions[0].value} + value={getRetentionBytes} onChange={onChange} minWidth="100%" options={RetentionBytesOptions} @@ -191,14 +226,16 @@ const TopicForm: React.FC = ({
    - Maximum message size in bytes * + Maximum message size in bytes - @@ -207,11 +244,29 @@ const TopicForm: React.FC = ({ Custom parameters - - - + + + + +
    ); diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TimeToRetainBtn.spec.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TimeToRetainBtn.spec.tsx index 756fc081c0b..f09eec6c5b6 100644 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TimeToRetainBtn.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TimeToRetainBtn.spec.tsx @@ -5,7 +5,7 @@ import TimeToRetainBtn, { Props, } from 'components/Topics/shared/Form/TimeToRetainBtn'; import { useForm, FormProvider } from 'react-hook-form'; -import theme from 'theme/theme'; +import { theme } from 'theme/theme'; import userEvent from '@testing-library/user-event'; describe('TimeToRetainBtn', () => { @@ -42,21 +42,17 @@ describe('TimeToRetainBtn', () => { it('should test the non active state of the button and its styling', () => { const buttonElement = screen.getByRole('button'); expect(buttonElement).toHaveStyle( - `background-color:${theme.button.primary.backgroundColor.normal}` - ); - expect(buttonElement).toHaveStyle( - `border:1px solid ${theme.button.primary.color}` + `background-color:${theme.chips.backgroundColor.normal}` ); + expect(buttonElement).toHaveStyle(`border:none`); }); - it('should test the non active state with click becoming active', () => { + it('should test the non active state with click becoming active', async () => { const buttonElement = screen.getByRole('button'); - userEvent.click(buttonElement); - expect(buttonElement).toHaveStyle( - `background-color:${theme.button.primary.backgroundColor.active}` - ); + await userEvent.click(buttonElement); expect(buttonElement).toHaveStyle( - `border:1px solid ${theme.button.border.active}` + `background-color:${theme.chips.backgroundColor.active}` ); + expect(buttonElement).toHaveStyle(`border:none`); }); }); @@ -65,11 +61,9 @@ describe('TimeToRetainBtn', () => { SetUpComponent({ value: 604800000 }); const buttonElement = screen.getByRole('button'); expect(buttonElement).toHaveStyle( - `background-color:${theme.button.primary.backgroundColor.active}` - ); - expect(buttonElement).toHaveStyle( - `border:1px solid ${theme.button.border.active}` + `background-color:${theme.button.secondary.invertedColors.normal}` ); + expect(buttonElement).toHaveStyle(`border:none`); }); }); }); diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.spec.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.spec.tsx index 3c54c28b0ca..347ce935e3d 100644 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.spec.tsx @@ -4,6 +4,7 @@ import { screen } from '@testing-library/dom'; import { FormProvider, useForm } from 'react-hook-form'; import TopicForm, { Props } from 'components/Topics/shared/Form/TopicForm'; import userEvent from '@testing-library/user-event'; +import { act } from 'react-dom/test-utils'; const isSubmitting = false; const onSubmit = jest.fn(); @@ -29,44 +30,53 @@ const expectByRoleAndNameToBeInDocument = ( }; describe('TopicForm', () => { - it('renders', () => { - renderComponent(); + it('renders', async () => { + await act(async () => { + renderComponent(); + }); expectByRoleAndNameToBeInDocument('textbox', 'Topic Name *'); - expectByRoleAndNameToBeInDocument('spinbutton', 'Number of partitions *'); - expectByRoleAndNameToBeInDocument('spinbutton', 'Replication Factor *'); + expectByRoleAndNameToBeInDocument('spinbutton', 'Number of Partitions *'); + expectByRoleAndNameToBeInDocument('spinbutton', 'Replication Factor'); - expectByRoleAndNameToBeInDocument('spinbutton', 'Min In Sync Replicas *'); + expectByRoleAndNameToBeInDocument('spinbutton', 'Min In Sync Replicas'); expectByRoleAndNameToBeInDocument('listbox', 'Cleanup policy'); expectByRoleAndNameToBeInDocument( 'spinbutton', 'Time to retain data (in ms)' ); - expectByRoleAndNameToBeInDocument('button', '12h'); - expectByRoleAndNameToBeInDocument('button', '2d'); - expectByRoleAndNameToBeInDocument('button', '7d'); - expectByRoleAndNameToBeInDocument('button', '4w'); + expectByRoleAndNameToBeInDocument('button', '12 hours'); + expectByRoleAndNameToBeInDocument('button', '2 days'); + expectByRoleAndNameToBeInDocument('button', '7 days'); + expectByRoleAndNameToBeInDocument('button', '4 weeks'); expectByRoleAndNameToBeInDocument('listbox', 'Max size on disk in GB'); expectByRoleAndNameToBeInDocument( 'spinbutton', - 'Maximum message size in bytes *' + 'Maximum message size in bytes' ); expectByRoleAndNameToBeInDocument('heading', 'Custom parameters'); - expectByRoleAndNameToBeInDocument('button', 'Submit'); + expectByRoleAndNameToBeInDocument('button', 'Create topic'); }); - it('submits', () => { - renderComponent({ - isSubmitting, - onSubmit: onSubmit.mockImplementation((e) => e.preventDefault()), + it('submits', async () => { + await act(async () => { + renderComponent({ + isSubmitting, + onSubmit: onSubmit.mockImplementation((e) => e.preventDefault()), + }); }); - userEvent.click(screen.getByRole('button', { name: 'Submit' })); + await userEvent.type( + screen.getByPlaceholderText('Topic Name'), + 'topicName' + ); + await userEvent.click(screen.getByRole('button', { name: 'Create topic' })); + expect(onSubmit).toBeCalledTimes(1); }); }); diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.styled.spec.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.styled.spec.tsx new file mode 100644 index 00000000000..385da7381ea --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.styled.spec.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { render } from 'lib/testHelpers'; +import * as S from 'components/Topics/shared/Form/TopicForm.styled'; +import { screen } from '@testing-library/react'; +import { theme } from 'theme/theme'; + +describe('TopicForm styled components', () => { + describe('Button', () => { + it('should check the button styling in isActive state', () => { + render(); + const button = screen.getByRole('button'); + expect(button).toHaveStyle({ + border: `none`, + backgroundColor: theme.chips.backgroundColor.active, + }); + }); + + it('should check the button styling in non Active state', () => { + render(); + const button = screen.getByRole('button'); + expect(button).toHaveStyle({ + border: `none`, + backgroundColor: theme.chips.backgroundColor.normal, + }); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.styled.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.styled.tsx deleted file mode 100644 index c0ff8e50d0e..00000000000 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.styled.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import { render } from 'lib/testHelpers'; -import * as S from 'components/Topics/shared/Form/TopicForm.styled'; -import { screen } from '@testing-library/react'; -import theme from 'theme/theme'; - -describe('TopicForm styled components', () => { - describe('Button', () => { - it('should check the button styling in isActive state', () => { - render(); - const button = screen.getByRole('button'); - expect(button).toHaveStyle({ - border: `1px solid ${theme.button.border.active}`, - backgroundColor: theme.button.primary.backgroundColor.active, - }); - }); - - it('should check the button styling in non Active state', () => { - render(); - const button = screen.getByRole('button'); - expect(button).toHaveStyle({ - border: `1px solid ${theme.button.primary.color}`, - backgroundColor: theme.button.primary.backgroundColor.normal, - }); - }); - }); -}); diff --git a/kafka-ui-react-app/src/components/Version/Version.styled.ts b/kafka-ui-react-app/src/components/Version/Version.styled.ts index 8c70d9b9f91..62e22b7b0be 100644 --- a/kafka-ui-react-app/src/components/Version/Version.styled.ts +++ b/kafka-ui-react-app/src/components/Version/Version.styled.ts @@ -2,22 +2,22 @@ import styled, { css } from 'styled-components'; export const Wrapper = styled.div` display: flex; - align-items: center; + align-items: baseline; `; const textStyle = css` font-family: Inter, sans-serif; font-style: normal; font-weight: normal; - font-size: 12px; - line-height: 16px; + font-size: 14px; + line-height: 20px; `; export const CurrentVersion = styled.span( ({ theme }) => css` - ${textStyle} + ${textStyle}; color: ${theme.version.currentVersion.color}; - margin-right: 0.25rem; + margin-left: 0.25rem; ` ); @@ -25,13 +25,13 @@ export const OutdatedWarning = styled.span` ${textStyle} `; -export const SymbolWrapper = styled.span( +export const CurrentCommitLink = styled.a( ({ theme }) => css` - ${textStyle} - color: ${theme.version.symbolWrapper.color}; + ${textStyle}; + color: ${theme.version.commitLink.color}; + margin-left: 0.25rem; + &:hover { + color: ${theme.version.commitLink.color}; + } ` ); - -export const CurrentCommitLink = styled.a` - ${textStyle} -`; diff --git a/kafka-ui-react-app/src/components/Version/Version.tsx b/kafka-ui-react-app/src/components/Version/Version.tsx index ecda8f4a6ee..775fbfa5d58 100644 --- a/kafka-ui-react-app/src/components/Version/Version.tsx +++ b/kafka-ui-react-app/src/components/Version/Version.tsx @@ -1,59 +1,46 @@ -import React, { useEffect, useState } from 'react'; -import { gitCommitPath } from 'lib/paths'; -import { GIT_REPO_LATEST_RELEASE_LINK } from 'lib/constants'; +import React from 'react'; import WarningIcon from 'components/common/Icons/WarningIcon'; +import { gitCommitPath } from 'lib/paths'; +import { useLatestVersion } from 'lib/hooks/api/latestVersion'; +import { formatTimestamp } from 'lib/dateTimeHelpers'; import * as S from './Version.styled'; -import compareVersions from './compareVersions'; -export interface VesionProps { - tag: string; - commit?: string; -} +const Version: React.FC = () => { + const { data: latestVersionInfo = {} } = useLatestVersion(); + const { buildTime, commitId, isLatestRelease, version } = + latestVersionInfo.build; + const { versionTag } = latestVersionInfo?.latestRelease || ''; -const Version: React.FC = ({ tag, commit }) => { - const [latestVersionInfo, setLatestVersionInfo] = useState({ - outdated: false, - latestTag: '', - }); + const currentVersion = + isLatestRelease && version?.match(versionTag) + ? versionTag + : formatTimestamp(buildTime); - useEffect(() => { - fetch(GIT_REPO_LATEST_RELEASE_LINK) - .then((response) => response.json()) - .then((data) => { - setLatestVersionInfo({ - outdated: compareVersions(tag, data.tag_name) === -1, - latestTag: data.tag_name, - }); - }); - }, [tag]); - - const { outdated, latestTag } = latestVersionInfo; return ( - {tag} - - {outdated && ( + {!isLatestRelease && ( )} - {commit && ( - <> - ( + {commitId && ( +
    - {commit} + {commitId} - ) - +
    )} + {currentVersion}
    ); }; diff --git a/kafka-ui-react-app/src/components/Version/__tests__/Version.spec.tsx b/kafka-ui-react-app/src/components/Version/__tests__/Version.spec.tsx index 2136032bcb4..2700dac8947 100644 --- a/kafka-ui-react-app/src/components/Version/__tests__/Version.spec.tsx +++ b/kafka-ui-react-app/src/components/Version/__tests__/Version.spec.tsx @@ -1,22 +1,41 @@ import React from 'react'; -import Version, { VesionProps } from 'components/Version/Version'; -import { screen } from '@testing-library/react'; +import { screen } from '@testing-library/dom'; +import Version from 'components/Version/Version'; import { render } from 'lib/testHelpers'; +import { useLatestVersion } from 'lib/hooks/api/latestVersion'; +import { + deprecatedVersionPayload, + latestVersionPayload, +} from 'lib/fixtures/latestVersion'; -const tag = 'v1.0.1-SHAPSHOT'; -const commit = '123sdf34'; +jest.mock('lib/hooks/api/latestVersion', () => ({ + useLatestVersion: jest.fn(), +})); +describe('Version Component', () => { + const commitId = '96a577a'; -describe('Version', () => { - const setupComponent = (props: VesionProps) => render(); + describe('render latest version', () => { + beforeEach(() => { + (useLatestVersion as jest.Mock).mockImplementation(() => ({ + data: latestVersionPayload, + })); + }); + it('renders latest release version as current version', async () => { + render(); + expect(screen.getByText(commitId)).toBeInTheDocument(); + }); - it('renders', () => { - setupComponent({ tag }); - expect(screen.getByText(tag)).toBeInTheDocument(); + it('should not show warning icon if it is last release', async () => { + render(); + expect(screen.queryByRole('img')).not.toBeInTheDocument(); + }); }); - it('shows current tag and commit', () => { - setupComponent({ tag, commit }); - expect(screen.getByText(tag)).toBeInTheDocument(); - expect(screen.getByText(commit)).toBeInTheDocument(); + it('show warning icon if it is not last release', async () => { + (useLatestVersion as jest.Mock).mockImplementation(() => ({ + data: deprecatedVersionPayload, + })); + render(); + expect(screen.getByRole('img')).toBeInTheDocument(); }); }); diff --git a/kafka-ui-react-app/src/components/__tests__/App.spec.tsx b/kafka-ui-react-app/src/components/__tests__/App.spec.tsx index c7cf41ecd8d..feb41e6a82d 100644 --- a/kafka-ui-react-app/src/components/__tests__/App.spec.tsx +++ b/kafka-ui-react-app/src/components/__tests__/App.spec.tsx @@ -1,66 +1,42 @@ import React from 'react'; -import { screen, within, act } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import App from 'components/App'; import { render } from 'lib/testHelpers'; -import { clustersPayload } from 'redux/reducers/clusters/__test__/fixtures'; -import userEvent from '@testing-library/user-event'; -import fetchMock from 'fetch-mock'; +import { useGetUserInfo } from 'lib/hooks/api/roles'; +import { useAppInfo } from 'lib/hooks/api/appConfig'; + +jest.mock('components/Nav/Nav', () => () =>
    Navigation
    ); + +jest.mock('components/Version/Version', () => () =>
    Version
    ); + +jest.mock('components/NavBar/NavBar', () => () =>
    NavBar
    ); + +jest.mock('lib/hooks/api/roles', () => ({ + useGetUserInfo: jest.fn(), +})); +jest.mock('lib/hooks/api/appConfig', () => ({ + useAppInfo: jest.fn(), +})); describe('App', () => { - describe('initial state', () => { - beforeEach(() => { - render(, { - initialEntries: ['/'], - }); - }); - it('shows PageLoader until clusters are fulfilled', () => { - expect(screen.getByText('Dashboard')).toBeInTheDocument(); - expect(screen.getByRole('progressbar')).toBeInTheDocument(); - }); - it('correctly renders header', () => { - const header = screen.getByLabelText('Page Header'); - expect(header).toBeInTheDocument(); - expect( - within(header).getByText('UI for Apache Kafka') - ).toBeInTheDocument(); - expect(within(header).getAllByRole('separator').length).toEqual(3); - expect(within(header).getByRole('button')).toBeInTheDocument(); - }); - it('handle burger click correctly', () => { - const header = screen.getByLabelText('Page Header'); - const burger = within(header).getByRole('button'); - const sidebar = screen.getByLabelText('Sidebar'); - const overlay = screen.getByLabelText('Overlay'); - expect(sidebar).toBeInTheDocument(); - expect(overlay).toBeInTheDocument(); - expect(overlay).toHaveStyleRule('visibility: hidden'); - expect(burger).toHaveStyleRule('display: none'); - userEvent.click(burger); - expect(overlay).toHaveStyleRule('visibility: visible'); + beforeEach(() => { + (useGetUserInfo as jest.Mock).mockImplementation(() => ({ + data: {}, + })); + (useAppInfo as jest.Mock).mockImplementation(() => ({ + data: {}, + })); + + render(, { + initialEntries: ['/'], }); }); - describe('with clusters list fetched', () => { - it('shows Cluster list', async () => { - const mock = fetchMock.getOnce('/api/clusters', clustersPayload); - await act(() => { - render(, { - initialEntries: ['/'], - }); - }); - - expect(mock.called()).toBeTruthy(); + it('Renders navigation', async () => { + expect(screen.getByText('Navigation')).toBeInTheDocument(); + }); - const menuContainer = screen.getByLabelText('Sidebar Menu'); - expect(menuContainer).toBeInTheDocument(); - expect(within(menuContainer).getByText('Dashboard')).toBeInTheDocument(); - expect( - within(menuContainer).getByText(clustersPayload[0].name) - ).toBeInTheDocument(); - expect( - within(menuContainer).getByText(clustersPayload[1].name) - ).toBeInTheDocument(); - expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); - }); + it('Renders NavBar', async () => { + expect(screen.getByText('NavBar')).toBeInTheDocument(); }); }); diff --git a/kafka-ui-react-app/src/components/common/ActionComponent/ActionButton/ActionButton.tsx b/kafka-ui-react-app/src/components/common/ActionComponent/ActionButton/ActionButton.tsx new file mode 100644 index 00000000000..b7be1a97b59 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/ActionComponent/ActionButton/ActionButton.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Props as ButtonProps } from 'components/common/Button/Button'; +import { ActionComponentProps } from 'components/common/ActionComponent/ActionComponent'; +import { Action } from 'generated-sources'; +import ActionPermissionButton from 'components/common/ActionComponent/ActionButton/ActionPermissionButton/ActionPermissionButton'; +import ActionCreateButton from 'components/common/ActionComponent/ActionButton//ActionCreateButton/ActionCreateButton'; + +interface Props extends ActionComponentProps, ButtonProps {} + +const ActionButton: React.FC = ({ permission, ...props }) => { + return permission.action === Action.CREATE ? ( + + ) : ( + + ); +}; + +export default ActionButton; diff --git a/kafka-ui-react-app/src/components/common/ActionComponent/ActionButton/ActionCanButton/ActionCanButton.tsx b/kafka-ui-react-app/src/components/common/ActionComponent/ActionButton/ActionCanButton/ActionCanButton.tsx new file mode 100644 index 00000000000..00d16d43ba4 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/ActionComponent/ActionButton/ActionCanButton/ActionCanButton.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Button, Props as ButtonProps } from 'components/common/Button/Button'; +import * as S from 'components/common/ActionComponent/ActionComponent.styled'; +import { + ActionComponentProps, + getDefaultActionMessage, +} from 'components/common/ActionComponent/ActionComponent'; +import { useActionTooltip } from 'lib/hooks/useActionTooltip'; + +interface Props extends Omit, ButtonProps { + canDoAction: boolean; +} + +const ActionButton: React.FC = ({ + placement = 'bottom-end', + message = getDefaultActionMessage(), + disabled, + canDoAction, + ...props +}) => { + const isDisabled = !canDoAction; + + const { x, y, reference, floating, strategy, open } = useActionTooltip( + isDisabled, + placement + ); + + return ( + + - - - - - ) : null; + + + + ); }; export default ConfirmationModal; diff --git a/kafka-ui-react-app/src/components/common/ConfirmationModal/__test__/ConfirmationModal.spec.tsx b/kafka-ui-react-app/src/components/common/ConfirmationModal/__test__/ConfirmationModal.spec.tsx deleted file mode 100644 index b2627d5bc9b..00000000000 --- a/kafka-ui-react-app/src/components/common/ConfirmationModal/__test__/ConfirmationModal.spec.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React from 'react'; -import ConfirmationModal, { - ConfirmationModalProps, -} from 'components/common/ConfirmationModal/ConfirmationModal'; -import { screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { render } from 'lib/testHelpers'; - -const confirmMock = jest.fn(); -const cancelMock = jest.fn(); -const body = 'Please Confirm the action!'; - -describe('ConfirmationModal', () => { - const setupWrapper = (props: Partial = {}) => ( - - {body} - - ); - - it('renders nothing', () => { - render(setupWrapper({ isOpen: false })); - expect(screen.queryByText(body)).not.toBeInTheDocument(); - }); - - it('renders modal', () => { - render(setupWrapper({ isOpen: true })); - expect(screen.getByRole('dialog')).toHaveTextContent(body); - expect(screen.getAllByRole('button').length).toEqual(2); - }); - it('renders modal with default header', () => { - render(setupWrapper({ isOpen: true })); - expect(screen.getByText('Confirm the action')).toBeInTheDocument(); - }); - it('renders modal with custom header', () => { - const title = 'My Custom Header'; - render(setupWrapper({ isOpen: true, title })); - expect(screen.getByText(title)).toBeInTheDocument(); - }); - - it('Check the text on the submit button default behavior', () => { - render(setupWrapper({ isOpen: true })); - expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument(); - }); - - it('handles onConfirm when user clicks confirm button', () => { - render(setupWrapper({ isOpen: true })); - const confirmBtn = screen.getByRole('button', { name: 'Submit' }); - userEvent.click(confirmBtn); - expect(cancelMock).toHaveBeenCalledTimes(0); - expect(confirmMock).toHaveBeenCalledTimes(1); - }); - - it('Check the text on the submit button', () => { - const submitBtnText = 'Submit btn Text'; - render(setupWrapper({ isOpen: true, submitBtnText })); - expect( - screen.getByRole('button', { name: submitBtnText }) - ).toBeInTheDocument(); - }); - - describe('cancellation', () => { - describe('when not confirming', () => { - beforeEach(() => { - render(setupWrapper({ isOpen: true })); - }); - - it('handles onCancel when user clicks on modal-background', () => { - const { container } = render(setupWrapper({ isOpen: true })); - userEvent.click(container.children[0].children[0]); - - expect(cancelMock).toHaveBeenCalledTimes(1); - expect(confirmMock).toHaveBeenCalledTimes(0); - }); - it('handles onCancel when user clicks on Cancel button', () => { - const cancelBtn = screen.getByRole('button', { name: 'Cancel' }); - - userEvent.click(cancelBtn); - expect(cancelMock).toHaveBeenCalledTimes(1); - expect(confirmMock).toHaveBeenCalledTimes(0); - }); - }); - - describe('when confirming', () => { - beforeEach(() => { - render(setupWrapper({ isOpen: true, isConfirming: true })); - }); - it('does not call onCancel when user clicks on modal-background', () => { - userEvent.click(screen.getByRole('dialog')); - expect(cancelMock).toHaveBeenCalledTimes(0); - expect(confirmMock).toHaveBeenCalledTimes(0); - }); - - it('does not call onCancel when user clicks on Cancel button', () => { - const cancelBtn = screen.getByRole('button', { name: 'Cancel' }); - userEvent.click(cancelBtn); - expect(cancelMock).toHaveBeenCalledTimes(0); - expect(confirmMock).toHaveBeenCalledTimes(0); - }); - }); - }); -}); diff --git a/kafka-ui-react-app/src/components/common/ControlPanel/ControlPanel.styled.ts b/kafka-ui-react-app/src/components/common/ControlPanel/ControlPanel.styled.ts index a1453183c29..7b61ca6efad 100644 --- a/kafka-ui-react-app/src/components/common/ControlPanel/ControlPanel.styled.ts +++ b/kafka-ui-react-app/src/components/common/ControlPanel/ControlPanel.styled.ts @@ -7,10 +7,11 @@ interface Props { export const ControlPanelWrapper = styled.div` display: flex; align-items: center; - padding: 0px 16px; - margin: 16px 0px; + padding: 0 16px; + margin: 0 0 16px; width: 100%; gap: 16px; + color: ${({ theme }) => theme.default.color.normal}; & > *:first-child { width: ${(props) => (props.hasInput ? '38%' : 'auto')}; } diff --git a/kafka-ui-react-app/src/components/common/DiffViewer/DiffViewer.tsx b/kafka-ui-react-app/src/components/common/DiffViewer/DiffViewer.tsx index 18abc774d15..b38b4f0af98 100644 --- a/kafka-ui-react-app/src/components/common/DiffViewer/DiffViewer.tsx +++ b/kafka-ui-react-app/src/components/common/DiffViewer/DiffViewer.tsx @@ -1,4 +1,5 @@ import { diff as DiffEditor } from 'react-ace'; +import 'ace-builds/src-noconflict/ace'; import 'ace-builds/src-noconflict/mode-json5'; import 'ace-builds/src-noconflict/mode-protobuf'; import 'ace-builds/src-noconflict/theme-textmate'; diff --git a/kafka-ui-react-app/src/components/common/Dropdown/Dropdown.styled.ts b/kafka-ui-react-app/src/components/common/Dropdown/Dropdown.styled.ts index 26aef95bcea..d7db888a096 100644 --- a/kafka-ui-react-app/src/components/common/Dropdown/Dropdown.styled.ts +++ b/kafka-ui-react-app/src/components/common/Dropdown/Dropdown.styled.ts @@ -1,30 +1,88 @@ -import styled from 'styled-components'; +import styled, { css, keyframes } from 'styled-components'; +import { ControlledMenu } from '@szhsin/react-menu'; +import { menuSelector, menuItemSelector } from '@szhsin/react-menu/style-utils'; -export const TriggerWrapper = styled.div` - display: flex; - align-self: center; +import '@szhsin/react-menu/dist/core.css'; + +const menuShow = keyframes` + from { + opacity: 0; + } `; +const menuHide = keyframes` + to { + opacity: 0; + } +`; + +export const Dropdown = styled(ControlledMenu)( + ({ theme: { dropdown } }) => css` + // container for the menu items + ${menuSelector.name} { + border: 1px solid ${dropdown.borderColor}; + box-shadow: 0 4px 16px ${dropdown.shadow}; + padding: 8px 0; + border-radius: 4px; + font-size: 14px; + background-color: ${dropdown.backgroundColor}; + text-align: left; + } + + ${menuSelector.stateOpening} { + animation: ${menuShow} 0.15s ease-out; + } + + // NOTE: animation-fill-mode: forwards is required to + // prevent flickering with React 18 createRoot() + ${menuSelector.stateClosing} { + animation: ${menuHide} 0.2s ease-out forwards; + } -export const Trigger = styled.button.attrs({ - type: 'button', - ariaHaspopup: 'true', - ariaControls: 'dropdown-menu', -})` - background: transparent; + ${menuItemSelector.name} { + padding: 6px 16px; + min-width: 150px; + background-color: ${dropdown.item.backgroundColor.default}; + white-space: nowrap; + } + + ${menuItemSelector.hover} { + background-color: ${dropdown.item.backgroundColor.hover}; + } + + ${menuItemSelector.disabled} { + cursor: not-allowed; + opacity: 0.5; + } + ` +); + +export const DropdownButton = styled.button` + background-color: transparent; border: none; display: flex; - align-items: 'center'; - justify-content: 'center'; - &:hover { - cursor: pointer; + cursor: pointer; + align-self: center; + + &:disabled { + opacity: 0.5; + cursor: not-allowed; } `; -export const Item = styled.a.attrs({ - href: '#end', - role: 'menuitem', - type: 'button', -})<{ $isDanger: boolean }>` - color: ${({ $isDanger, theme }) => - $isDanger ? theme.dropdown.color : 'initial'}; +export const DangerItem = styled.div` + color: ${({ theme: { dropdown } }) => dropdown.item.color.danger}; +`; + +export const DropdownItemHint = styled.div` + color: ${({ theme }) => theme.topicMetaData.color.label}; + font-size: 12px; + line-height: 1.4; + margin-top: 5px; +`; + +export const Wrapper = styled.div` + display: inline-flex; + align-items: center; + justify-content: end; + color: ${({ theme: { dropdown } }) => dropdown.item.color.normal}; `; diff --git a/kafka-ui-react-app/src/components/common/Dropdown/Dropdown.tsx b/kafka-ui-react-app/src/components/common/Dropdown/Dropdown.tsx index 073f79ddfe8..daf26ac1280 100644 --- a/kafka-ui-react-app/src/components/common/Dropdown/Dropdown.tsx +++ b/kafka-ui-react-app/src/components/common/Dropdown/Dropdown.tsx @@ -1,46 +1,48 @@ -import useOutsideClickRef from '@rooks/use-outside-click-ref'; -import cx from 'classnames'; -import React, { PropsWithChildren, useMemo, useState } from 'react'; +import { MenuProps } from '@szhsin/react-menu'; +import React, { PropsWithChildren, useRef } from 'react'; +import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon'; +import useBoolean from 'lib/hooks/useBoolean'; import * as S from './Dropdown.styled'; -export interface DropdownProps { - label: React.ReactNode; - right?: boolean; - up?: boolean; +interface DropdownProps extends PropsWithChildren> { + label?: React.ReactNode; + disabled?: boolean; } -const Dropdown: React.FC> = ({ - label, - right, - up, - children, -}) => { - const [active, setActive] = useState(false); - const [wrapperRef] = useOutsideClickRef(() => setActive(false)); - const onClick = (e: React.MouseEvent) => { +const Dropdown: React.FC = ({ label, disabled, children }) => { + const ref = useRef(null); + const { value: isOpen, setFalse, setTrue } = useBoolean(false); + + const handleClick: React.MouseEventHandler = (e) => { + e.preventDefault(); e.stopPropagation(); - setActive(!active); + setTrue(); }; - const classNames = useMemo( - () => - cx('dropdown', { - 'is-active': active, - 'is-right': right, - 'is-up': up, - }), - [active, right, up] - ); return ( -
    - - {label} - - -
    + + + {label || } + + + {children} + + ); }; diff --git a/kafka-ui-react-app/src/components/common/Dropdown/DropdownDivider.tsx b/kafka-ui-react-app/src/components/common/Dropdown/DropdownDivider.tsx deleted file mode 100644 index d9a8dc1ecdf..00000000000 --- a/kafka-ui-react-app/src/components/common/Dropdown/DropdownDivider.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -const DropdownDivider: React.FC = () =>
    ; - -export default DropdownDivider; diff --git a/kafka-ui-react-app/src/components/common/Dropdown/DropdownItem.tsx b/kafka-ui-react-app/src/components/common/Dropdown/DropdownItem.tsx index ccfa5222668..4caf6b5f637 100644 --- a/kafka-ui-react-app/src/components/common/Dropdown/DropdownItem.tsx +++ b/kafka-ui-react-app/src/components/common/Dropdown/DropdownItem.tsx @@ -1,32 +1,39 @@ import React, { PropsWithChildren } from 'react'; +import { ClickEvent, MenuItem, MenuItemProps } from '@szhsin/react-menu'; +import { useConfirm } from 'lib/hooks/useConfirm'; import * as S from './Dropdown.styled'; -export interface DropdownItemProps { - onClick(): void; +export interface DropdownItemProps extends PropsWithChildren { danger?: boolean; + onClick?(): void; + confirm?: React.ReactNode; } -const DropdownItem: React.FC> = ({ - onClick, - danger, - children, -}) => { - const onClickHandler = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - onClick(); - }; - - return ( - - {children} - - ); -}; +const DropdownItem = React.forwardRef( + ({ onClick, danger, children, confirm, ...rest }, ref) => { + const confirmation = useConfirm(); + + const handleClick = (e: ClickEvent) => { + if (!onClick) return; + + // eslint-disable-next-line no-param-reassign + e.stopPropagation = true; + e.syntheticEvent.stopPropagation(); + + if (confirm) { + confirmation(confirm, onClick); + } else { + onClick(); + } + }; + + return ( + + {danger ? {children} : children} + + ); + } +); export default DropdownItem; diff --git a/kafka-ui-react-app/src/components/common/Dropdown/__tests__/Dropdown.spec.tsx b/kafka-ui-react-app/src/components/common/Dropdown/__tests__/Dropdown.spec.tsx deleted file mode 100644 index 3df18aa511b..00000000000 --- a/kafka-ui-react-app/src/components/common/Dropdown/__tests__/Dropdown.spec.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React from 'react'; -import Dropdown, { DropdownProps } from 'components/common/Dropdown/Dropdown'; -import DropdownItem from 'components/common/Dropdown/DropdownItem'; -import DropdownDivider from 'components/common/Dropdown/DropdownDivider'; -import userEvent from '@testing-library/user-event'; -import { render } from 'lib/testHelpers'; -import { screen } from '@testing-library/react'; - -const dummyLable = 'My Test Label'; -const dummyChildren = ( - <> - Child 1 - Child 2 - - Child 3 - -); - -describe('Dropdown', () => { - const setupWrapper = ( - props: Partial = {}, - children: React.ReactNode = undefined - ) => ( - - {children} - - ); - - it('renders Dropdown with initial props', () => { - const wrapper = render(setupWrapper()).baseElement; - expect(wrapper.querySelector('.dropdown')).toBeTruthy(); - - expect(wrapper.querySelector('.dropdown.is-active')).toBeFalsy(); - expect(wrapper.querySelector('.dropdown.is-right')).toBeFalsy(); - expect(wrapper.querySelector('.dropdown.is-up')).toBeFalsy(); - - expect(wrapper.querySelector('.dropdown-content')).toBeTruthy(); - expect(wrapper.querySelector('.dropdown-content')).toHaveTextContent(''); - }); - - it('renders custom children', () => { - const wrapper = render(setupWrapper({}, dummyChildren)).baseElement; - expect(wrapper.querySelector('.dropdown-content')).toBeTruthy(); - expect(wrapper.querySelectorAll('.dropdown-item').length).toEqual(3); - expect(wrapper.querySelectorAll('.dropdown-divider').length).toEqual(1); - }); - - it('renders dropdown with a right-aligned menu', () => { - const wrapper = render(setupWrapper({ right: true })).baseElement; - expect(wrapper.querySelector('.dropdown.is-right')).toBeTruthy(); - }); - - it('renders dropdown with a popup menu', () => { - const wrapper = render(setupWrapper({ up: true })).baseElement; - expect(wrapper.querySelector('.dropdown.is-up')).toBeTruthy(); - }); - - it('handles click', () => { - const wrapper = render(setupWrapper()).baseElement; - const button = screen.getByText('My Test Label'); - - expect(button).toBeInTheDocument(); - expect(wrapper.querySelector('.dropdown.is-active')).toBeFalsy(); - - userEvent.click(button); - expect(wrapper.querySelector('.dropdown.is-active')).toBeTruthy(); - }); - - it('to be in the document', () => { - render( - setupWrapper( - { - right: true, - up: true, - }, - dummyChildren - ) - ); - expect(screen.getByRole('menu')).toBeInTheDocument(); - }); -}); diff --git a/kafka-ui-react-app/src/components/common/Dropdown/__tests__/DropdownItem.spec.tsx b/kafka-ui-react-app/src/components/common/Dropdown/__tests__/DropdownItem.spec.tsx deleted file mode 100644 index 7efe0bcbb2c..00000000000 --- a/kafka-ui-react-app/src/components/common/Dropdown/__tests__/DropdownItem.spec.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import DropdownItem from 'components/common/Dropdown/DropdownItem'; -import { render } from 'lib/testHelpers'; -import userEvent from '@testing-library/user-event'; -import { screen } from '@testing-library/react'; - -const onClick = jest.fn(); - -describe('DropdownItem', () => { - it('to be in the document', () => { - render(Item 1); - expect(screen.getByText('Item 1')).toBeInTheDocument(); - }); - - it('handles Click', () => { - render(Item 1); - userEvent.click(screen.getByText('Item 1')); - expect(onClick).toHaveBeenCalled(); - }); -}); diff --git a/kafka-ui-react-app/src/components/common/Dropdown/index.ts b/kafka-ui-react-app/src/components/common/Dropdown/index.ts new file mode 100644 index 00000000000..c11d5f30402 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Dropdown/index.ts @@ -0,0 +1,5 @@ +import { DropdownItemHint } from './Dropdown.styled'; +import Dropdown from './Dropdown'; +import DropdownItem from './DropdownItem'; + +export { Dropdown, DropdownItem, DropdownItemHint }; diff --git a/kafka-ui-react-app/src/components/common/DynamicTextButton/DynamicTextButton.tsx b/kafka-ui-react-app/src/components/common/DynamicTextButton/DynamicTextButton.tsx deleted file mode 100644 index 732cfa5178f..00000000000 --- a/kafka-ui-react-app/src/components/common/DynamicTextButton/DynamicTextButton.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React, { useRef } from 'react'; -import cx from 'classnames'; - -interface DynamicTextButtonProps { - onClick(): void; - className?: string; - title: string; - delay?: number; - render(clicked: boolean): React.ReactNode; -} - -const DynamicTextButton: React.FC = ({ - onClick, - className, - title, - render, - delay = 3000, -}) => { - const [clicked, setClicked] = React.useState(false); - - const timeout = useRef(0); - - const clickHandler = () => { - onClick(); - setClicked(true); - timeout.current = window.setTimeout(() => setClicked(false), delay); - }; - - React.useEffect(() => () => window.clearTimeout(timeout.current)); - - return ( - - ); -}; - -export default DynamicTextButton; diff --git a/kafka-ui-react-app/src/components/common/DynamicTextButton/__tests__/DynamicTextButton.spec.tsx b/kafka-ui-react-app/src/components/common/DynamicTextButton/__tests__/DynamicTextButton.spec.tsx deleted file mode 100644 index 21dec04e806..00000000000 --- a/kafka-ui-react-app/src/components/common/DynamicTextButton/__tests__/DynamicTextButton.spec.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import DynamicTextButton from 'components/common/DynamicTextButton/DynamicTextButton'; -import { render } from 'lib/testHelpers'; -import userEvent from '@testing-library/user-event'; -import { screen } from '@testing-library/react'; - -describe('DynamicButton', () => { - const mockCallback = jest.fn(); - it('exectutes callback', () => { - render( - 'text'} - /> - ); - - userEvent.click(screen.getByTitle('title')); - expect(mockCallback).toBeCalled(); - }); - - it('changes the text', () => { - render( - (clicked ? 'active' : 'default')} - /> - ); - - const button = screen.getByTitle('title'); - expect(button).toHaveTextContent('default'); - userEvent.click(button); - expect(button).toHaveTextContent('active'); - }); -}); diff --git a/kafka-ui-react-app/src/components/common/Editor/Editor.tsx b/kafka-ui-react-app/src/components/common/Editor/Editor.tsx index 76a63771865..05c91e3557b 100644 --- a/kafka-ui-react-app/src/components/common/Editor/Editor.tsx +++ b/kafka-ui-react-app/src/components/common/Editor/Editor.tsx @@ -1,11 +1,9 @@ -/* eslint-disable react/jsx-props-no-spreading */ import AceEditor, { IAceEditorProps } from 'react-ace'; import 'ace-builds/src-noconflict/mode-json5'; import 'ace-builds/src-noconflict/mode-protobuf'; import 'ace-builds/src-noconflict/theme-tomorrow'; import { SchemaType } from 'generated-sources'; import React from 'react'; -import ReactAce from 'react-ace/lib/ace'; import styled from 'styled-components'; interface EditorProps extends IAceEditorProps { @@ -13,7 +11,7 @@ interface EditorProps extends IAceEditorProps { schemaType?: string; } -const Editor = React.forwardRef((props, ref) => { +const Editor = React.forwardRef((props, ref) => { const { isFixedHeight, schemaType, ...rest } = props; return ( + theme.ksqlDb.query.editor.layer.backgroundColor}; + } + .ace_gutter-active-line { + background-color: ${({ theme }) => + theme.ksqlDb.query.editor.cell.backgroundColor}; + color: ${({ theme }) => theme.default.color.normal}; + } + .ace_scroller { + background-color: ${({ theme }) => theme.default.backgroundColor}; + } + .ace_line { + color: ${({ theme }) => theme.default.color.normal}; + } + .ace_cursor { + color: ${({ theme }) => theme.ksqlDb.query.editor.cursor}; + } + .ace_active-line { + background-color: ${({ theme }) => + theme.ksqlDb.query.editor.cell.backgroundColor}; + } + .ace_gutter-cell { + color: ${({ theme }) => theme.default.color.normal}; + } + .ace_variable { + color: ${({ theme }) => theme.ksqlDb.query.editor.variable}; + } + .ace_string { + color: ${({ theme }) => theme.ksqlDb.query.editor.aceString}; + } + .ace_print-margin { + display: none; + } } `; diff --git a/kafka-ui-react-app/src/components/common/EditorViewer/EditorViewer.styled.ts b/kafka-ui-react-app/src/components/common/EditorViewer/EditorViewer.styled.ts new file mode 100644 index 00000000000..a135f4dfcc2 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/EditorViewer/EditorViewer.styled.ts @@ -0,0 +1,13 @@ +import styled from 'styled-components'; + +export const Wrapper = styled.div` + background-color: ${({ theme }) => theme.viewer.wrapper.backgroundColor}; + padding: 8px 16px; + .ace_active-line { + background-color: ${({ theme }) => + theme.default.backgroundColor} !important; + } + .ace_line { + color: ${({ theme }) => theme.viewer.wrapper.color} !important; + } +`; diff --git a/kafka-ui-react-app/src/components/common/EditorViewer/EditorViewer.tsx b/kafka-ui-react-app/src/components/common/EditorViewer/EditorViewer.tsx index 8caff44cbb8..4b46fafbf1d 100644 --- a/kafka-ui-react-app/src/components/common/EditorViewer/EditorViewer.tsx +++ b/kafka-ui-react-app/src/components/common/EditorViewer/EditorViewer.tsx @@ -1,29 +1,29 @@ import React from 'react'; import Editor from 'components/common/Editor/Editor'; import { SchemaType } from 'generated-sources'; +import { parse, stringify } from 'lossless-json'; -import { StyledWrapper } from './StyledWrapper.styled'; +import * as S from './EditorViewer.styled'; -export interface FullMessageProps { +export interface EditorViewerProps { data: string; schemaType?: string; maxLines?: number; } - const getSchemaValue = (data: string, schemaType?: string) => { if (schemaType === SchemaType.JSON || schemaType === SchemaType.AVRO) { - return JSON.stringify(JSON.parse(data), null, '\t'); + return stringify(parse(data), undefined, '\t'); } return data; }; -const EditorViewer: React.FC = ({ +const EditorViewer: React.FC = ({ data, schemaType, maxLines, }) => { try { return ( - + = ({ }} readOnly /> - + ); } catch (e) { return ( - +

    {data}

    -
    + ); } }; diff --git a/kafka-ui-react-app/src/components/common/EditorViewer/StyledWrapper.styled.ts b/kafka-ui-react-app/src/components/common/EditorViewer/StyledWrapper.styled.ts deleted file mode 100644 index cd2bccaae54..00000000000 --- a/kafka-ui-react-app/src/components/common/EditorViewer/StyledWrapper.styled.ts +++ /dev/null @@ -1,8 +0,0 @@ -import styled, { css } from 'styled-components'; - -export const StyledWrapper = styled.div( - ({ theme }) => css` - background-color: ${theme.viewer.wrapper}; - padding: 8px 16px; - ` -); diff --git a/kafka-ui-react-app/src/components/common/EditorViewer/__test__/EditorViewer.spec.tsx b/kafka-ui-react-app/src/components/common/EditorViewer/__test__/EditorViewer.spec.tsx index 13989784973..87a1b380ddc 100644 --- a/kafka-ui-react-app/src/components/common/EditorViewer/__test__/EditorViewer.spec.tsx +++ b/kafka-ui-react-app/src/components/common/EditorViewer/__test__/EditorViewer.spec.tsx @@ -1,6 +1,6 @@ import React from 'react'; import EditorViewer, { - FullMessageProps, + EditorViewerProps, } from 'components/common/EditorViewer/EditorViewer'; import { render } from 'lib/testHelpers'; import { screen } from '@testing-library/react'; @@ -10,7 +10,7 @@ const maxLines = 28; const schemaType = 'JSON'; describe('EditorViewer component', () => { - const setupComponent = (props: FullMessageProps) => + const setupComponent = (props: EditorViewerProps) => render(); it('renders JSONTree', () => { diff --git a/kafka-ui-react-app/src/components/common/Ellipsis/Ellipsis.styled.ts b/kafka-ui-react-app/src/components/common/Ellipsis/Ellipsis.styled.ts new file mode 100644 index 00000000000..d301090b46c --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Ellipsis/Ellipsis.styled.ts @@ -0,0 +1,14 @@ +import styled from 'styled-components'; + +export const Text = styled.div` + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + max-width: 340px; +`; + +export const Wrapper = styled.div` + display: flex; + gap: 8px; + align-items: center; +`; diff --git a/kafka-ui-react-app/src/components/common/Ellipsis/Ellipsis.tsx b/kafka-ui-react-app/src/components/common/Ellipsis/Ellipsis.tsx new file mode 100644 index 00000000000..f6a690d9c60 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Ellipsis/Ellipsis.tsx @@ -0,0 +1,20 @@ +import React, { PropsWithChildren } from 'react'; + +import * as S from './Ellipsis.styled'; + +type EllipsisProps = { + text: React.ReactNode; +}; + +const Ellipsis: React.FC> = ({ + text, + children, +}) => { + return ( + + {text} + {children} + + ); +}; +export default Ellipsis; diff --git a/kafka-ui-react-app/src/components/common/Form/Form.styled.ts b/kafka-ui-react-app/src/components/common/Form/Form.styled.ts index 6316b7b0f78..b9a598040c9 100644 --- a/kafka-ui-react-app/src/components/common/Form/Form.styled.ts +++ b/kafka-ui-react-app/src/components/common/Form/Form.styled.ts @@ -1,6 +1,28 @@ import styled from 'styled-components'; export const StyledForm = styled.form` - padding: 0 16px; + padding: 16px; max-width: 800px; + display: flex; + gap: 16px; + flex-direction: column; + + h3 { + margin-bottom: 0; + line-height: 32px; + } +`; + +export const FlexFieldset = styled.fieldset` + display: flex; + gap: 16px; + flex-direction: column; + + &:disabled { + ul { + opacity: 0.5; + background-color: #f5f5f5; + pointer-events: none; + } + } `; diff --git a/kafka-ui-react-app/src/components/common/Icons/ArrowDownIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/ArrowDownIcon.tsx new file mode 100644 index 00000000000..9f6bd245b3f --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/ArrowDownIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { useTheme } from 'styled-components'; + +const ArrowDownIcon: React.FC = () => { + const theme = useTheme(); + return ( + + {/* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */} + + + ); +}; + +export default ArrowDownIcon; diff --git a/kafka-ui-react-app/src/components/common/Icons/AutoIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/AutoIcon.tsx new file mode 100644 index 00000000000..2408378be81 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/AutoIcon.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { useTheme } from 'styled-components'; + +const AutoIcon: React.FC = () => { + const theme = useTheme(); + return ( + + + + + ); +}; + +export default AutoIcon; diff --git a/kafka-ui-react-app/src/components/common/Icons/CancelIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/CancelIcon.tsx new file mode 100644 index 00000000000..46b645af010 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/CancelIcon.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { useTheme } from 'styled-components'; + +const CancelIcon: React.FC = () => { + const theme = useTheme(); + return ( + + Cancel + A line styled icon from Orion Icon Library. + + + ); +}; + +export default CancelIcon; diff --git a/kafka-ui-react-app/src/components/common/Icons/CheckMarkRoundIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/CheckMarkRoundIcon.tsx new file mode 100644 index 00000000000..bbf212541e8 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/CheckMarkRoundIcon.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +const CheckMarkRoundIcon: React.FC = () => { + return ( + + + + ); +}; + +export default CheckMarkRoundIcon; diff --git a/kafka-ui-react-app/src/components/common/Icons/CheckmarkIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/CheckmarkIcon.tsx new file mode 100644 index 00000000000..9b93600d2c4 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/CheckmarkIcon.tsx @@ -0,0 +1,30 @@ +import React, { FC } from 'react'; + +const CheckmarkIcon: FC = () => { + return ( + + Checkmark + A line styled icon from Orion Icon Library. + + + ); +}; + +export default CheckmarkIcon; diff --git a/kafka-ui-react-app/src/components/common/Icons/ChevronDownIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/ChevronDownIcon.tsx new file mode 100644 index 00000000000..d9bf102474d --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/ChevronDownIcon.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useTheme } from 'styled-components'; + +const ChevronDownIcon: React.FC = () => { + const theme = useTheme(); + return ( + + + + ); +}; + +export default ChevronDownIcon; diff --git a/kafka-ui-react-app/src/components/common/Icons/ClockIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/ClockIcon.tsx new file mode 100644 index 00000000000..4c7de485a40 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/ClockIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { useTheme } from 'styled-components'; + +const ClockIcon: React.FC = () => { + const theme = useTheme(); + return ( + + {/* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */} + + + ); +}; + +export default ClockIcon; diff --git a/kafka-ui-react-app/src/components/common/Icons/CloseCircleIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/CloseCircleIcon.tsx new file mode 100644 index 00000000000..bdc64f02b00 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/CloseCircleIcon.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useTheme } from 'styled-components'; + +const CloseCircleIcon: React.FC = () => { + const theme = useTheme(); + return ( + + + + ); +}; + +export default CloseCircleIcon; diff --git a/kafka-ui-react-app/src/components/common/Icons/CloseIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/CloseIcon.tsx index 17e750e1204..ca66ba22238 100644 --- a/kafka-ui-react-app/src/components/common/Icons/CloseIcon.tsx +++ b/kafka-ui-react-app/src/components/common/Icons/CloseIcon.tsx @@ -1,24 +1,24 @@ import React from 'react'; -import { useTheme } from 'styled-components'; +import styled, { useTheme } from 'styled-components'; -const CloseIcon: React.FC = () => { +const CloseIcon: React.FC<{ className?: string }> = ({ className }) => { const theme = useTheme(); return ( ); }; -export default CloseIcon; +export default styled(CloseIcon)``; diff --git a/kafka-ui-react-app/src/components/common/Icons/DeleteIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/DeleteIcon.tsx new file mode 100644 index 00000000000..33a2be02167 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/DeleteIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { useTheme } from 'styled-components'; + +const DeleteIcon: React.FC<{ fill?: string }> = ({ fill }) => { + const theme = useTheme(); + const curentFill = fill || theme.editFilter.deleteIconColor; + return ( + + {/* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */} + + + ); +}; + +export default DeleteIcon; diff --git a/kafka-ui-react-app/src/components/common/Icons/DiscordIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/DiscordIcon.tsx new file mode 100644 index 00000000000..0df8701b93e --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/DiscordIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import styled from 'styled-components'; + +const DiscordIcon: React.FC<{ className?: string }> = ({ className }) => ( + + + +); + +export default styled(DiscordIcon)``; diff --git a/kafka-ui-react-app/src/components/common/Icons/DropdownArrowIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/DropdownArrowIcon.tsx index 22c783ac55a..5e826f76168 100644 --- a/kafka-ui-react-app/src/components/common/Icons/DropdownArrowIcon.tsx +++ b/kafka-ui-react-app/src/components/common/Icons/DropdownArrowIcon.tsx @@ -1,8 +1,10 @@ -import React from 'react'; +import React, { CSSProperties } from 'react'; import { useTheme } from 'styled-components'; interface Props { isOpen: boolean; + style?: CSSProperties; + color?: string; } const DropdownArrowIcon: React.FC = ({ isOpen }) => { @@ -10,15 +12,16 @@ const DropdownArrowIcon: React.FC = ({ isOpen }) => { return ( - + ); }; diff --git a/kafka-ui-react-app/src/components/common/Icons/EditIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/EditIcon.tsx new file mode 100644 index 00000000000..93e09c1b0d4 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/EditIcon.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import styled, { useTheme } from 'styled-components'; + +const EditIcon: React.FC<{ className?: string }> = ({ className }) => { + const theme = useTheme(); + return ( + + Edit + + + + ); +}; + +export default styled(EditIcon)``; diff --git a/kafka-ui-react-app/src/components/common/Icons/FileIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/FileIcon.tsx new file mode 100644 index 00000000000..a6db41ebaaf --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/FileIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { useTheme } from 'styled-components'; + +const FileIcon: React.FC = () => { + const theme = useTheme(); + return ( + + {/* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */} + + + ); +}; + +export default FileIcon; diff --git a/kafka-ui-react-app/src/components/common/Icons/GitIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/GitIcon.tsx new file mode 100644 index 00000000000..daecb611ff2 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/GitIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import styled from 'styled-components'; + +const GitIcon: React.FC<{ className?: string }> = ({ className }) => ( + + + +); + +export default styled(GitIcon)``; diff --git a/kafka-ui-react-app/src/components/common/Icons/InfoIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/InfoIcon.tsx new file mode 100644 index 00000000000..f9f3bcae437 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/InfoIcon.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { useTheme } from 'styled-components'; + +const InfoIcon: React.FC = () => { + const theme = useTheme(); + return ( + + + + + + ); +}; + +export default InfoIcon; diff --git a/kafka-ui-react-app/src/components/common/Icons/MessageToggleIcon.styled.ts b/kafka-ui-react-app/src/components/common/Icons/MessageToggleIcon.styled.ts index d779c86fd78..2762608c781 100644 --- a/kafka-ui-react-app/src/components/common/Icons/MessageToggleIcon.styled.ts +++ b/kafka-ui-react-app/src/components/common/Icons/MessageToggleIcon.styled.ts @@ -1,10 +1,19 @@ import styled from 'styled-components'; -export const Svg = styled.svg` +type Props = { + isOpen?: boolean; +}; +export const Svg = styled.svg` & > path { - fill: ${({ theme }) => theme.icons.messageToggleIcon.normal}; + fill: ${({ theme, isOpen }) => + isOpen + ? theme.icons.messageToggleIcon.active + : theme.icons.messageToggleIcon.normal}; &:hover { fill: ${({ theme }) => theme.icons.messageToggleIcon.hover}; } + &:active { + fill: ${({ theme }) => theme.icons.messageToggleIcon.active}; + } } `; diff --git a/kafka-ui-react-app/src/components/common/Icons/MessageToggleIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/MessageToggleIcon.tsx index a71d868719b..ec430879481 100644 --- a/kafka-ui-react-app/src/components/common/Icons/MessageToggleIcon.tsx +++ b/kafka-ui-react-app/src/components/common/Icons/MessageToggleIcon.tsx @@ -9,6 +9,7 @@ const MessageToggleIcon: React.FC = ({ isOpen }) => { if (isOpen) { return ( { + const theme = useTheme(); + return ( + + + + ); +}; + +export default MoonIcon; diff --git a/kafka-ui-react-app/src/components/common/Icons/PlusIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/PlusIcon.tsx new file mode 100644 index 00000000000..8c7f33a2002 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/PlusIcon.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +const PlusIcon: React.FC = () => { + return ( + + {/* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */} + + + ); +}; + +export default PlusIcon; diff --git a/kafka-ui-react-app/src/components/common/Icons/SavedIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/SavedIcon.tsx index 01895e2731b..1376a24d33f 100644 --- a/kafka-ui-react-app/src/components/common/Icons/SavedIcon.tsx +++ b/kafka-ui-react-app/src/components/common/Icons/SavedIcon.tsx @@ -9,7 +9,7 @@ const SavedIcon: FC = () => { width="18" height="20" viewBox="0 0 18 20" - fill="none" + fill={theme.icons.savedIcon} xmlns="http://www.w3.org/2000/svg" > ( + + {/* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */} + + +); + +export default SearchIcon; diff --git a/kafka-ui-react-app/src/components/common/Icons/SpinnerIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/SpinnerIcon.tsx new file mode 100644 index 00000000000..38ab1daa0c2 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/SpinnerIcon.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { useTheme } from 'styled-components'; + +const SpinnerIcon: React.FC = () => { + const theme = useTheme(); + return ( + + {/* By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL */} + + + + + + + + + + + + + + ); +}; + +export default SpinnerIcon; diff --git a/kafka-ui-react-app/src/components/common/Icons/StarIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/StarIcon.tsx new file mode 100644 index 00000000000..7dacf2b1b90 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/StarIcon.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +const StarIcon: React.FC = () => ( + + + +); + +export default StarIcon; diff --git a/kafka-ui-react-app/src/components/common/Icons/SunIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/SunIcon.tsx new file mode 100644 index 00000000000..7416f624dbe --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/SunIcon.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { useTheme } from 'styled-components'; + +const SunIcon: React.FC = () => { + const theme = useTheme(); + return ( + + + + + + + + + + + + ); +}; + +export default SunIcon; diff --git a/kafka-ui-react-app/src/components/common/Icons/UserIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/UserIcon.tsx new file mode 100644 index 00000000000..e9d462ed8b4 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/UserIcon.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const UserIcon = () => { + return ( + + + + ); +}; + +export default UserIcon; diff --git a/kafka-ui-react-app/src/components/common/Icons/WarningIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/WarningIcon.tsx index dec81b585f0..1bffe0db537 100644 --- a/kafka-ui-react-app/src/components/common/Icons/WarningIcon.tsx +++ b/kafka-ui-react-app/src/components/common/Icons/WarningIcon.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import styled, { useTheme } from 'styled-components'; +import styled from 'styled-components'; const WarningIconContainer = styled.span` align-items: center; @@ -10,27 +10,22 @@ const WarningIconContainer = styled.span` `; const WarningIcon: React.FC = () => { - const theme = useTheme(); - return ( - - - - - + ); diff --git a/kafka-ui-react-app/src/components/common/Icons/WarningRedIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/WarningRedIcon.tsx new file mode 100644 index 00000000000..13231f58944 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Icons/WarningRedIcon.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { useTheme } from 'styled-components'; + +const WarningRedIcon: React.FC = () => { + const theme = useTheme(); + return ( + + + + + + ); +}; + +export default WarningRedIcon; diff --git a/kafka-ui-react-app/src/components/common/IndeterminateCheckbox/IndeterminateCheckbox.tsx b/kafka-ui-react-app/src/components/common/IndeterminateCheckbox/IndeterminateCheckbox.tsx new file mode 100644 index 00000000000..58afff859c8 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/IndeterminateCheckbox/IndeterminateCheckbox.tsx @@ -0,0 +1,24 @@ +import React, { HTMLProps } from 'react'; +import styled from 'styled-components'; + +interface IndeterminateCheckboxProps extends HTMLProps { + indeterminate?: boolean; +} + +const IndeterminateCheckbox: React.FC = ({ + indeterminate, + ...rest +}) => { + const ref = React.useRef(null); + React.useEffect(() => { + if (typeof indeterminate === 'boolean' && ref.current) { + ref.current.indeterminate = !rest.checked && indeterminate; + } + }, [ref, indeterminate]); + + return ; +}; + +export default styled(IndeterminateCheckbox)` + cursor: pointer; +`; diff --git a/kafka-ui-react-app/src/components/common/Input/Input.styled.ts b/kafka-ui-react-app/src/components/common/Input/Input.styled.ts index 06bcce4f42b..f21962fe6b6 100644 --- a/kafka-ui-react-app/src/components/common/Input/Input.styled.ts +++ b/kafka-ui-react-app/src/components/common/Input/Input.styled.ts @@ -1,45 +1,86 @@ import styled, { css } from 'styled-components'; export interface InputProps { - inputSize?: 'M' | 'L'; - hasLeftIcon: boolean; + inputSize?: 'S' | 'M' | 'L'; + search: boolean; } +const INPUT_SIZES = { + S: '24px', + M: '32px', + L: '40px', +}; + +export const Wrapper = styled.div` + position: relative; + &:hover { + svg:first-child { + fill: ${({ theme }) => theme.input.icon.hover}; + } + } + svg:first-child { + position: absolute; + top: 8px; + line-height: 0; + z-index: 1; + left: 12px; + right: unset; + height: 16px; + width: 16px; + fill: ${({ theme }) => theme.input.icon.color}; + } + svg:last-child { + position: absolute; + top: 8px; + line-height: 0; + z-index: 1; + left: unset; + right: 12px; + height: 16px; + width: 16px; + } +`; + export const Input = styled.input( - ({ theme, ...props }) => css` - border: 1px ${theme.input.borderColor.normal} solid; + ({ theme: { input }, inputSize, search }) => css` + background-color: ${input.backgroundColor.normal}; + border: 1px ${input.borderColor.normal} solid; border-radius: 4px; - height: ${props.inputSize === 'M' ? '32px' : '40px'}; + color: ${input.color.normal}; + height: ${inputSize && INPUT_SIZES[inputSize] + ? INPUT_SIZES[inputSize] + : '40px'}; width: 100%; - padding-left: ${props.hasLeftIcon ? '36px' : '12px'}; + padding-left: ${search ? '36px' : '12px'}; font-size: 14px; &::placeholder { - color: ${theme.input.color.placeholder.normal}; + color: ${input.color.placeholder.normal}; font-size: 14px; } &:hover { - border-color: ${theme.input.borderColor.hover}; + border-color: ${input.borderColor.hover}; } &:focus { outline: none; - border-color: ${theme.input.borderColor.focus}; + border-color: ${input.borderColor.focus}; &::placeholder { color: transparent; } } &:disabled { - color: ${theme.input.color.disabled}; - border-color: ${theme.input.borderColor.disabled}; + color: ${input.color.disabled}; + border-color: ${input.borderColor.disabled}; + background-color: ${input.backgroundColor.disabled}; cursor: not-allowed; } &:read-only { - color: ${theme.input.color.readOnly}; + color: ${input.color.readOnly}; border: none; - background-color: ${theme.input.backgroundColor.readOnly}; + background-color: ${input.backgroundColor.readOnly}; &:focus { &::placeholder { - color: ${theme.input.color.placeholder.readOnly}; + color: ${input.color.placeholder.readOnly}; } } cursor: not-allowed; @@ -51,3 +92,9 @@ export const FormError = styled.p` color: ${({ theme }) => theme.input.error}; font-size: 12px; `; + +export const InputHint = styled.p` + font-size: 0.85rem; + margin-top: 0.25rem; + color: ${({ theme }) => theme.clusterConfigForm.inputHintText.secondary}; +`; diff --git a/kafka-ui-react-app/src/components/common/Input/Input.tsx b/kafka-ui-react-app/src/components/common/Input/Input.tsx index ecc5eea317e..4d04b730e51 100644 --- a/kafka-ui-react-app/src/components/common/Input/Input.tsx +++ b/kafka-ui-react-app/src/components/common/Input/Input.tsx @@ -1,63 +1,203 @@ import React from 'react'; import { RegisterOptions, useFormContext } from 'react-hook-form'; -import styled from 'styled-components'; +import SearchIcon from 'components/common/Icons/SearchIcon'; +import { ErrorMessage } from '@hookform/error-message'; -import { InputIcon } from './InputIcon.styled'; import * as S from './Input.styled'; +import { InputLabel } from './InputLabel.styled'; export interface InputProps extends React.InputHTMLAttributes, - Omit { + Omit { name?: string; hookFormOptions?: RegisterOptions; - leftIcon?: string; - rightIcon?: string; + search?: boolean; + positiveOnly?: boolean; + withError?: boolean; + label?: React.ReactNode; + hint?: React.ReactNode; + clearIcon?: React.ReactNode; + + // Some may only accept integer, like `Number of Partitions` + // some may accept decimal + integerOnly?: boolean; +} + +function inputNumberCheck( + key: string, + positiveOnly: boolean, + integerOnly: boolean, + getValues: (name: string) => string, + componentName: string +) { + let isValid = true; + if (!((key >= '0' && key <= '9') || key === '-' || key === '.')) { + // If not a valid digit char. + isValid = false; + } else { + // If there is any restriction. + if (positiveOnly) { + isValid = !(key === '-'); + } + if (isValid && integerOnly) { + isValid = !(key === '.'); + } + + // Check invalid format + const value = getValues(componentName); + + if (isValid && (key === '-' || key === '.')) { + if (!positiveOnly) { + if (key === '-') { + if (value !== '') { + // '-' should not appear anywhere except the start of the string + isValid = false; + } + } + } + if (!integerOnly) { + if (key === '.') { + if (value === '' || value.indexOf('.') !== -1) { + // '.' should not appear at the start of the string or appear twice + isValid = false; + } + } + } + } + } + return isValid; } -const Input: React.FC = ({ - className, - name, - hookFormOptions, - leftIcon, - rightIcon, - inputSize = 'L', - ...rest -}) => { +function pasteNumberCheck( + text: string, + positiveOnly: boolean, + integerOnly: boolean +) { + let value: string; + value = text; + let sign = ''; + if (!positiveOnly) { + if (value.charAt(0) === '-') { + sign = '-'; + } + } + if (integerOnly) { + value = value.replace(/\D/g, ''); + } else { + value = value.replace(/[^\d.]/g, ''); + if (value.indexOf('.') !== value.lastIndexOf('.')) { + const strs = value.split('.'); + value = ''; + for (let i = 0; i < strs.length; i += 1) { + value += strs[i]; + if (i === 0) { + value += '.'; + } + } + } + } + value = sign + value; + return value; +} + +const Input = React.forwardRef((props, ref) => { + const { + name, + hookFormOptions, + search, + inputSize = 'L', + type, + positiveOnly, + integerOnly, + withError = false, + label, + hint, + clearIcon, + ...rest + } = props; + const methods = useFormContext(); + + const fieldId = React.useId(); + + const isHookFormField = !!name && !!methods.register; + + const keyPressEventHandler = ( + event: React.KeyboardEvent + ) => { + const { key } = event; + if (type === 'number') { + // Manually prevent input of non-digit and non-minus for all number inputs + // and prevent input of negative numbers for positiveOnly inputs + if ( + !inputNumberCheck( + key, + typeof positiveOnly === 'boolean' ? positiveOnly : false, + typeof integerOnly === 'boolean' ? integerOnly : false, + methods.getValues, + typeof name === 'string' ? name : '' + ) + ) { + event.preventDefault(); + } + } + }; + const pasteEventHandler = (event: React.ClipboardEvent) => { + if (type === 'number') { + const { clipboardData } = event; + // The 'clipboardData' does not have key 'Text', but has key 'text' instead. + const text = clipboardData.getData('text'); + // Check the format of pasted text. + const value = pasteNumberCheck( + text, + typeof positiveOnly === 'boolean' ? positiveOnly : false, + typeof integerOnly === 'boolean' ? integerOnly : false + ); + // if paste value contains non-numeric characters or + // negative for positiveOnly fields then prevent paste + if (value !== text) { + event.preventDefault(); + + // for react-hook-form fields only set transformed value + if (isHookFormField) { + methods.setValue(name, value); + } + } + } + }; + + let inputOptions = { ...rest }; + if (isHookFormField) { + // extend input options with react-hook-form options + // if the field is a part of react-hook-form form + inputOptions = { ...rest, ...methods.register(name, hookFormOptions) }; + } return ( -
    - {leftIcon && ( - - )} - {name ? ( - - ) : ( +
    + {label && {label}} + + {search && } - )} - {rightIcon && ( - - )} + {clearIcon} + + {withError && isHookFormField && ( + + + + )} + {hint && {hint}} +
    ); -}; - -const InputWrapper = styled(Input)` - position: relative; -`; +}); -export default InputWrapper; +export default Input; diff --git a/kafka-ui-react-app/src/components/common/Input/InputIcon.styled.ts b/kafka-ui-react-app/src/components/common/Input/InputIcon.styled.ts deleted file mode 100644 index 470bb330dd2..00000000000 --- a/kafka-ui-react-app/src/components/common/Input/InputIcon.styled.ts +++ /dev/null @@ -1,19 +0,0 @@ -import styled from 'styled-components'; - -interface Props { - className: string; - position: 'left' | 'right'; - inputSize: 'M' | 'L'; -} - -export const InputIcon = styled.i` - position: absolute; - top: 50%; - line-height: 0; - z-index: 1; - left: ${(props) => (props.position === 'left' ? '12px' : 'unset')}; - right: ${(props) => (props.position === 'right' ? '15px' : 'unset')}; - height: 11px; - width: 11px; - color: ${({ theme }) => theme.input.icon.color}; -`; diff --git a/kafka-ui-react-app/src/components/common/Input/InputLabel.styled.ts b/kafka-ui-react-app/src/components/common/Input/InputLabel.styled.ts index 392145f3703..a33fb1f7633 100644 --- a/kafka-ui-react-app/src/components/common/Input/InputLabel.styled.ts +++ b/kafka-ui-react-app/src/components/common/Input/InputLabel.styled.ts @@ -5,4 +5,9 @@ export const InputLabel = styled.label` font-size: 12px; line-height: 20px; color: ${({ theme }) => theme.input.label.color}; + input[type='checkbox'] { + display: inline-block; + margin-right: 8px; + vertical-align: text-top; + } `; diff --git a/kafka-ui-react-app/src/components/common/Input/__tests__/Input.spec.tsx b/kafka-ui-react-app/src/components/common/Input/__tests__/Input.spec.tsx index e831baf19a3..0254196965d 100644 --- a/kafka-ui-react-app/src/components/common/Input/__tests__/Input.spec.tsx +++ b/kafka-ui-react-app/src/components/common/Input/__tests__/Input.spec.tsx @@ -2,6 +2,10 @@ import Input, { InputProps } from 'components/common/Input/Input'; import React from 'react'; import { screen } from '@testing-library/react'; import { render } from 'lib/testHelpers'; +import userEvent from '@testing-library/user-event'; + +// Mock useFormContext +let component: HTMLInputElement; const setupWrapper = (props?: Partial) => ( @@ -9,13 +13,167 @@ const setupWrapper = (props?: Partial) => ( jest.mock('react-hook-form', () => ({ useFormContext: () => ({ register: jest.fn(), + + // Mock methods.getValues and methods.setValue + getValues: jest.fn(() => { + return component.value; + }), + setValue: jest.fn((key, val) => { + component.value = val; + }), }), })); + describe('Custom Input', () => { describe('with no icons', () => { + const getInput = () => screen.getByRole('textbox'); + it('to be in the document', () => { render(setupWrapper()); - expect(screen.getByRole('textbox')).toBeInTheDocument(); + expect(getInput()).toBeInTheDocument(); + }); + }); + describe('number', () => { + const getInput = () => screen.getByRole('spinbutton'); + + describe('input', () => { + it('allows user to type numbers only', async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.type(input, 'abc131'); + expect(input).toHaveValue(131); + }); + + it('allows user to type negative values', async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.type(input, '-2'); + expect(input).toHaveValue(-2); + }); + + it('allows user to type positive values only', async () => { + render(setupWrapper({ type: 'number', positiveOnly: true })); + const input = getInput(); + component = input; + await userEvent.type(input, '-2'); + expect(input).toHaveValue(2); + }); + + it('allows user to type decimal', async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.type(input, '2.3'); + expect(input).toHaveValue(2.3); + }); + + it('allows user to type integer only', async () => { + render(setupWrapper({ type: 'number', integerOnly: true })); + const input = getInput(); + component = input; + await userEvent.type(input, '2.3'); + expect(input).toHaveValue(23); + }); + + it("not allow '-' appear at any position of the string except the start", async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.type(input, '2-3'); + expect(input).toHaveValue(23); + }); + + it("not allow '.' appear at the start of the string", async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.type(input, '.33'); + expect(input).toHaveValue(33); + }); + + it("not allow '.' appear twice in the string", async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.type(input, '3.3.3'); + expect(input).toHaveValue(3.33); + }); + }); + + describe('paste', () => { + it('allows user to paste numbers only', async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.click(input); + await userEvent.paste('abc131'); + expect(input).toHaveValue(131); + }); + + it('allows user to paste negative values', async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.click(input); + await userEvent.paste('-2'); + expect(input).toHaveValue(-2); + }); + + it('allows user to paste positive values only', async () => { + render(setupWrapper({ type: 'number', positiveOnly: true })); + const input = getInput(); + component = input; + await userEvent.click(input); + await userEvent.paste('-2'); + expect(input).toHaveValue(2); + }); + + it('allows user to paste decimal', async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.click(input); + await userEvent.paste('2.3'); + expect(input).toHaveValue(2.3); + }); + + it('allows user to paste integer only', async () => { + render(setupWrapper({ type: 'number', integerOnly: true })); + const input = getInput(); + component = input; + await userEvent.click(input); + await userEvent.paste('2.3'); + expect(input).toHaveValue(23); + }); + + it("not allow '-' appear at any position of the pasted string except the start", async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.click(input); + await userEvent.paste('2-3'); + expect(input).toHaveValue(23); + }); + + it("not allow '.' appear at the start of the pasted string", async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.click(input); + await userEvent.paste('.33'); + expect(input).toHaveValue(0.33); + }); + + it("not allow '.' appear twice in the pasted string", async () => { + render(setupWrapper({ type: 'number' })); + const input = getInput(); + component = input; + await userEvent.click(input); + await userEvent.paste('3.3.3'); + expect(input).toHaveValue(3.33); + }); }); }); }); diff --git a/kafka-ui-react-app/src/components/common/Metrics/Indicator.tsx b/kafka-ui-react-app/src/components/common/Metrics/Indicator.tsx index 7be70696604..be691322d6c 100644 --- a/kafka-ui-react-app/src/components/common/Metrics/Indicator.tsx +++ b/kafka-ui-react-app/src/components/common/Metrics/Indicator.tsx @@ -1,5 +1,5 @@ import React, { PropsWithChildren } from 'react'; -import { AlertType } from 'redux/interfaces'; +import SpinnerIcon from 'components/common/Icons/SpinnerIcon'; import * as S from './Metrics.styled'; @@ -8,7 +8,7 @@ export interface Props { isAlert?: boolean; label: React.ReactNode; title?: string; - alertType?: AlertType; + alertType?: 'success' | 'error' | 'warning' | 'info'; } const Indicator: React.FC> = ({ @@ -29,9 +29,7 @@ const Indicator: React.FC> = ({ )} - - {fetching ? : children} - + {fetching ? : children}
    ); diff --git a/kafka-ui-react-app/src/components/common/Metrics/Metrics.styled.tsx b/kafka-ui-react-app/src/components/common/Metrics/Metrics.styled.tsx index 9924c5ba13c..b9aae50998a 100644 --- a/kafka-ui-react-app/src/components/common/Metrics/Metrics.styled.tsx +++ b/kafka-ui-react-app/src/components/common/Metrics/Metrics.styled.tsx @@ -1,5 +1,4 @@ import styled, { css } from 'styled-components'; -import { AlertType } from 'redux/interfaces'; export const Wrapper = styled.div` padding: 1.5rem 1rem; @@ -11,7 +10,7 @@ export const Wrapper = styled.div` `; export const IndicatorWrapper = styled.div` - background-color: ${({ theme }) => theme.metrics.indicator.backgroundColor}; + background-color: ${({ theme }) => theme.default.backgroundColor}; height: 68px; width: fit-content; min-width: 150px; @@ -22,6 +21,7 @@ export const IndicatorWrapper = styled.div` padding: 12px 16px; box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.08); flex-grow: 1; + color: ${({ theme }) => theme.default.color.normal}; `; export const IndicatorTitle = styled.div` @@ -40,12 +40,14 @@ export const IndicatorsWrapper = styled.div` border-radius: 8px; overflow: auto; box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.08); + color: ${({ theme }) => theme.metrics.wrapper}; `; export const SectionTitle = styled.h5` font-weight: 500; - margin: 0 0 0.5rem 0; + margin: 0 0 0.5rem 16px; font-size: 100%; + color: ${({ theme }) => theme.metrics.sectionTitle}; `; export const LightText = styled.span` @@ -75,7 +77,7 @@ export const CircularAlert = styled.circle.attrs({ cy: 2, r: 2, })<{ - $type: AlertType; + $type: 'error' | 'success' | 'warning' | 'info'; }>( ({ theme, $type }) => css` fill: ${theme.circularAlert.color[$type]}; diff --git a/kafka-ui-react-app/src/components/common/Metrics/Section.tsx b/kafka-ui-react-app/src/components/common/Metrics/Section.tsx index 9cc05b8bc06..64d5a2a76bb 100644 --- a/kafka-ui-react-app/src/components/common/Metrics/Section.tsx +++ b/kafka-ui-react-app/src/components/common/Metrics/Section.tsx @@ -7,7 +7,7 @@ interface Props { } const Section: React.FC> = ({ title, children }) => ( -
    +
    {title && {title}} {children}
    diff --git a/kafka-ui-react-app/src/components/common/Metrics/__tests__/Indicator.spec.tsx b/kafka-ui-react-app/src/components/common/Metrics/__tests__/Indicator.spec.tsx index 873fdc1e979..ba4d5fedcfd 100644 --- a/kafka-ui-react-app/src/components/common/Metrics/__tests__/Indicator.spec.tsx +++ b/kafka-ui-react-app/src/components/common/Metrics/__tests__/Indicator.spec.tsx @@ -3,7 +3,7 @@ import { Indicator } from 'components/common/Metrics'; import { screen } from '@testing-library/react'; import { render } from 'lib/testHelpers'; import { Props } from 'components/common/Metrics/Indicator'; -import theme from 'theme/theme'; +import { theme } from 'theme/theme'; const title = 'Test Title'; const label = 'Test Label'; diff --git a/kafka-ui-react-app/src/components/common/Metrics/__tests__/Section.spec.tsx b/kafka-ui-react-app/src/components/common/Metrics/__tests__/Section.spec.tsx index fd1e13aa722..fb173fa116f 100644 --- a/kafka-ui-react-app/src/components/common/Metrics/__tests__/Section.spec.tsx +++ b/kafka-ui-react-app/src/components/common/Metrics/__tests__/Section.spec.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Section } from 'components/common/Metrics'; -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; +import { render } from 'lib/testHelpers'; const child = 'Child'; const title = 'Test Title'; diff --git a/kafka-ui-react-app/src/components/common/MultiSelect/MultiSelect.styled.ts b/kafka-ui-react-app/src/components/common/MultiSelect/MultiSelect.styled.ts index 0052a99411b..b05bb6d0350 100644 --- a/kafka-ui-react-app/src/components/common/MultiSelect/MultiSelect.styled.ts +++ b/kafka-ui-react-app/src/components/common/MultiSelect/MultiSelect.styled.ts @@ -1,21 +1,56 @@ import styled from 'styled-components'; -import ReactMultiSelect from 'react-multi-select-component'; +import { MultiSelect as ReactMultiSelect } from 'react-multi-select-component'; -const MultiSelect = styled(ReactMultiSelect)<{ minWidth?: string }>` +const MultiSelect = styled(ReactMultiSelect)<{ + minWidth?: string; + height?: string; +}>` min-width: ${({ minWidth }) => minWidth || '200px;'}; - height: 32px; + height: ${({ height }) => height ?? '32px'}; font-size: 14px; + .search input { + color: ${({ theme }) => theme.input.color.normal}; + background-color: ${(props) => + props.theme.input.backgroundColor.normal} !important; + } + .select-item { + color: ${({ theme }) => theme.select.color.normal}; + background-color: ${({ theme }) => + theme.select.backgroundColor.normal} !important; + + &:active { + background-color: ${({ theme }) => + theme.select.backgroundColor.active} !important; + } + } + .select-item.selected { + background-color: ${({ theme }) => + theme.select.backgroundColor.active} !important; + } + .options li { + background-color: ${({ theme }) => + theme.select.backgroundColor.normal} !important; + } & > .dropdown-container { - height: 32px; + background-color: ${({ theme }) => + theme.input.backgroundColor.normal} !important; + border-color: ${({ theme }) => theme.select.borderColor.normal} !important; + &:hover { + border-color: ${({ theme }) => theme.select.borderColor.hover} !important; + } + + height: ${({ height }) => height ?? '32px'}; * { cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; } & > .dropdown-heading { - height: 32px; + height: ${({ height }) => height ?? '32px'}; color: ${({ disabled, theme }) => - disabled ? theme.select.color.disabled : theme.select.color.active}; + disabled + ? theme.select.color.disabled + : theme.select.color.active} !important; & > .clear-selected-button { display: none; } diff --git a/kafka-ui-react-app/src/components/common/MultiSelect/__test__/MultiSelect.styled.spec.tsx b/kafka-ui-react-app/src/components/common/MultiSelect/__test__/MultiSelect.styled.spec.tsx deleted file mode 100644 index 16b30e042d3..00000000000 --- a/kafka-ui-react-app/src/components/common/MultiSelect/__test__/MultiSelect.styled.spec.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import { render } from 'lib/testHelpers'; -import MultiSelect from 'components/common/MultiSelect/MultiSelect.styled'; -import { ISelectProps } from 'react-multi-select-component/dist/lib/interfaces'; - -const Option1 = { value: 1, label: 'option 1' }; -const Option2 = { value: 2, label: 'option 2' }; - -interface IMultiSelectProps extends ISelectProps { - minWidth?: string; -} - -const DefaultProps: IMultiSelectProps = { - options: [Option1, Option2], - labelledBy: 'multi-select', - value: [Option1, Option2], -}; - -describe('MultiSelect.Styled', () => { - const setUpComponent = (props: IMultiSelectProps = DefaultProps) => { - const { container } = render(); - const multiSelect = container.firstChild; - const dropdownContainer = multiSelect?.firstChild?.firstChild; - - return { container, multiSelect, dropdownContainer }; - }; - - it('should have 200px minWidth by default', () => { - const { container } = setUpComponent(); - const multiSelect = container.firstChild; - - expect(multiSelect).toHaveStyle('min-width: 200px'); - }); - - it('should have the provided minWidth in styles', () => { - const minWidth = '400px'; - const { container } = setUpComponent({ ...DefaultProps, minWidth }); - const multiSelect = container.firstChild; - - expect(multiSelect).toHaveStyle(`min-width: ${minWidth}`); - }); - - describe('when not disabled', () => { - it('should have cursor pointer', () => { - const { dropdownContainer } = setUpComponent(); - - expect(dropdownContainer).toHaveStyle(`cursor: pointer`); - }); - }); - - describe('when disabled', () => { - it('should have cursor not-allowed', () => { - const { dropdownContainer } = setUpComponent({ - ...DefaultProps, - disabled: true, - }); - - expect(dropdownContainer).toHaveStyle(`cursor: not-allowed`); - }); - }); -}); diff --git a/kafka-ui-react-app/src/components/common/Navigation/Navbar.styled.ts b/kafka-ui-react-app/src/components/common/Navigation/Navbar.styled.ts index a8a52413b2b..113047bd477 100644 --- a/kafka-ui-react-app/src/components/common/Navigation/Navbar.styled.ts +++ b/kafka-ui-react-app/src/components/common/Navigation/Navbar.styled.ts @@ -3,26 +3,32 @@ import styled from 'styled-components'; const Navbar = styled.nav` display: flex; border-bottom: 1px ${({ theme }) => theme.primaryTab.borderColor.nav} solid; + height: ${({ theme }) => theme.primaryTab.height}; & a { height: 40px; - width: 96px; + min-width: 96px; + padding: 0 16px; display: flex; justify-content: center; align-items: center; font-weight: 500; font-size: 14px; - color: ${(props) => props.theme.primaryTab.color.normal}; - border-bottom: 1px ${(props) => props.theme.primaryTab.borderColor.normal} - solid; + white-space: nowrap; + color: ${({ theme }) => theme.primaryTab.color.normal}; + border-bottom: 1px ${({ theme }) => theme.default.transparentColor} solid; &.is-active { - border-bottom: 1px ${(props) => props.theme.primaryTab.borderColor.active} + border-bottom: 1px ${({ theme }) => theme.primaryTab.borderColor.active} solid; - color: ${(props) => props.theme.primaryTab.color.active}; + color: ${({ theme }) => theme.primaryTab.color.active}; } - &:hover:not(.is-active) { - border-bottom: 1px ${(props) => props.theme.primaryTab.borderColor.hover} - solid; - color: ${(props) => props.theme.primaryTab.color.hover}; + &.is-disabled { + color: ${(props) => props.theme.primaryTab.color.disabled}; + border-bottom: 1px ${({ theme }) => theme.default.transparentColor}; + cursor: not-allowed; + } + &:hover:not(.is-active, .is-disabled) { + border-bottom: 1px ${({ theme }) => theme.default.transparentColor} solid; + color: ${({ theme }) => theme.primaryTab.color.hover}; } } `; diff --git a/kafka-ui-react-app/src/components/common/NewTable/ColoredCell.tsx b/kafka-ui-react-app/src/components/common/NewTable/ColoredCell.tsx new file mode 100644 index 00000000000..df8ab2d6a8d --- /dev/null +++ b/kafka-ui-react-app/src/components/common/NewTable/ColoredCell.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import styled from 'styled-components'; + +interface CellProps { + isWarning?: boolean; + isAttention?: boolean; +} + +interface ColoredCellProps { + value: number | string; + warn?: boolean; + attention?: boolean; +} + +const Cell = styled.div` + color: ${(props) => { + if (props.isAttention) { + return props.theme.table.colored.color.attention; + } + + if (props.isWarning) { + return props.theme.table.colored.color.warning; + } + + return 'inherit'; + }}; +`; + +const ColoredCell: React.FC = ({ + value, + warn, + attention, +}) => { + return ( + + {value} + + ); +}; + +export default ColoredCell; diff --git a/kafka-ui-react-app/src/components/common/NewTable/ExpanderCell.tsx b/kafka-ui-react-app/src/components/common/NewTable/ExpanderCell.tsx new file mode 100644 index 00000000000..d5b78c38399 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/NewTable/ExpanderCell.tsx @@ -0,0 +1,36 @@ +import { CellContext } from '@tanstack/react-table'; +import React from 'react'; + +import * as S from './Table.styled'; + +const ExpanderCell: React.FC> = ({ row }) => { + return ( + + {row.getIsExpanded() ? ( + + ) : ( + + )} + + ); +}; + +export default ExpanderCell; diff --git a/kafka-ui-react-app/src/components/common/NewTable/LinkCell.tsx b/kafka-ui-react-app/src/components/common/NewTable/LinkCell.tsx new file mode 100644 index 00000000000..e400fa0b9c3 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/NewTable/LinkCell.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { NavLink } from 'react-router-dom'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const LinkCell = ({ value, to = '' }: any) => { + const handleClick: React.MouseEventHandler = (e) => e.stopPropagation(); + return ( + + {value} + + ); +}; + +export default LinkCell; diff --git a/kafka-ui-react-app/src/components/common/NewTable/SelectRowCell.tsx b/kafka-ui-react-app/src/components/common/NewTable/SelectRowCell.tsx new file mode 100644 index 00000000000..5c212e1d561 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/NewTable/SelectRowCell.tsx @@ -0,0 +1,14 @@ +import { CellContext } from '@tanstack/react-table'; +import React from 'react'; +import IndeterminateCheckbox from 'components/common/IndeterminateCheckbox/IndeterminateCheckbox'; + +const SelectRowCell: React.FC> = ({ row }) => ( + +); + +export default SelectRowCell; diff --git a/kafka-ui-react-app/src/components/common/NewTable/SelectRowHeader.tsx b/kafka-ui-react-app/src/components/common/NewTable/SelectRowHeader.tsx new file mode 100644 index 00000000000..d33282f2838 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/NewTable/SelectRowHeader.tsx @@ -0,0 +1,15 @@ +import { HeaderContext } from '@tanstack/react-table'; +import React from 'react'; +import IndeterminateCheckbox from 'components/common/IndeterminateCheckbox/IndeterminateCheckbox'; + +const SelectRowHeader: React.FC> = ({ + table, +}) => ( + +); + +export default SelectRowHeader; diff --git a/kafka-ui-react-app/src/components/common/NewTable/SizeCell.tsx b/kafka-ui-react-app/src/components/common/NewTable/SizeCell.tsx new file mode 100644 index 00000000000..7a230be8121 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/NewTable/SizeCell.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { CellContext } from '@tanstack/react-table'; +import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AsAny = any; + +const SizeCell: React.FC< + CellContext & { renderSegments?: boolean; precision?: number } +> = ({ getValue, row, renderSegments = false, precision = 0 }) => ( + <> + ()} precision={precision} /> + {renderSegments ? `, ${row?.original.count} segment(s)` : null} + +); + +export default SizeCell; diff --git a/kafka-ui-react-app/src/components/common/NewTable/Table.styled.ts b/kafka-ui-react-app/src/components/common/NewTable/Table.styled.ts new file mode 100644 index 00000000000..5db0ba80683 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/NewTable/Table.styled.ts @@ -0,0 +1,237 @@ +import styled, { css } from 'styled-components'; + +export const ExpaderButton = styled.svg<{ + $disabled: boolean; + getIsExpanded: boolean; +}>( + ({ theme: { table }, $disabled, getIsExpanded }) => css` + & > path { + fill: ${table.expander[ + ($disabled && 'disabled') || (getIsExpanded && 'active') || 'normal' + ]}; + } + &:hover > path { + fill: ${table.expander[$disabled ? 'disabled' : 'hover']}; + } + &:active > path { + fill: ${table.expander[$disabled ? 'disabled' : 'active']}; + } + ` +); + +interface ThProps { + sortable?: boolean; + sortOrder?: 'desc' | 'asc' | false; + expander?: boolean; +} + +const sortableMixin = (normalColor: string, hoverColor: string) => ` + cursor: pointer; + padding-left: 14px; + position: relative; + + &::before, + &::after { + border: 4px solid transparent; + content: ''; + display: block; + height: 0; + left: 0px; + top: 50%; + position: absolute; + } + &::before { + border-bottom-color: ${normalColor}; + margin-top: -9px; + } + &::after { + border-top-color: ${normalColor}; + margin-top: 1px; + } + &:hover { + color: ${hoverColor}; + } +`; + +const ASCMixin = (color: string) => ` + color: ${color}; + &:before { + border-bottom-color: ${color}; + } + &:after { + border-top-color: rgba(0, 0, 0, 0.1); + } +`; +const DESCMixin = (color: string) => ` + color: ${color}; + &:before { + border-bottom-color: rgba(0, 0, 0, 0.1); + } + &:after { + border-top-color: ${color}; + } +`; + +export const Th = styled.th( + ({ + theme: { + table: { th }, + }, + sortable, + sortOrder, + expander, + }) => ` + padding: 8px 0 8px 24px; + border-bottom-width: 1px; + vertical-align: middle; + text-align: left; + font-family: Inter, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; + letter-spacing: 0em; + text-align: left; + background: ${th.backgroundColor.normal}; + width: ${expander ? '5px' : 'auto'}; + white-space: nowrap; + + & > div { + cursor: default; + color: ${th.color.normal}; + ${sortable ? sortableMixin(th.color.sortable, th.color.hover) : ''} + ${sortable && sortOrder === 'asc' && ASCMixin(th.color.active)} + ${sortable && sortOrder === 'desc' && DESCMixin(th.color.active)} + } +` +); + +interface RowProps { + clickable?: boolean; + expanded?: boolean; +} + +export const Row = styled.tr( + ({ theme: { table }, expanded, clickable }) => ` + cursor: ${clickable ? 'pointer' : 'default'}; + background-color: ${table.tr.backgroundColor[expanded ? 'hover' : 'normal']}; + &:hover { + background-color: ${table.tr.backgroundColor.hover}; + } +` +); + +export const ExpandedRowInfo = styled.div` + background-color: ${({ theme }) => theme.table.tr.backgroundColor.normal}; + padding: 24px; + border-radius: 8px; + margin: 0 8px 8px 0; +`; + +export const Nowrap = styled.div` + white-space: nowrap; +`; + +export const TableActionsBar = styled.div` + padding: 8px; + background-color: ${({ theme }) => theme.table.actionBar.backgroundColor}; + margin: 16px 0; + display: flex; + gap: 8px; +`; + +export const Table = styled.table( + ({ theme: { table } }) => ` + width: 100%; + + td { + border-top: 1px ${table.td.borderTop} solid; + font-size: 14px; + font-weight: 400; + padding: 8px 8px 8px 24px; + color: ${table.td.color.normal}; + vertical-align: middle; + word-wrap: break-word; + + & a { + color: ${table.td.color.normal}; + font-weight: 500; + max-width: 450px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; + + &:hover { + color: ${table.link.color.hover}; + } + + &:active { + color: ${table.link.color.active}; + } + &:button { + color: ${table.link.color.active}; + } + + } + } +` +); + +export const EmptyTableMessageCell = styled.td` + padding: 16px; + text-align: center; +`; + +export const Pagination = styled.div` + display: flex; + justify-content: space-between; + padding: 16px; + line-height: 32px; +`; + +export const Pages = styled.div` + display: flex; + justify-content: left; + white-space: nowrap; + flex-wrap: nowrap; + gap: 8px; +`; + +export const GoToPage = styled.label` + display: flex; + flex-wrap: nowrap; + gap: 8px; + margin-left: 8px; + color: ${({ theme }) => theme.table.pagination.info}; +`; + +export const PageInfo = styled.div` + display: flex; + justify-content: right; + gap: 8px; + font-size: 14px; + flex-wrap: nowrap; + white-space: nowrap; + margin-left: 16px; + color: ${({ theme }) => theme.table.pagination.info}; +`; + +export const Ellipsis = styled.div` + max-width: 300px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; +`; + +export const TableWrapper = styled.div<{ $disabled: boolean }>( + ({ $disabled }) => css` + overflow-x: auto; + ${$disabled && + css` + pointer-events: none; + opacity: 0.5; + `} + ` +); diff --git a/kafka-ui-react-app/src/components/common/NewTable/Table.tsx b/kafka-ui-react-app/src/components/common/NewTable/Table.tsx new file mode 100644 index 00000000000..4a3bf3030ba --- /dev/null +++ b/kafka-ui-react-app/src/components/common/NewTable/Table.tsx @@ -0,0 +1,396 @@ +import React from 'react'; +import { + flexRender, + getCoreRowModel, + getExpandedRowModel, + getSortedRowModel, + useReactTable, + getPaginationRowModel, +} from '@tanstack/react-table'; +import type { + Row, + SortingState, + OnChangeFn, + PaginationState, + ColumnDef, +} from '@tanstack/react-table'; +import { useSearchParams, useLocation } from 'react-router-dom'; +import { PER_PAGE } from 'lib/constants'; +import { Button } from 'components/common/Button/Button'; +import Input from 'components/common/Input/Input'; + +import * as S from './Table.styled'; +import updateSortingState from './utils/updateSortingState'; +import updatePaginationState from './utils/updatePaginationState'; +import ExpanderCell from './ExpanderCell'; +import SelectRowCell from './SelectRowCell'; +import SelectRowHeader from './SelectRowHeader'; + +export interface TableProps { + data: TData[]; + pageCount?: number; + columns: ColumnDef[]; + + // Server-side processing: sorting, pagination + serverSideProcessing?: boolean; + + // Expandeble rows + getRowCanExpand?: (row: Row) => boolean; // Enables the ability to expand row. Use `() => true` when want to expand all rows. + renderSubComponent?: React.FC<{ row: Row }>; // Component to render expanded row. + + // Selectable rows + enableRowSelection?: boolean | ((row: Row) => boolean); // Enables the ability to select row. + batchActionsBar?: React.FC<{ rows: Row[]; resetRowSelection(): void }>; // Component to render batch actions bar for slected rows + + // Sorting. + enableSorting?: boolean; // Enables sorting for table. + + // Placeholder for empty table + emptyMessage?: React.ReactNode; + + disabled?: boolean; + + // Handles row click. Can not be combined with `enableRowSelection` && expandable rows. + onRowClick?: (row: Row) => void; + + onRowHover?: (row: Row) => void; + onMouseLeave?: () => void; +} + +type UpdaterFn = (previousState: T) => T; + +const getPaginationFromSearchParams = (searchParams: URLSearchParams) => { + const page = searchParams.get('page'); + const perPage = searchParams.get('perPage'); + const pageIndex = page ? Number(page) - 1 : 0; + return { + pageIndex, + pageSize: Number(perPage || PER_PAGE), + }; +}; + +const getSortingFromSearchParams = (searchParams: URLSearchParams) => { + const sortBy = searchParams.get('sortBy'); + const sortDirection = searchParams.get('sortDirection'); + if (!sortBy) return []; + return [{ id: sortBy, desc: sortDirection === 'desc' }]; +}; + +/** + * Table component that uses the react-table library to render a table. + * https://tanstack.com/table/v8 + * + * The most important props are: + * - `data`: the data to render in the table + * - `columns`: ColumnsDef. You can finde more info about it on https://tanstack.com/table/v8/docs/guide/column-defs + * - `emptyMessage`: the message to show when there is no data to render + * + * Usecases: + * 1. Sortable table + * - set `enableSorting` property of component to true. It will enable sorting for all columns. + * If you want to disable sorting for some particular columns you can pass + * `enableSorting = false` to the column def. + * - table component stores the sorting state in URLSearchParams. Use `sortBy` and `sortDirection` + * search param to set default sortings. + * - use `id` property of the column def to set the sortBy for server side sorting. + * + * 2. Pagination + * - pagination enabled by default. + * - use `perPage` search param to manage default page size. + * - use `page` search param to manage default page index. + * - use `pageCount` prop to set the total number of pages only in case of server side processing. + * + * 3. Expandable rows + * - use `getRowCanExpand` prop to set a function that returns true if the row can be expanded. + * - use `renderSubComponent` prop to provide a sub component for each expanded row. + * + * 4. Row selection + * - use `enableRowSelection` prop to enable row selection. This prop can be a boolean or + * a function that returns true if the particular row can be selected. + * - use `batchActionsBar` prop to provide a component that will be rendered at the top of the table + * when row selection is enabled. + * + * 5. Server side processing: + * - set `serverSideProcessing` to true + * - set `pageCount` to the total number of pages + * - use URLSearchParams to get the pagination and sorting state from the url for your server side processing. + */ + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const Table: React.FC> = ({ + data, + pageCount, + columns, + getRowCanExpand, + renderSubComponent: SubComponent, + serverSideProcessing = false, + enableSorting = false, + enableRowSelection = false, + batchActionsBar: BatchActionsBar, + emptyMessage, + disabled, + onRowClick, + onRowHover, + onMouseLeave, +}) => { + const [searchParams, setSearchParams] = useSearchParams(); + const location = useLocation(); + const [rowSelection, setRowSelection] = React.useState({}); + const onSortingChange = React.useCallback( + (updater: UpdaterFn) => { + const newState = updateSortingState(updater, searchParams); + setSearchParams(searchParams); + return newState; + }, + [searchParams, location] + ); + const onPaginationChange = React.useCallback( + (updater: UpdaterFn) => { + const newState = updatePaginationState(updater, searchParams); + setSearchParams(searchParams); + setRowSelection({}); + return newState; + }, + [searchParams, location] + ); + + const table = useReactTable({ + data, + pageCount, + columns, + state: { + sorting: getSortingFromSearchParams(searchParams), + pagination: getPaginationFromSearchParams(searchParams), + rowSelection, + }, + getRowId: (originalRow, index) => { + return originalRow.name ? originalRow.name : `${index}`; + }, + onSortingChange: onSortingChange as OnChangeFn, + onPaginationChange: onPaginationChange as OnChangeFn, + onRowSelectionChange: setRowSelection, + getRowCanExpand, + getCoreRowModel: getCoreRowModel(), + getExpandedRowModel: getExpandedRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + manualSorting: serverSideProcessing, + manualPagination: serverSideProcessing, + enableSorting, + autoResetPageIndex: false, + enableRowSelection, + }); + + const handleRowClick = (row: Row) => (e: React.MouseEvent) => { + // If row selection is enabled do not handle row click. + if (enableRowSelection) return undefined; + + // If row can be expanded do not handle row click. + if (row.getCanExpand()) { + e.stopPropagation(); + return row.toggleExpanded(); + } + + if (onRowClick) { + e.stopPropagation(); + return onRowClick(row); + } + + return undefined; + }; + + const handleRowHover = (row: Row) => (e: React.MouseEvent) => { + if (onRowHover) { + e.stopPropagation(); + return onRowHover(row); + } + + return undefined; + }; + + const handleMouseLeave = () => { + if (onMouseLeave) { + onMouseLeave(); + } + }; + + return ( + <> + {BatchActionsBar && ( + + + + )} + + +
    + {table.getHeaderGroups().map((headerGroup) => ( + + {!!enableRowSelection && ( + + {flexRender( + SelectRowHeader, + headerGroup.headers[0].getContext() + )} + + )} + {table.getCanSomeRowsExpand() && ( + + )} + {headerGroup.headers.map((header) => ( + +
    + {flexRender( + header.column.columnDef.header, + header.getContext() + )} +
    +
    + ))} +
    + ))} + + + {table.getRowModel().rows.map((row) => ( + + + {!!enableRowSelection && ( + + )} + {table.getCanSomeRowsExpand() && ( + + )} + {row + .getVisibleCells() + .map(({ id, getContext, column: { columnDef } }) => ( + + ))} + + {row.getIsExpanded() && SubComponent && ( + + + + )} + + ))} + {table.getRowModel().rows.length === 0 && ( + + + {emptyMessage || 'No rows found'} + + + )} + + + + {table.getPageCount() > 1 && ( + + + + + + + + + Go to page: + { + const index = value ? Number(value) - 1 : 0; + table.setPageIndex(index); + }} + /> + + + + + Page {table.getState().pagination.pageIndex + 1} of{' '} + {table.getPageCount()}{' '} + + + + )} + + ); +}; + +export default Table; diff --git a/kafka-ui-react-app/src/components/common/NewTable/TagCell.tsx b/kafka-ui-react-app/src/components/common/NewTable/TagCell.tsx new file mode 100644 index 00000000000..c78d64a2dea --- /dev/null +++ b/kafka-ui-react-app/src/components/common/NewTable/TagCell.tsx @@ -0,0 +1,12 @@ +import { CellContext } from '@tanstack/react-table'; +import React from 'react'; +import getTagColor from 'components/common/Tag/getTagColor'; +import { Tag } from 'components/common/Tag/Tag.styled'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const TagCell: React.FC> = ({ getValue }) => { + const value = getValue(); + return {value}; +}; + +export default TagCell; diff --git a/kafka-ui-react-app/src/components/common/NewTable/TimestampCell.tsx b/kafka-ui-react-app/src/components/common/NewTable/TimestampCell.tsx new file mode 100644 index 00000000000..f0ff3d5f65f --- /dev/null +++ b/kafka-ui-react-app/src/components/common/NewTable/TimestampCell.tsx @@ -0,0 +1,12 @@ +import { CellContext } from '@tanstack/react-table'; +import { formatTimestamp } from 'lib/dateTimeHelpers'; +import React from 'react'; + +import * as S from './Table.styled'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const TimestampCell: React.FC> = ({ getValue }) => ( + {formatTimestamp(getValue())} +); + +export default TimestampCell; diff --git a/kafka-ui-react-app/src/components/common/NewTable/__test__/Table.spec.tsx b/kafka-ui-react-app/src/components/common/NewTable/__test__/Table.spec.tsx new file mode 100644 index 00000000000..e31a88846d5 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/NewTable/__test__/Table.spec.tsx @@ -0,0 +1,369 @@ +import React from 'react'; +import { render, WithRoute } from 'lib/testHelpers'; +import Table, { + TableProps, + TimestampCell, + SizeCell, + LinkCell, + TagCell, +} from 'components/common/NewTable'; +import { screen, waitFor } from '@testing-library/dom'; +import { ColumnDef, Row } from '@tanstack/react-table'; +import userEvent from '@testing-library/user-event'; +import { formatTimestamp } from 'lib/dateTimeHelpers'; +import { ConnectorState, ConsumerGroupState } from 'generated-sources'; + +const mockedUsedNavigate = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockedUsedNavigate, +})); + +// This is needed by ESLint. +jest.mock('react-hook-form', () => ({ + useFormContext: () => ({ + register: jest.fn(), + + // Mock methods.getValues and methods.setValue + getValues: jest.fn(), + setValue: jest.fn(), + }), +})); + +type Datum = (typeof data)[0]; + +const data = [ + { + timestamp: 1660034383725, + text: 'lorem', + selectable: false, + size: 1234, + tag: ConnectorState.RUNNING, + }, + { + timestamp: 1660034399999, + text: 'ipsum', + selectable: true, + size: 3, + tag: ConnectorState.FAILED, + }, + { + timestamp: 1660034399922, + text: 'dolor', + selectable: true, + size: 50000, + tag: ConsumerGroupState.EMPTY, + }, + { + timestamp: 1660034199922, + text: 'sit', + selectable: false, + size: 1_312_323, + tag: 'some_string', + }, +]; + +const columns: ColumnDef[] = [ + { + header: 'DateTime', + accessorKey: 'timestamp', + cell: TimestampCell, + }, + { + header: 'Text', + accessorKey: 'text', + cell: ({ getValue }) => ( + ()}`} + to={encodeURIComponent(`${getValue()}`)} + /> + ), + }, + { + header: 'Size', + accessorKey: 'size', + cell: SizeCell, + }, + { + header: 'Tag', + accessorKey: 'tag', + cell: TagCell, + }, +]; + +const ExpandedRow: React.FC = () =>
    I am expanded row
    ; + +interface Props extends TableProps { + path?: string; +} + +const renderComponent = (props: Partial = {}) => { + render( + +
    + {flexRender( + SelectRowCell, + row.getVisibleCells()[0].getContext() + )} + + {flexRender( + ExpanderCell, + row.getVisibleCells()[0].getContext() + )} + + {flexRender(columnDef.cell, getContext())} + + + + +
    + , + { initialEntries: [props.path || ''] } + ); +}; + +describe('Table', () => { + it('renders table', () => { + renderComponent(); + expect(screen.getByRole('table')).toBeInTheDocument(); + expect(screen.getAllByRole('row').length).toEqual(data.length + 1); + }); + + it('renders empty table', () => { + renderComponent({ data: [] }); + expect(screen.getByRole('table')).toBeInTheDocument(); + expect(screen.getAllByRole('row').length).toEqual(2); + expect(screen.getByText('No rows found')).toBeInTheDocument(); + }); + + it('renders empty table with custom message', () => { + const emptyMessage = 'Super custom message'; + renderComponent({ data: [], emptyMessage }); + expect(screen.getByRole('table')).toBeInTheDocument(); + expect(screen.getAllByRole('row').length).toEqual(2); + expect(screen.getByText(emptyMessage)).toBeInTheDocument(); + }); + + it('renders SizeCell', () => { + renderComponent(); + expect(screen.getByText('1 KB')).toBeInTheDocument(); + expect(screen.getByText('3 Bytes')).toBeInTheDocument(); + expect(screen.getByText('49 KB')).toBeInTheDocument(); + expect(screen.getByText('1 MB')).toBeInTheDocument(); + }); + + it('renders TimestampCell', () => { + renderComponent(); + expect( + screen.getByText(formatTimestamp(data[0].timestamp)) + ).toBeInTheDocument(); + }); + + describe('LinkCell', () => { + it('renders link', () => { + renderComponent(); + expect(screen.getByRole('link', { name: 'lorem' })).toBeInTheDocument(); + }); + + it('link click stops propagation', async () => { + const onRowClick = jest.fn(); + renderComponent({ onRowClick }); + const link = screen.getByRole('link', { name: 'lorem' }); + await userEvent.click(link); + expect(onRowClick).not.toHaveBeenCalled(); + }); + }); + + describe('ExpanderCell', () => { + it('renders button', async () => { + renderComponent({ getRowCanExpand: () => true }); + const btns = screen.getAllByRole('button', { name: 'Expand row' }); + expect(btns.length).toEqual(data.length); + + expect(screen.queryByText('I am expanded row')).not.toBeInTheDocument(); + await userEvent.click(btns[2]); + expect(screen.getByText('I am expanded row')).toBeInTheDocument(); + await userEvent.click(btns[0]); + expect(screen.getAllByText('I am expanded row').length).toEqual(2); + }); + + it('does not render button', () => { + renderComponent({ getRowCanExpand: () => false }); + expect( + screen.queryByRole('button', { name: 'Expand row' }) + ).not.toBeInTheDocument(); + expect(screen.queryByText('I am expanded row')).not.toBeInTheDocument(); + }); + }); + + it('renders TagCell', () => { + renderComponent(); + expect(screen.getByText(data[0].tag)).toBeInTheDocument(); + expect(screen.getByText(data[1].tag)).toBeInTheDocument(); + expect(screen.getByText(data[2].tag)).toBeInTheDocument(); + expect(screen.getByText(data[3].tag)).toBeInTheDocument(); + }); + + describe('Pagination', () => { + it('does not render page buttons', () => { + renderComponent(); + expect( + screen.queryByRole('button', { name: 'Next' }) + ).not.toBeInTheDocument(); + }); + + it('renders page buttons', async () => { + renderComponent({ path: '?perPage=1' }); + // Check it renders header row and only one data row + expect(screen.getAllByRole('row').length).toEqual(2); + expect(screen.getByText('lorem')).toBeInTheDocument(); + + // Check it renders page buttons + const firstBtn = screen.getByRole('button', { name: '⇤' }); + const prevBtn = screen.getByRole('button', { name: '← Previous' }); + const nextBtn = screen.getByRole('button', { name: 'Next →' }); + const lastBtn = screen.getByRole('button', { name: '⇥' }); + + expect(firstBtn).toBeInTheDocument(); + expect(firstBtn).toBeDisabled(); + expect(prevBtn).toBeInTheDocument(); + expect(prevBtn).toBeDisabled(); + expect(nextBtn).toBeInTheDocument(); + expect(nextBtn).toBeEnabled(); + expect(lastBtn).toBeInTheDocument(); + expect(lastBtn).toBeEnabled(); + + await userEvent.click(nextBtn); + expect(screen.getByText('ipsum')).toBeInTheDocument(); + expect(prevBtn).toBeEnabled(); + expect(firstBtn).toBeEnabled(); + + await userEvent.click(lastBtn); + expect(screen.getByText('sit')).toBeInTheDocument(); + expect(lastBtn).toBeDisabled(); + expect(nextBtn).toBeDisabled(); + + await userEvent.click(prevBtn); + expect(screen.getByText('dolor')).toBeInTheDocument(); + + await userEvent.click(firstBtn); + expect(screen.getByText('lorem')).toBeInTheDocument(); + }); + + describe('Go To page', () => { + const getGoToPageInput = () => + screen.getByRole('spinbutton', { name: 'Go to page:' }); + + beforeEach(() => { + renderComponent({ path: '?perPage=1' }); + }); + + it('renders Go To page', () => { + const goToPage = getGoToPageInput(); + expect(goToPage).toBeInTheDocument(); + expect(goToPage).toHaveValue(1); + }); + it('updates page on Go To page change', async () => { + const goToPage = getGoToPageInput(); + await userEvent.clear(goToPage); + await userEvent.type(goToPage, '2'); + expect(goToPage).toHaveValue(2); + expect(screen.getByText('ipsum')).toBeInTheDocument(); + }); + it('does not update page on Go To page change if page is out of range', async () => { + const goToPage = getGoToPageInput(); + await userEvent.type(goToPage, '5'); + expect(goToPage).toHaveValue(15); + expect(screen.getByText('No rows found')).toBeInTheDocument(); + }); + it('does not update page on Go To page change if page is not a number', async () => { + const goToPage = getGoToPageInput(); + await userEvent.type(goToPage, 'abc'); + expect(goToPage).toHaveValue(1); + }); + }); + }); + + describe('Sorting', () => { + it('sort rows', async () => { + await renderComponent({ + path: '/?sortBy=text&&sortDirection=desc', + enableSorting: true, + }); + expect(screen.getAllByRole('row').length).toEqual(data.length + 1); + const th = screen.getByRole('columnheader', { name: 'Text' }); + expect(th).toBeInTheDocument(); + + let rows = screen.getAllByRole('row'); + // Check initial sort order by text column is descending + + expect(rows[4].textContent?.indexOf('dolor')).toBeGreaterThan(-1); + expect(rows[3].textContent?.indexOf('ipsum')).toBeGreaterThan(-1); + expect(rows[2].textContent?.indexOf('lorem')).toBeGreaterThan(-1); + expect(rows[1].textContent?.indexOf('sit')).toBeGreaterThan(-1); + + // Disable sorting by text column + await waitFor(() => userEvent.click(th)); + rows = screen.getAllByRole('row'); + expect(rows[1].textContent?.indexOf('lorem')).toBeGreaterThan(-1); + expect(rows[2].textContent?.indexOf('ipsum')).toBeGreaterThan(-1); + expect(rows[3].textContent?.indexOf('dolor')).toBeGreaterThan(-1); + expect(rows[4].textContent?.indexOf('sit')).toBeGreaterThan(-1); + + // Sort by text column ascending + await waitFor(() => userEvent.click(th)); + rows = screen.getAllByRole('row'); + expect(rows[1].textContent?.indexOf('dolor')).toBeGreaterThan(-1); + expect(rows[2].textContent?.indexOf('ipsum')).toBeGreaterThan(-1); + expect(rows[3].textContent?.indexOf('lorem')).toBeGreaterThan(-1); + expect(rows[4].textContent?.indexOf('sit')).toBeGreaterThan(-1); + }); + }); + + describe('Row Selecting', () => { + beforeEach(() => { + renderComponent({ + enableRowSelection: (row: Row) => row.original.selectable, + batchActionsBar: () =>
    I am Action Bar
    , + }); + }); + it('renders selectable rows', () => { + expect(screen.getAllByRole('row').length).toEqual(data.length + 1); + const checkboxes = screen.getAllByRole('checkbox'); + expect(checkboxes.length).toEqual(data.length + 1); + expect(checkboxes[1]).toBeDisabled(); + expect(checkboxes[2]).toBeEnabled(); + expect(checkboxes[3]).toBeEnabled(); + expect(checkboxes[4]).toBeDisabled(); + }); + + it('renders action bar', async () => { + expect(screen.getAllByRole('row').length).toEqual(data.length + 1); + expect(screen.queryByText('I am Action Bar')).toBeInTheDocument(); + const checkboxes = screen.getAllByRole('checkbox'); + expect(checkboxes.length).toEqual(data.length + 1); + await userEvent.click(checkboxes[2]); + expect(screen.getByText('I am Action Bar')).toBeInTheDocument(); + }); + }); + describe('Clickable Row', () => { + const onRowClick = jest.fn(); + it('handles onRowClick', async () => { + renderComponent({ onRowClick }); + const rows = screen.getAllByRole('row'); + expect(rows.length).toEqual(data.length + 1); + await userEvent.click(rows[1]); + expect(onRowClick).toHaveBeenCalledTimes(1); + }); + it('does nothing unless onRowClick is provided', async () => { + renderComponent(); + const rows = screen.getAllByRole('row'); + expect(rows.length).toEqual(data.length + 1); + await userEvent.click(rows[1]); + }); + it('does not handle onRowClick if enableRowSelection', async () => { + renderComponent({ onRowClick, enableRowSelection: true }); + const rows = screen.getAllByRole('row'); + expect(rows.length).toEqual(data.length + 1); + await userEvent.click(rows[1]); + expect(onRowClick).not.toHaveBeenCalled(); + }); + it('does not handle onRowClick if expandable rows', async () => { + renderComponent({ onRowClick, getRowCanExpand: () => true }); + const rows = screen.getAllByRole('row'); + expect(rows.length).toEqual(data.length + 1); + await userEvent.click(rows[1]); + expect(onRowClick).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/common/NewTable/index.ts b/kafka-ui-react-app/src/components/common/NewTable/index.ts new file mode 100644 index 00000000000..4584db2a564 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/NewTable/index.ts @@ -0,0 +1,11 @@ +import Table, { TableProps } from './Table'; +import TimestampCell from './TimestampCell'; +import SizeCell from './SizeCell'; +import LinkCell from './LinkCell'; +import TagCell from './TagCell'; + +export type { TableProps }; + +export { TimestampCell, SizeCell, LinkCell, TagCell }; + +export default Table; diff --git a/kafka-ui-react-app/src/components/common/NewTable/utils/__test__/updateSortingState.spec.ts b/kafka-ui-react-app/src/components/common/NewTable/utils/__test__/updateSortingState.spec.ts new file mode 100644 index 00000000000..07bd60991bc --- /dev/null +++ b/kafka-ui-react-app/src/components/common/NewTable/utils/__test__/updateSortingState.spec.ts @@ -0,0 +1,34 @@ +import updateSortingState from 'components/common/NewTable/utils/updateSortingState'; +import { SortingState } from '@tanstack/react-table'; +import compact from 'lodash/compact'; + +const updater = (previousState: SortingState): SortingState => { + return compact( + previousState.map(({ id, desc }) => { + if (!id) return null; + return { id, desc: !desc }; + }) + ); +}; + +describe('updateSortingState', () => { + it('should update the sorting state', () => { + const searchParams = new URLSearchParams(); + searchParams.set('sortBy', 'date'); + searchParams.set('sortDirection', 'desc'); + const newState = updateSortingState(updater, searchParams); + expect(searchParams.get('sortBy')).toBe('date'); + expect(searchParams.get('sortDirection')).toBe('asc'); + expect(newState.length).toBe(1); + expect(newState[0].id).toBe('date'); + expect(newState[0].desc).toBe(false); + }); + + it('should update the sorting state', () => { + const searchParams = new URLSearchParams(); + const newState = updateSortingState(updater, searchParams); + expect(searchParams.get('sortBy')).toBeNull(); + expect(searchParams.get('sortDirection')).toBeNull(); + expect(newState.length).toBe(0); + }); +}); diff --git a/kafka-ui-react-app/src/components/common/NewTable/utils/updatePaginationState.ts b/kafka-ui-react-app/src/components/common/NewTable/utils/updatePaginationState.ts new file mode 100644 index 00000000000..8b6bc3dc825 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/NewTable/utils/updatePaginationState.ts @@ -0,0 +1,20 @@ +import { PaginationState } from '@tanstack/react-table'; +import { PER_PAGE } from 'lib/constants'; + +type UpdaterFn = (previousState: T) => T; + +export default ( + updater: UpdaterFn, + searchParams: URLSearchParams +) => { + const page = searchParams.get('page'); + const previousState: PaginationState = { + // Page number starts at 1, but the pageIndex starts at 0 + pageIndex: page ? Number(page) - 1 : 0, + pageSize: Number(searchParams.get('perPage') || PER_PAGE), + }; + const newState = updater(previousState); + searchParams.set('page', String(newState.pageIndex + 1)); + searchParams.set('perPage', newState.pageSize.toString()); + return previousState; +}; diff --git a/kafka-ui-react-app/src/components/common/NewTable/utils/updateSortingState.ts b/kafka-ui-react-app/src/components/common/NewTable/utils/updateSortingState.ts new file mode 100644 index 00000000000..d2aaa3598d8 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/NewTable/utils/updateSortingState.ts @@ -0,0 +1,26 @@ +import { SortingState } from '@tanstack/react-table'; + +type UpdaterFn = (previousState: T) => T; + +export default ( + updater: UpdaterFn, + searchParams: URLSearchParams +) => { + const previousState: SortingState = [ + { + id: searchParams.get('sortBy') || '', + desc: searchParams.get('sortDirection') === 'desc', + }, + ]; + const newState = updater(previousState); + + if (newState.length > 0) { + const { id, desc } = newState[0]; + searchParams.set('sortBy', id); + searchParams.set('sortDirection', desc ? 'desc' : 'asc'); + } else { + searchParams.delete('sortBy'); + searchParams.delete('sortDirection'); + } + return newState; +}; diff --git a/kafka-ui-react-app/src/components/common/PageHeading/PageHeading.styled.ts b/kafka-ui-react-app/src/components/common/PageHeading/PageHeading.styled.ts new file mode 100644 index 00000000000..7010429ecb3 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/PageHeading/PageHeading.styled.ts @@ -0,0 +1,42 @@ +import styled from 'styled-components'; +import { NavLink } from 'react-router-dom'; + +export const Breadcrumbs = styled.div` + display: flex; + align-items: baseline; +`; + +export const BackLink = styled(NavLink)` + color: ${({ theme }) => theme.pageHeading.backLink.color.normal}; + position: relative; + + &:hover { + ${({ theme }) => theme.pageHeading.backLink.color.hover}; + } + + &::after { + content: ''; + position: absolute; + right: -11px; + bottom: 2px; + border-left: 1px solid ${({ theme }) => theme.pageHeading.dividerColor}; + height: 20px; + transform: rotate(14deg); + } +`; + +export const Wrapper = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + + & > div { + display: flex; + gap: 16px; + } + + & > ${Breadcrumbs} { + gap: 20px; + } +`; diff --git a/kafka-ui-react-app/src/components/common/PageHeading/PageHeading.tsx b/kafka-ui-react-app/src/components/common/PageHeading/PageHeading.tsx index 426532ef4cc..ffbdc1a2797 100644 --- a/kafka-ui-react-app/src/components/common/PageHeading/PageHeading.tsx +++ b/kafka-ui-react-app/src/components/common/PageHeading/PageHeading.tsx @@ -1,34 +1,31 @@ -import styled from 'styled-components'; import React, { PropsWithChildren } from 'react'; import Heading from 'components/common/heading/Heading.styled'; -interface Props { +import * as S from './PageHeading.styled'; + +interface PageHeadingProps { text: string; - className?: string; + backTo?: string; + backText?: string; } -const PageHeading: React.FC> = ({ +const PageHeading: React.FC> = ({ text, - className, + backTo, + backText, children, }) => { + const isBackButtonVisible = backTo && backText; + return ( -
    - {text} + + + {isBackButtonVisible && {backText}} + {text} +
    {children}
    -
    + ); }; -export default styled(PageHeading)` - height: 56px; - display: flex; - justify-content: space-between; - align-items: center; - padding: 0px 16px; - - & > div { - display: flex; - gap: 16px; - } -`; +export default PageHeading; diff --git a/kafka-ui-react-app/src/components/common/PageLoader/PageLoader.styled.ts b/kafka-ui-react-app/src/components/common/PageLoader/PageLoader.styled.ts index 87f7a27fd13..f38f21c0b28 100644 --- a/kafka-ui-react-app/src/components/common/PageLoader/PageLoader.styled.ts +++ b/kafka-ui-react-app/src/components/common/PageLoader/PageLoader.styled.ts @@ -1,4 +1,4 @@ -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; export const Wrapper = styled.div` display: flex; @@ -8,23 +8,3 @@ export const Wrapper = styled.div` height: 100%; width: 100%; `; - -export const Spinner = styled.div( - ({ theme }) => css` - border: 10px solid ${theme.pageLoader.borderColor}; - border-bottom: 10px solid ${theme.pageLoader.borderBottomColor}; - border-radius: 50%; - width: 80px; - height: 80px; - animation: spin 1.3s linear infinite; - - @keyframes spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } - } - ` -); diff --git a/kafka-ui-react-app/src/components/common/PageLoader/PageLoader.tsx b/kafka-ui-react-app/src/components/common/PageLoader/PageLoader.tsx index 33348b17ea1..674ab0f0cee 100644 --- a/kafka-ui-react-app/src/components/common/PageLoader/PageLoader.tsx +++ b/kafka-ui-react-app/src/components/common/PageLoader/PageLoader.tsx @@ -1,10 +1,11 @@ import React from 'react'; +import Spinner from 'components/common/Spinner/Spinner'; import * as S from './PageLoader.styled'; const PageLoader: React.FC = () => ( - + ); diff --git a/kafka-ui-react-app/src/components/common/Pagination/PageControl.tsx b/kafka-ui-react-app/src/components/common/Pagination/PageControl.tsx deleted file mode 100644 index 87254a72221..00000000000 --- a/kafka-ui-react-app/src/components/common/Pagination/PageControl.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; - -import { PaginationLink } from './Pagination.styled'; - -export interface PageControlProps { - current: boolean; - url: string; - page: number; -} - -const PageControl: React.FC = ({ current, url, page }) => { - return ( -
  • - - {page} - -
  • - ); -}; - -export default PageControl; diff --git a/kafka-ui-react-app/src/components/common/Pagination/Pagination.styled.ts b/kafka-ui-react-app/src/components/common/Pagination/Pagination.styled.ts deleted file mode 100644 index 5551e311a24..00000000000 --- a/kafka-ui-react-app/src/components/common/Pagination/Pagination.styled.ts +++ /dev/null @@ -1,87 +0,0 @@ -import styled from 'styled-components'; -import { Link } from 'react-router-dom'; -import theme from 'theme/theme'; - -export const Wrapper = styled.nav` - display: flex; - align-items: flex-end; - padding: 25px 16px; - gap: 15px; - - & > ul { - display: flex; - align-items: flex-end; - - & > li:not(:last-child) { - margin-right: 12px; - } - } -`; - -export const PaginationLink = styled(Link)<{ $isCurrent: boolean }>` - display: flex; - justify-content: center; - align-items: center; - - height: 32px; - width: 33px; - - border-radius: 4px; - border: 1px solid - ${({ $isCurrent }) => - $isCurrent - ? theme.pagination.currentPage - : theme.pagination.borderColor.normal}; - background-color: ${({ $isCurrent }) => - $isCurrent - ? theme.pagination.currentPage - : theme.pagination.backgroundColor}; - color: ${theme.pagination.color.normal}; - - &:hover { - border: 1px solid - ${({ $isCurrent }) => - $isCurrent - ? theme.pagination.currentPage - : theme.pagination.borderColor.hover}; - color: ${(props) => props.theme.pagination.color.hover}; - cursor: ${({ $isCurrent }) => ($isCurrent ? 'default' : 'pointer')}; - } -`; - -export const PaginationButton = styled(Link)` - display: flex; - align-items: center; - padding: 6px 12px; - height: 32px; - border: 1px solid ${theme.pagination.borderColor.normal}; - border-radius: 4px; - color: ${theme.pagination.color.normal}; - - &:hover { - border: 1px solid ${theme.pagination.borderColor.hover}; - color: ${theme.pagination.color.hover}; - cursor: pointer; - } - &:active { - border: 1px solid ${theme.pagination.borderColor.active}; - color: ${theme.pagination.color.active}; - } - &:disabled { - border: 1px solid ${theme.pagination.borderColor.disabled}; - color: ${theme.pagination.color.disabled}; - cursor: not-allowed; - } -`; - -export const DisabledButton = styled.button` - display: flex; - align-items: center; - padding: 6px 12px; - height: 32px; - border: 1px solid ${theme.pagination.borderColor.disabled}; - background-color: ${theme.pagination.backgroundColor}; - border-radius: 4px; - font-size: 16px; - color: ${theme.pagination.color.disabled}; -`; diff --git a/kafka-ui-react-app/src/components/common/Pagination/Pagination.tsx b/kafka-ui-react-app/src/components/common/Pagination/Pagination.tsx deleted file mode 100644 index 7da12b479de..00000000000 --- a/kafka-ui-react-app/src/components/common/Pagination/Pagination.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { PER_PAGE } from 'lib/constants'; -import usePagination from 'lib/hooks/usePagination'; -import { range } from 'lodash'; -import React from 'react'; -import PageControl from 'components/common/Pagination/PageControl'; -import useSearch from 'lib/hooks/useSearch'; - -import * as S from './Pagination.styled'; - -export interface PaginationProps { - totalPages: number; -} - -const NEIGHBOURS = 2; - -const Pagination: React.FC = ({ totalPages }) => { - const { page, perPage, pathname } = usePagination(); - const [searchText] = useSearch(); - - const currentPage = page || 1; - const currentPerPage = perPage || PER_PAGE; - - const searchParam = searchText ? `&q=${searchText}` : ''; - const getPath = (newPage: number) => - `${pathname}?page=${Math.max( - newPage, - 1 - )}&perPage=${currentPerPage}${searchParam}`; - - const pages = React.useMemo(() => { - // Total visible numbers: neighbours, current, first & last - const totalNumbers = NEIGHBOURS * 2 + 3; - // totalNumbers + `...`*2 - const totalBlocks = totalNumbers + 2; - - if (totalPages <= totalBlocks) { - return range(1, totalPages + 1); - } - - const startPage = Math.max( - 2, - Math.min(currentPage - NEIGHBOURS, totalPages) - ); - const endPage = Math.min( - totalPages - 1, - Math.min(currentPage + NEIGHBOURS, totalPages) - ); - - let p = range(startPage, endPage + 1); - - const hasLeftSpill = startPage > 2; - const hasRightSpill = totalPages - endPage > 1; - const spillOffset = totalNumbers - (p.length + 1); - - switch (true) { - case hasLeftSpill && !hasRightSpill: { - p = [...range(startPage - spillOffset - 1, startPage - 1), ...p]; - break; - } - - case !hasLeftSpill && hasRightSpill: { - p = [...p, ...range(endPage + 1, endPage + spillOffset + 1)]; - break; - } - - default: - break; - } - - return p; - }, [currentPage, totalPages]); - - return ( - - {currentPage > 1 ? ( - - Previous - - ) : ( - Previous - )} - {totalPages > 1 && ( -
      - {!pages.includes(1) && ( - - )} - {!pages.includes(2) && ( -
    • - -
    • - )} - {pages.map((p) => ( - - ))} - {!pages.includes(totalPages - 1) && ( -
    • - -
    • - )} - {!pages.includes(totalPages) && ( - - )} -
    - )} - {currentPage < totalPages ? ( - - Next - - ) : ( - Next - )} -
    - ); -}; - -export default Pagination; diff --git a/kafka-ui-react-app/src/components/common/Pagination/__tests__/PageControl.spec.tsx b/kafka-ui-react-app/src/components/common/Pagination/__tests__/PageControl.spec.tsx deleted file mode 100644 index 5e2c37471c5..00000000000 --- a/kafka-ui-react-app/src/components/common/Pagination/__tests__/PageControl.spec.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import PageControl, { - PageControlProps, -} from 'components/common/Pagination/PageControl'; -import { screen } from '@testing-library/react'; -import { render } from 'lib/testHelpers'; -import theme from 'theme/theme'; - -const page = 138; - -describe('PageControl', () => { - const setupComponent = (props: Partial = {}) => - render(); - - const getButton = () => screen.getByRole('button'); - - it('renders current page', () => { - setupComponent({ current: true }); - expect(getButton()).toHaveStyle( - `background-color: ${theme.pagination.currentPage}` - ); - }); - - it('renders non-current page', () => { - setupComponent({ current: false }); - expect(getButton()).toHaveStyle( - `background-color: ${theme.pagination.backgroundColor}` - ); - }); - - it('renders page number', () => { - setupComponent({ current: false }); - expect(getButton()).toHaveTextContent(String(page)); - }); -}); diff --git a/kafka-ui-react-app/src/components/common/Pagination/__tests__/Pagination.spec.tsx b/kafka-ui-react-app/src/components/common/Pagination/__tests__/Pagination.spec.tsx deleted file mode 100644 index 260d2c93445..00000000000 --- a/kafka-ui-react-app/src/components/common/Pagination/__tests__/Pagination.spec.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; -import Pagination, { - PaginationProps, -} from 'components/common/Pagination/Pagination'; -import theme from 'theme/theme'; -import { render } from 'lib/testHelpers'; -import { screen } from '@testing-library/react'; - -describe('Pagination', () => { - const setupComponent = ( - search = '', - props: Partial = {} - ) => { - const defaultPath = '/my/test/path/23'; - const pathName = search ? `${defaultPath}${search}` : defaultPath; - return render(, { - initialEntries: [pathName], - }); - }; - - describe('next & prev buttons', () => { - it('renders disable prev button and enabled next link', () => { - setupComponent('?page=1'); - expect(screen.getByText('Previous')).toBeDisabled(); - expect(screen.getByText('Next')).toBeInTheDocument(); - }); - - it('renders disable next button and enabled prev link', () => { - setupComponent('?page=11'); - expect(screen.getByText('Previous')).toBeInTheDocument(); - expect(screen.getByText('Next')).toBeDisabled(); - }); - - it('renders next & prev links with correct path', () => { - setupComponent('?page=5&perPage=20'); - expect(screen.getByText('Previous')).toBeInTheDocument(); - expect(screen.getByText('Next')).toBeInTheDocument(); - expect(screen.getByText('Previous')).toHaveAttribute( - 'href', - '/my/test/path/23?page=4&perPage=20' - ); - expect(screen.getByText('Next')).toHaveAttribute( - 'href', - '/my/test/path/23?page=6&perPage=20' - ); - }); - }); - - describe('spread', () => { - it('renders 1 spread element after first page control', () => { - setupComponent('?page=8'); - expect(screen.getAllByRole('listitem')[1]).toHaveTextContent('…'); - expect(screen.getAllByRole('listitem')[1].firstChild).toHaveClass( - 'pagination-ellipsis' - ); - }); - - it('renders 1 spread element before last spread control', () => { - setupComponent('?page=2'); - expect(screen.getAllByRole('listitem')[7]).toHaveTextContent('…'); - expect(screen.getAllByRole('listitem')[7].firstChild).toHaveClass( - 'pagination-ellipsis' - ); - }); - - it('renders 2 spread elements', () => { - setupComponent('?page=6'); - expect(screen.getAllByText('…').length).toEqual(2); - expect(screen.getAllByRole('listitem')[0]).toHaveTextContent('1'); - expect(screen.getAllByRole('listitem')[1]).toHaveTextContent('…'); - expect(screen.getAllByRole('listitem')[7]).toHaveTextContent('…'); - expect(screen.getAllByRole('listitem')[8]).toHaveTextContent('11'); - }); - - it('renders 0 spread elements', () => { - setupComponent('?page=2', { totalPages: 8 }); - expect(screen.queryAllByText('…').length).toEqual(0); - expect(screen.getAllByRole('listitem').length).toEqual(8); - }); - }); - - describe('current page', () => { - it('check if it sets page 8 as current when page param is set', () => { - setupComponent('?page=8'); - expect(screen.getByText('8')).toHaveStyle( - `background-color: ${theme.pagination.currentPage}` - ); - }); - - it('check if it sets first page as current when page param not set', () => { - setupComponent('', { totalPages: 8 }); - expect(screen.getByText('1')).toHaveStyle( - `background-color: ${theme.pagination.currentPage}` - ); - }); - }); -}); diff --git a/kafka-ui-react-app/src/components/common/ProgressBar/ProgressBar.styled.ts b/kafka-ui-react-app/src/components/common/ProgressBar/ProgressBar.styled.ts new file mode 100644 index 00000000000..7f612615d9f --- /dev/null +++ b/kafka-ui-react-app/src/components/common/ProgressBar/ProgressBar.styled.ts @@ -0,0 +1,22 @@ +import styled, { css } from 'styled-components'; + +export const Wrapper = styled.div` + height: 10px; + width: 100%; + min-width: 200px; + background-color: ${({ theme }) => theme.progressBar.backgroundColor}; + border-radius: 5px; + margin: 16px; + border: 1px solid ${({ theme }) => theme.progressBar.borderColor}; +`; + +export const Filler = styled.div<{ completed: number }>( + ({ theme: { progressBar }, completed }) => css` + height: 100%; + width: ${completed}%; + background-color: ${progressBar.compleatedColor}; + border-radius: 5px; + text-align: right; + transition: width 1.2s linear; + ` +); diff --git a/kafka-ui-react-app/src/components/common/ProgressBar/ProgressBar.tsx b/kafka-ui-react-app/src/components/common/ProgressBar/ProgressBar.tsx new file mode 100644 index 00000000000..17d492cbc64 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/ProgressBar/ProgressBar.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import * as S from './ProgressBar.styled'; + +interface ProgressBarProps { + completed: number; +} + +const ProgressBar: React.FC = ({ completed }) => { + const p = Math.max(Math.min(completed, 100), 0); + return ( + + + + ); +}; + +export default ProgressBar; diff --git a/kafka-ui-react-app/src/components/common/ProgressBar/__test__/ProgressBar.spec.tsx b/kafka-ui-react-app/src/components/common/ProgressBar/__test__/ProgressBar.spec.tsx new file mode 100644 index 00000000000..86e6922ee49 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/ProgressBar/__test__/ProgressBar.spec.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { render } from 'lib/testHelpers'; +import ProgressBar from 'components/common/ProgressBar/ProgressBar'; +import { screen } from '@testing-library/dom'; + +describe('Progressbar', () => { + const itRendersCorrectPercentage = (completed: number, expected: number) => { + it('renders correct percentage', () => { + render(); + const bar = screen.getByRole('progressbar'); + expect(bar).toHaveStyleRule('width', `${expected}%`); + }); + }; + + [ + [-143, 0], + [0, 0], + [67, 67], + [143, 100], + ].forEach(([completed, expected]) => + itRendersCorrectPercentage(completed, expected) + ); +}); diff --git a/kafka-ui-react-app/src/components/common/PropertiesList/PropertiesList.styled.tsx b/kafka-ui-react-app/src/components/common/PropertiesList/PropertiesList.styled.tsx new file mode 100644 index 00000000000..e769cf5abf1 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/PropertiesList/PropertiesList.styled.tsx @@ -0,0 +1,17 @@ +import styled from 'styled-components'; + +export const List = styled.div` + display: grid; + grid-template-columns: repeat(2, max-content); + gap: 8px; + column-gap: 24px; + margin: 16px 0; + text-align: left; +`; + +export const Label = styled.div` + font-size: 14px; + font-weight: 500; + color: ${({ theme }) => theme.default.color.normal}; + white-space: nowrap; +`; diff --git a/kafka-ui-react-app/src/components/common/SQLEditor/SQLEditor.tsx b/kafka-ui-react-app/src/components/common/SQLEditor/SQLEditor.tsx index de52f9a4518..14433f06edd 100644 --- a/kafka-ui-react-app/src/components/common/SQLEditor/SQLEditor.tsx +++ b/kafka-ui-react-app/src/components/common/SQLEditor/SQLEditor.tsx @@ -1,22 +1,26 @@ /* eslint-disable react/jsx-props-no-spreading */ import AceEditor, { IAceEditorProps } from 'react-ace'; +import 'ace-builds/src-noconflict/ace'; import 'ace-builds/src-noconflict/mode-sql'; import 'ace-builds/src-noconflict/theme-textmate'; -import React from 'react'; -import ReactAce from 'react-ace/lib/ace'; +import 'ace-builds/src-noconflict/theme-dracula'; +import React, { useContext } from 'react'; +import { ThemeModeContext } from 'components/contexts/ThemeModeContext'; interface SQLEditorProps extends IAceEditorProps { isFixedHeight?: boolean; } -const SQLEditor = React.forwardRef( +const SQLEditor = React.forwardRef( (props, ref) => { const { isFixedHeight, ...rest } = props; + const { isDarkMode } = useContext(ThemeModeContext); + return ( void; placeholder?: string; - value: string; disabled?: boolean; + onChange?: (value: string) => void; + value?: string; } +const IconButtonWrapper = styled.span.attrs(() => ({ + role: 'button', + tabIndex: '0', +}))` + height: 16px !important; + display: inline-block; + &:hover { + cursor: pointer; + } +`; const Search: React.FC = ({ - handleSearch, placeholder = 'Search', - value, disabled = false, + value, + onChange, }) => { - const onChange = useDebouncedCallback( - (e) => handleSearch(e.target.value), - 300 - ); + const [searchParams, setSearchParams] = useSearchParams(); + const ref = useRef(null); + const handleChange = useDebouncedCallback((e) => { + if (ref.current != null) { + ref.current.value = e.target.value; + } + if (onChange) { + onChange(e.target.value); + } else { + searchParams.set('q', e.target.value); + if (searchParams.get('page')) { + searchParams.set('page', '1'); + } + setSearchParams(searchParams); + } + }, 500); + const clearSearchValue = () => { + if (searchParams.get('q')) { + searchParams.set('q', ''); + setSearchParams(searchParams); + } + if (ref.current != null) { + ref.current.value = ''; + } + }; return ( + + + } /> ); }; diff --git a/kafka-ui-react-app/src/components/common/Search/__tests__/Search.spec.tsx b/kafka-ui-react-app/src/components/common/Search/__tests__/Search.spec.tsx index 1d9b5deaec8..2103d223367 100644 --- a/kafka-ui-react-app/src/components/common/Search/__tests__/Search.spec.tsx +++ b/kafka-ui-react-app/src/components/common/Search/__tests__/Search.spec.tsx @@ -3,42 +3,62 @@ import React from 'react'; import { render } from 'lib/testHelpers'; import userEvent from '@testing-library/user-event'; import { screen } from '@testing-library/react'; +import { useSearchParams } from 'react-router-dom'; jest.mock('use-debounce', () => ({ useDebouncedCallback: (fn: (e: Event) => void) => fn, })); +const setSearchParamsMock = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...(jest.requireActual('react-router-dom') as object), + useSearchParams: jest.fn(), +})); + +const placeholder = 'I am a search placeholder'; + describe('Search', () => { - const handleSearch = jest.fn(); - it('calls handleSearch on input', () => { - render( - - ); - const input = screen.getByPlaceholderText('Search bt the Topic name'); - userEvent.click(input); - userEvent.keyboard('value'); - expect(handleSearch).toHaveBeenCalledTimes(5); + beforeEach(() => { + (useSearchParams as jest.Mock).mockImplementation(() => [ + new URLSearchParams(), + setSearchParamsMock, + ]); + }); + it('calls handleSearch on input', async () => { + render(); + const input = screen.getByPlaceholderText(placeholder); + await userEvent.click(input); + await userEvent.keyboard('value'); + expect(setSearchParamsMock).toHaveBeenCalledTimes(5); }); it('when placeholder is provided', () => { - render( - - ); - expect( - screen.getByPlaceholderText('Search bt the Topic name') - ).toBeInTheDocument(); + render(); + expect(screen.getByPlaceholderText(placeholder)).toBeInTheDocument(); }); it('when placeholder is not provided', () => { - render(); + render(); expect(screen.queryByPlaceholderText('Search')).toBeInTheDocument(); }); + + it('Clear button is visible', () => { + render(); + + const clearButton = screen.getByRole('button'); + expect(clearButton).toBeInTheDocument(); + }); + + it('Clear button should clear text from input', async () => { + render(); + + const searchField = screen.getAllByRole('textbox')[0]; + await userEvent.type(searchField, 'some text'); + expect(searchField).toHaveValue('some text'); + + const clearButton = screen.getByRole('button'); + await userEvent.click(clearButton); + + expect(searchField).toHaveValue(''); + }); }); diff --git a/kafka-ui-react-app/src/components/common/Select/ControlledSelect.tsx b/kafka-ui-react-app/src/components/common/Select/ControlledSelect.tsx new file mode 100644 index 00000000000..1ea90c356ae --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Select/ControlledSelect.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; +import { Controller } from 'react-hook-form'; +import { FormError } from 'components/common/Input/Input.styled'; +import { InputLabel } from 'components/common/Input/InputLabel.styled'; +import { ErrorMessage } from '@hookform/error-message'; + +import Select, { SelectOption } from './Select'; + +interface ControlledSelectProps { + name: string; + label: React.ReactNode; + hint?: string; + options: SelectOption[]; + onChange?: (val: string | number) => void; + disabled?: boolean; + placeholder?: string; +} + +const ControlledSelect: React.FC = ({ + name, + label, + onChange, + options, + disabled = false, + placeholder, +}) => { + const id = React.useId(); + + return ( +
    + {label} + { + return ( +
    - {checkboxElement} - {headerCells} - - ); - }, [children, allSelectable, tableState]); - - const bodyRows = React.useMemo(() => { - if (tableState.data.length === 0) { - const colspan = React.Children.count(children) + +selectable; - return ( - - - - ); - } - return tableState.data.map((dataItem, index) => { - return ( - - {children} - - ); - }); - }, [ - children, - handleRowSelection, - hoverable, - placeholder, - selectable, - tableState, - ]); - - return ( - <> -
    {placeholder}
    - {headerRow} - {bodyRows} -
    - {paginated && tableState.totalPages !== undefined && ( - - )} - - ); -}; diff --git a/kafka-ui-react-app/src/components/common/SmartTable/TableColumn.tsx b/kafka-ui-react-app/src/components/common/SmartTable/TableColumn.tsx deleted file mode 100644 index 27f03398607..00000000000 --- a/kafka-ui-react-app/src/components/common/SmartTable/TableColumn.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React from 'react'; -import { TableState } from 'lib/hooks/useTableState'; -import { SortOrder } from 'generated-sources'; -import * as S from 'components/common/table/TableHeaderCell/TableHeaderCell.styled'; -import { DefaultTheme, StyledComponent } from 'styled-components'; - -export interface OrderableProps { - orderBy: OT | null; - sortOrder: SortOrder; - handleOrderBy: (orderBy: OT | null) => void; -} - -interface TableCellPropsBase { - tableState: TableState; -} - -export interface TableHeaderCellProps - extends TableCellPropsBase { - orderable?: OrderableProps; - orderValue?: OT; -} - -export interface TableCellProps - extends TableCellPropsBase { - rowIndex: number; - dataItem: T; - hovered?: boolean; -} - -interface TableColumnProps { - cell?: React.FC>; - children?: React.ReactElement; - headerCell?: React.FC>; - field?: string; - title?: string; - maxWidth?: string; - className?: string; - orderValue?: OT; - customTd?: typeof S.Td; -} - -export const TableColumn = ( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _props: React.PropsWithChildren> -): React.ReactElement => { - return ; -}; - -export function isColumnElement( - element: React.ReactNode -): element is React.ReactElement> { - if (!React.isValidElement(element)) { - return false; - } - - const elementType = (element as React.ReactElement).type; - return ( - elementType === TableColumn || - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (elementType as any).originalType === TableColumn - ); -} - -interface SelectCellProps { - selected: boolean; - selectable: boolean; - el: 'td' | 'th'; - rowIndex: number; - onChange: (checked: boolean) => void; -} - -export const SelectCell: React.FC = ({ - selected, - selectable, - rowIndex, - onChange, - el, -}) => { - const handleChange = (e: React.ChangeEvent) => { - onChange(e.target.checked); - }; - - let El: 'td' | StyledComponent<'th', DefaultTheme>; - if (el === 'th') { - El = S.TableHeaderCell; - } else { - El = el; - } - - return ( - - {selectable && ( - - )} - - ); -}; diff --git a/kafka-ui-react-app/src/components/common/SmartTable/TableRow.tsx b/kafka-ui-react-app/src/components/common/SmartTable/TableRow.tsx deleted file mode 100644 index 92ab8804f5d..00000000000 --- a/kafka-ui-react-app/src/components/common/SmartTable/TableRow.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react'; -import { propertyLookup } from 'lib/propertyLookup'; -import { TableState } from 'lib/hooks/useTableState'; -import { Td } from 'components/common/table/TableHeaderCell/TableHeaderCell.styled'; - -import { isColumnElement, SelectCell, TableCellProps } from './TableColumn'; - -interface TableRowProps { - index: number; - id?: TId; - hoverable?: boolean; - tableState: TableState; - dataItem: T; - selectable: boolean; - onSelectChange?: (row: T, checked: boolean) => void; -} - -export const TableRow = ({ - children, - hoverable = false, - id, - index, - dataItem, - selectable, - tableState, - onSelectChange, -}: React.PropsWithChildren>): React.ReactElement => { - const [hovered, setHovered] = React.useState(false); - - const handleMouseEnter = () => { - setHovered(true); - }; - - const handleMouseLeave = () => { - setHovered(false); - }; - - const handleSelectChange = (checked: boolean) => { - onSelectChange?.(dataItem, checked); - }; - - return ( - - {selectable && ( - - )} - {React.Children.map(children, (child) => { - if (!isColumnElement(child)) { - return child; - } - const { cell, field, maxWidth, customTd } = child.props; - - const Cell = cell as React.FC> | undefined; - const TdComponent = customTd || Td; - - const content = Cell ? ( - - ) : ( - field && propertyLookup(field, dataItem) - ); - - return ( - - {content as React.ReactNode} - - ); - })} - - ); -}; diff --git a/kafka-ui-react-app/src/components/common/Spinner/Spinner.styled.ts b/kafka-ui-react-app/src/components/common/Spinner/Spinner.styled.ts new file mode 100644 index 00000000000..32edbcb11c4 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Spinner/Spinner.styled.ts @@ -0,0 +1,26 @@ +import styled from 'styled-components'; +import { SpinnerProps } from 'components/common/Spinner/types'; + +export const Spinner = styled.div` + border-width: ${(props) => props.borderWidth}px; + border-style: solid; + border-color: ${({ theme }) => theme.pageLoader.borderColor}; + border-bottom-color: ${(props) => + props.emptyBorderColor + ? 'transparent' + : props.theme.pageLoader.borderBottomColor}; + border-radius: 50%; + width: ${(props) => props.size}px; + height: ${(props) => props.size}px; + margin-left: ${(props) => props.marginLeft}px; + animation: spin 1.3s linear infinite; + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } +`; diff --git a/kafka-ui-react-app/src/components/common/Spinner/Spinner.tsx b/kafka-ui-react-app/src/components/common/Spinner/Spinner.tsx new file mode 100644 index 00000000000..1d1cd597333 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Spinner/Spinner.tsx @@ -0,0 +1,20 @@ +/* eslint-disable react/default-props-match-prop-types */ +import React from 'react'; +import { SpinnerProps } from 'components/common/Spinner/types'; + +import * as S from './Spinner.styled'; + +const defaultProps: SpinnerProps = { + size: 80, + borderWidth: 10, + emptyBorderColor: false, + marginLeft: 0, +}; + +const Spinner: React.FC = (props) => ( + +); + +Spinner.defaultProps = defaultProps; + +export default Spinner; diff --git a/kafka-ui-react-app/src/components/common/Spinner/types.ts b/kafka-ui-react-app/src/components/common/Spinner/types.ts new file mode 100644 index 00000000000..7db64b7fd30 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Spinner/types.ts @@ -0,0 +1,6 @@ +export interface SpinnerProps { + size?: number; + borderWidth?: number; + emptyBorderColor?: boolean; + marginLeft?: number; +} diff --git a/kafka-ui-react-app/src/components/common/SuspenseQueryComponent/SuspenseQueryComponent.tsx b/kafka-ui-react-app/src/components/common/SuspenseQueryComponent/SuspenseQueryComponent.tsx new file mode 100644 index 00000000000..2efbb1e0b00 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/SuspenseQueryComponent/SuspenseQueryComponent.tsx @@ -0,0 +1,29 @@ +import React, { PropsWithChildren } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; +import { Navigate } from 'react-router-dom'; + +const ErrorComponent: React.FC<{ error: Error }> = ({ error }) => { + const errorStatus = (error as unknown as Response)?.status + ? (error as unknown as Response).status + : '404'; + + return ; +}; + +/** + * @description + * basic idea that you can not choose a wrong url, that is why you are safe, but when + * the user tries to manipulate some url to get the the desired result and the BE returns 404 + * it will be propagated to this component and redirected + * + * !!NOTE!! But only use this Component for GET query Throw error cause maybe in the future inner functionality may change + * */ +const SuspenseQueryComponent: React.FC> = ({ + children, +}) => { + return ( + {children} + ); +}; + +export default SuspenseQueryComponent; diff --git a/kafka-ui-react-app/src/components/common/SuspenseQueryComponent/__test__/SuspenseQueryComponent.spec.tsx b/kafka-ui-react-app/src/components/common/SuspenseQueryComponent/__test__/SuspenseQueryComponent.spec.tsx new file mode 100644 index 00000000000..fd191236a8a --- /dev/null +++ b/kafka-ui-react-app/src/components/common/SuspenseQueryComponent/__test__/SuspenseQueryComponent.spec.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { render } from 'lib/testHelpers'; +import { screen } from '@testing-library/react'; +import SuspenseQueryComponent from 'components/common/SuspenseQueryComponent/SuspenseQueryComponent'; + +const fallback = 'fallback'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + Navigate: () =>
    {fallback}
    , +})); + +describe('SuspenseQueryComponent', () => { + const text = 'text'; + + it('should render the inner component if no error occurs', () => { + render({text}); + expect(screen.getByText(text)).toBeInTheDocument(); + }); + + it('should not render the inner component and call navigate', () => { + // throwing intentional For error boundaries to work + jest.spyOn(console, 'error').mockImplementation(() => undefined); + const Component = () => { + throw new Error('new Error'); + }; + + render( + + + + ); + expect(screen.queryByText(text)).not.toBeInTheDocument(); + expect(screen.getByText(fallback)).toBeInTheDocument(); + jest.clearAllMocks(); + }); +}); diff --git a/kafka-ui-react-app/src/components/common/Switch/Switch.styled.ts b/kafka-ui-react-app/src/components/common/Switch/Switch.styled.ts index cced933eb80..0f4f2c1d11c 100644 --- a/kafka-ui-react-app/src/components/common/Switch/Switch.styled.ts +++ b/kafka-ui-react-app/src/components/common/Switch/Switch.styled.ts @@ -1,24 +1,48 @@ import styled from 'styled-components'; -export const StyledLabel = styled.label` +interface Props { + isCheckedIcon?: boolean; +} + +export const StyledLabel = styled.label` position: relative; display: inline-block; - width: 34px; + width: ${({ isCheckedIcon }) => (isCheckedIcon ? '40px' : '34px')}; height: 20px; margin-right: 8px; `; - -export const StyledSlider = styled.span` +export const CheckedIcon = styled.span` + position: absolute; + top: 1px; + left: 24px; + z-index: 10; + cursor: pointer; +`; +export const UnCheckedIcon = styled.span` + position: absolute; + top: 2px; + right: 23px; + z-index: 10; + cursor: pointer; +`; +export const StyledSlider = styled.span` position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; - background-color: ${({ theme }) => theme.switch.unchecked}; + background-color: ${({ isCheckedIcon, theme }) => + isCheckedIcon + ? theme.switch.checkedIcon.backgroundColor + : theme.switch.unchecked}; transition: 0.4s; border-radius: 20px; + :hover { + background-color: ${({ theme }) => theme.switch.hover}; + } + &::before { position: absolute; content: ''; @@ -29,16 +53,20 @@ export const StyledSlider = styled.span` background-color: ${({ theme }) => theme.switch.circle}; transition: 0.4s; border-radius: 50%; + z-index: 11; } `; -export const StyledInput = styled.input` +export const StyledInput = styled.input` opacity: 0; width: 0; height: 0; &:checked + ${StyledSlider} { - background-color: ${({ theme }) => theme.switch.checked}; + background-color: ${({ isCheckedIcon, theme }) => + isCheckedIcon + ? theme.switch.checkedIcon.backgroundColor + : theme.switch.checked}; } &:focus + ${StyledSlider} { @@ -46,6 +74,8 @@ export const StyledInput = styled.input` } :checked + ${StyledSlider}:before { - transform: translateX(14px); + transform: translateX( + ${({ isCheckedIcon }) => (isCheckedIcon ? '20px' : '14px')} + ); } `; diff --git a/kafka-ui-react-app/src/components/common/Switch/Switch.tsx b/kafka-ui-react-app/src/components/common/Switch/Switch.tsx index 9252239e848..e1cb56d7a03 100644 --- a/kafka-ui-react-app/src/components/common/Switch/Switch.tsx +++ b/kafka-ui-react-app/src/components/common/Switch/Switch.tsx @@ -6,18 +6,30 @@ export interface SwitchProps { onChange(): void; checked: boolean; name: string; + checkedIcon?: React.ReactNode; + unCheckedIcon?: React.ReactNode; + bgCustomColor?: string; } - -const Switch: React.FC = ({ name, checked, onChange }) => { +const Switch: React.FC = ({ + name, + checked, + onChange, + checkedIcon, + unCheckedIcon, +}) => { + const isCheckedIcon = !!(checkedIcon || unCheckedIcon); return ( - + - + + {checkedIcon && {checkedIcon}} + {unCheckedIcon && {unCheckedIcon}} ); }; diff --git a/kafka-ui-react-app/src/components/common/Tabs/SecondaryTabs.styled.ts b/kafka-ui-react-app/src/components/common/Tabs/SecondaryTabs.styled.ts deleted file mode 100644 index c41fa23fb2b..00000000000 --- a/kafka-ui-react-app/src/components/common/Tabs/SecondaryTabs.styled.ts +++ /dev/null @@ -1,38 +0,0 @@ -import styled from 'styled-components'; - -export const SecondaryTabs = styled.nav` - & button { - background-color: ${(props) => - props.theme.secondaryTab.backgroundColor.normal}; - color: ${(props) => props.theme.secondaryTab.color.normal}; - padding: 6px; - height: 32px; - min-width: 57px; - border: 1px solid ${(props) => props.theme.layout.stuffBorderColor}; - cursor: pointer; - - &:hover { - background-color: ${(props) => - props.theme.secondaryTab.backgroundColor.hover}; - color: ${(props) => props.theme.secondaryTab.color.hover}; - } - - &.is-active { - background-color: ${(props) => - props.theme.secondaryTab.backgroundColor.active}; - color: ${(props) => props.theme.secondaryTab.color.active}; - } - } - - & > * { - &:first-child { - border-radius: 4px 0 0 4px; - } - &:last-child { - border-radius: 0 4px 4px 0; - } - &:not(:last-child) { - border-right: 0px; - } - } -`; diff --git a/kafka-ui-react-app/src/components/common/Tabs/Tabs.tsx b/kafka-ui-react-app/src/components/common/Tabs/Tabs.tsx deleted file mode 100644 index 9ff6c20a02f..00000000000 --- a/kafka-ui-react-app/src/components/common/Tabs/Tabs.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* eslint-disable jsx-a11y/anchor-is-valid */ -import React, { PropsWithChildren } from 'react'; -import classNames from 'classnames'; - -interface TabsProps { - tabs: string[]; - defaultSelectedIndex?: number; - onChange?(index: number): void; -} - -const Tabs: React.FC> = ({ - tabs, - defaultSelectedIndex = 0, - onChange, - children, -}) => { - const [selectedIndex, setSelectedIndex] = - React.useState(defaultSelectedIndex); - - React.useEffect(() => { - setSelectedIndex(defaultSelectedIndex); - }, [defaultSelectedIndex]); - - const handleChange = (index: number) => { - setSelectedIndex(index); - onChange?.(index); - }; - - return ( - <> - - {React.Children.toArray(children)[selectedIndex]} - - ); -}; - -export default Tabs; diff --git a/kafka-ui-react-app/src/components/common/Tabs/__tests__/Tabs.spec.tsx b/kafka-ui-react-app/src/components/common/Tabs/__tests__/Tabs.spec.tsx deleted file mode 100644 index 2c6c88b178c..00000000000 --- a/kafka-ui-react-app/src/components/common/Tabs/__tests__/Tabs.spec.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import Tabs from 'components/common/Tabs/Tabs'; -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -describe('Tabs component', () => { - const tabs: string[] = ['Tab 1', 'Tab 2', 'Tab 3']; - - const child1 =
    ; - const child2 =
    ; - const child3 =
    ; - - beforeEach(() => - render( - - {child1} - {child2} - {child3} - - ) - ); - - it('renders the tabs with default index 0', () => { - expect(screen.getAllByRole('listitem')[0]).toHaveClass('is-active'); - }); - it('renders the list of tabs', () => { - screen.queryAllByRole('button').forEach((link, idx) => { - expect(link).toHaveTextContent(tabs[idx]); - }); - }); - it('expects list items to be in the document', () => { - screen.queryAllByRole('button').forEach((link, idx) => { - userEvent.click(link); - expect(screen.getByTestId(`child_${idx + 1}`)).toBeInTheDocument(); - }); - }); -}); diff --git a/kafka-ui-react-app/src/components/common/Tag/Tag.styled.tsx b/kafka-ui-react-app/src/components/common/Tag/Tag.styled.tsx index 7884dc24089..00f76588e34 100644 --- a/kafka-ui-react-app/src/components/common/Tag/Tag.styled.tsx +++ b/kafka-ui-react-app/src/components/common/Tag/Tag.styled.tsx @@ -4,17 +4,18 @@ interface Props { color: 'green' | 'gray' | 'yellow' | 'red' | 'white' | 'blue'; } -export const Tag = styled.p` +export const Tag = styled.span.attrs({ role: 'widget' })` border: none; border-radius: 16px; height: 20px; line-height: 20px; - background-color: ${(props) => props.theme.tag.backgroundColor[props.color]}; - color: ${(props) => props.theme.tag.color}; + background-color: ${({ theme, color }) => theme.tag.backgroundColor[color]}; + color: ${({ theme }) => theme.tag.color}; font-size: 12px; display: inline-block; padding-left: 0.75em; padding-right: 0.75em; text-align: center; width: max-content; + margin: 2px 0; `; diff --git a/kafka-ui-react-app/src/components/common/Tag/getTagColor.ts b/kafka-ui-react-app/src/components/common/Tag/getTagColor.ts index a91b7b0d45c..d52c6676266 100644 --- a/kafka-ui-react-app/src/components/common/Tag/getTagColor.ts +++ b/kafka-ui-react-app/src/components/common/Tag/getTagColor.ts @@ -1,14 +1,6 @@ -import { - ConnectorState, - ConnectorStatus, - ConsumerGroup, - ConsumerGroupState, - TaskStatus, -} from 'generated-sources'; +import { ConnectorState, ConsumerGroupState } from 'generated-sources'; -const getTagColor = ({ - state, -}: ConnectorStatus | TaskStatus | ConsumerGroup) => { +const getTagColor = (state?: string) => { switch (state) { case ConnectorState.RUNNING: case ConsumerGroupState.STABLE: diff --git a/kafka-ui-react-app/src/components/common/Textbox/Textarea.styled.ts b/kafka-ui-react-app/src/components/common/Textbox/Textarea.styled.ts index bc6a6593f36..62b752f7b06 100644 --- a/kafka-ui-react-app/src/components/common/Textbox/Textarea.styled.ts +++ b/kafka-ui-react-app/src/components/common/Textbox/Textarea.styled.ts @@ -7,6 +7,8 @@ export const Textarea = styled.textarea( width: 100%; padding: 12px; padding-top: 6px; + color: ${({ theme }) => theme.default.color.normal}; + background-color: ${({ theme }) => theme.schema.backgroundColor.textarea}; &::placeholder { color: ${textArea.color.placeholder.normal}; font-size: 14px; diff --git a/kafka-ui-react-app/src/components/common/Tooltip/Tooltip.styled.ts b/kafka-ui-react-app/src/components/common/Tooltip/Tooltip.styled.ts new file mode 100644 index 00000000000..ee352995bd4 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Tooltip/Tooltip.styled.ts @@ -0,0 +1,17 @@ +import styled from 'styled-components'; + +export const MessageTooltip = styled.div` + max-width: 100%; + max-height: 100%; + background-color: ${({ theme }) => theme.tooltip.bg}; + color: ${({ theme }) => theme.tooltip.text}; + border-radius: 6px; + padding: 5px; + z-index: 1; + white-space: pre-wrap; +`; + +export const Wrapper = styled.div` + display: flex; + align-items: center; +`; diff --git a/kafka-ui-react-app/src/components/common/Tooltip/Tooltip.tsx b/kafka-ui-react-app/src/components/common/Tooltip/Tooltip.tsx new file mode 100644 index 00000000000..0764320f58b --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Tooltip/Tooltip.tsx @@ -0,0 +1,49 @@ +import React, { useState } from 'react'; +import { + useFloating, + useHover, + useInteractions, + Placement, +} from '@floating-ui/react'; + +import * as S from './Tooltip.styled'; + +interface TooltipProps { + value: React.ReactNode; + content: string; + placement?: Placement; +} + +const Tooltip: React.FC = ({ value, content, placement }) => { + const [open, setOpen] = useState(false); + const { x, y, refs, strategy, context } = useFloating({ + open, + onOpenChange: setOpen, + placement, + }); + const hover = useHover(context); + const { getReferenceProps, getFloatingProps } = useInteractions([hover]); + return ( + <> +
    + {value} +
    + {open && ( + + {content} + + )} + + ); +}; + +export default Tooltip; diff --git a/kafka-ui-react-app/src/components/common/Tooltip/__tests__/Tooltip.spec.tsx b/kafka-ui-react-app/src/components/common/Tooltip/__tests__/Tooltip.spec.tsx new file mode 100644 index 00000000000..df0cc470a1c --- /dev/null +++ b/kafka-ui-react-app/src/components/common/Tooltip/__tests__/Tooltip.spec.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { render } from 'lib/testHelpers'; +import { screen } from '@testing-library/react'; +import Tooltip from 'components/common/Tooltip/Tooltip'; +import userEvent from '@testing-library/user-event'; + +describe('Tooltip', () => { + const tooltipText = 'tooltip'; + const tooltipContent = 'tooltip_Content'; + + const setUpComponent = () => + render(); + + it('should render the tooltip element with its value text', () => { + setUpComponent(); + expect(screen.getByText(tooltipText)).toBeInTheDocument(); + }); + + it('should render the tooltip with default closed', () => { + setUpComponent(); + expect(screen.queryByText(tooltipContent)).not.toBeInTheDocument(); + }); + + it('should render the tooltip with and open during hover', async () => { + setUpComponent(); + await userEvent.hover(screen.getByText(tooltipText)); + expect(screen.getByText(tooltipContent)).toBeInTheDocument(); + }); +}); diff --git a/kafka-ui-react-app/src/components/common/heading/Heading.styled.tsx b/kafka-ui-react-app/src/components/common/heading/Heading.styled.tsx index 661f3b31471..90978fed3be 100644 --- a/kafka-ui-react-app/src/components/common/heading/Heading.styled.tsx +++ b/kafka-ui-react-app/src/components/common/heading/Heading.styled.tsx @@ -10,10 +10,10 @@ const HeadingBase = styled.h1` ${({ theme, $level }) => theme.heading?.variants[$level]} `; -export interface Props { +interface HeadingProps { level?: HeadingLevel; } -const Heading: React.FC> = ({ +const Heading: React.FC> = ({ level = 1, ...rest }) => { diff --git a/kafka-ui-react-app/src/components/common/table/SortableCulumnHeader/SortableColumnHeader.tsx b/kafka-ui-react-app/src/components/common/table/SortableCulumnHeader/SortableColumnHeader.tsx deleted file mode 100644 index 1e58044c4bd..00000000000 --- a/kafka-ui-react-app/src/components/common/table/SortableCulumnHeader/SortableColumnHeader.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import React from 'react'; -import cx from 'classnames'; - -export interface ListHeaderProps { - value: any; - title: string; - orderBy: any; - setOrderBy: React.Dispatch>; -} - -const ListHeaderCell: React.FC = ({ - value, - title, - orderBy, - setOrderBy, -}) => ( - setOrderBy(value)} - > - {title} - -); - -export default ListHeaderCell; diff --git a/kafka-ui-react-app/src/components/common/table/Table/Table.styled.ts b/kafka-ui-react-app/src/components/common/table/Table/Table.styled.ts index 994e1b1fb76..f76b89540d8 100644 --- a/kafka-ui-react-app/src/components/common/table/Table/Table.styled.ts +++ b/kafka-ui-react-app/src/components/common/table/Table/Table.styled.ts @@ -8,7 +8,7 @@ export const Table = styled.table` width: ${(props) => (props.isFullwidth ? '100%' : 'auto')}; & td { - border-top: 1px #f1f2f3 solid; + border-top: 1px ${({ theme }) => theme.table.td.borderTop} solid; font-size: 14px; font-weight: 400; padding: 8px 8px 8px 24px; diff --git a/kafka-ui-react-app/src/components/common/table/Table/TableKeyLink.styled.ts b/kafka-ui-react-app/src/components/common/table/Table/TableKeyLink.styled.ts index 3b5fdf59532..2560e48d915 100644 --- a/kafka-ui-react-app/src/components/common/table/Table/TableKeyLink.styled.ts +++ b/kafka-ui-react-app/src/components/common/table/Table/TableKeyLink.styled.ts @@ -3,17 +3,24 @@ import styled, { css } from 'styled-components'; const tableLinkMixin = css( ({ theme }) => ` & > a { - color: ${theme.table.link.color}; + color: ${theme.table.link.color.normal}; font-weight: 500; text-overflow: ellipsis; + + &:hover { + color: ${theme.table.link.color.hover}; + } + + &:active { + color: ${theme.table.link.color.active}; + } + } + tr { + background-color: red; } ` ); -export const TableKeyLink = styled.td` - ${tableLinkMixin} -`; - -export const SmartTableKeyLink = styled.div` +export const TableKeyLink = styled.div` ${tableLinkMixin} `; diff --git a/kafka-ui-react-app/src/components/common/table/TableHeaderCell/TableHeaderCell.styled.ts b/kafka-ui-react-app/src/components/common/table/TableHeaderCell/TableHeaderCell.styled.ts index 1e35e7d1a57..07b0d158ac5 100644 --- a/kafka-ui-react-app/src/components/common/table/TableHeaderCell/TableHeaderCell.styled.ts +++ b/kafka-ui-react-app/src/components/common/table/TableHeaderCell/TableHeaderCell.styled.ts @@ -67,12 +67,6 @@ const DESCMixin = css( ` ); -export const Td = styled.td<{ maxWidth?: string }>` - overflow: hidden; - text-overflow: ellipsis; - max-width: ${(props) => props.maxWidth}; -`; - export const Title = styled.span( ({ isOrderable, isOrdered, sortOrder, theme: { table } }) => css` font-family: Inter, sans-serif; @@ -80,7 +74,7 @@ export const Title = styled.span( font-style: normal; font-weight: 400; line-height: 16px; - letter-spacing: 0em; + letter-spacing: 0; text-align: left; display: inline-block; justify-content: start; @@ -103,11 +97,11 @@ export const Preview = styled.span` font-style: normal; font-weight: 400; line-height: 16px; - letter-spacing: 0em; + letter-spacing: 0; text-align: left; - background: ${(props) => props.theme.table.th.backgroundColor.normal}; + background: ${({ theme }) => theme.table.th.backgroundColor.normal}; font-size: 14px; - color: ${(props) => props.theme.table.th.previewColor.normal}; + color: ${({ theme }) => theme.table.th.previewColor.normal}; cursor: pointer; `; @@ -115,4 +109,5 @@ export const TableHeaderCell = styled.th` padding: 4px 0 4px 24px; border-bottom-width: 1px; vertical-align: middle; + text-align: left; `; diff --git a/kafka-ui-react-app/src/components/common/table/TableHeaderCell/TableHeaderCell.tsx b/kafka-ui-react-app/src/components/common/table/TableHeaderCell/TableHeaderCell.tsx index 98eb36b56e5..341ab6c8814 100644 --- a/kafka-ui-react-app/src/components/common/table/TableHeaderCell/TableHeaderCell.tsx +++ b/kafka-ui-react-app/src/components/common/table/TableHeaderCell/TableHeaderCell.tsx @@ -1,15 +1,15 @@ import React, { PropsWithChildren } from 'react'; -import { SortOrder, TopicColumnsToSort } from 'generated-sources'; +import { SortOrder } from 'generated-sources'; import * as S from 'components/common/table/TableHeaderCell/TableHeaderCell.styled'; export interface TableHeaderCellProps { title?: string; previewText?: string; onPreview?: () => void; - orderBy?: TopicColumnsToSort | null; + orderBy?: string | null; sortOrder?: SortOrder; - orderValue?: TopicColumnsToSort | null; - handleOrderBy?: (orderBy: TopicColumnsToSort | null) => void; + orderValue?: string; + handleOrderBy?: (orderBy: string | null) => void; } const TableHeaderCell: React.FC> = ( diff --git a/kafka-ui-react-app/src/components/common/table/TableHeaderCell/__test__/TableHeaderCell.styled.spec.tsx b/kafka-ui-react-app/src/components/common/table/TableHeaderCell/__test__/TableHeaderCell.styled.spec.tsx index e780c044ce1..38c3b7abe55 100644 --- a/kafka-ui-react-app/src/components/common/table/TableHeaderCell/__test__/TableHeaderCell.styled.spec.tsx +++ b/kafka-ui-react-app/src/components/common/table/TableHeaderCell/__test__/TableHeaderCell.styled.spec.tsx @@ -3,7 +3,7 @@ import { render } from 'lib/testHelpers'; import * as S from 'components/common/table/TableHeaderCell/TableHeaderCell.styled'; import { SortOrder } from 'generated-sources'; import { screen } from '@testing-library/react'; -import theme from 'theme/theme'; +import { theme } from 'theme/theme'; describe('TableHeaderCell.Styled', () => { describe('Title Component', () => { diff --git a/kafka-ui-react-app/src/components/common/table/TableTitle/TableTitle.styled.tsx b/kafka-ui-react-app/src/components/common/table/TableTitle/TableTitle.styled.tsx index 1ef6ee258d1..ee1d26d504a 100644 --- a/kafka-ui-react-app/src/components/common/table/TableTitle/TableTitle.styled.tsx +++ b/kafka-ui-react-app/src/components/common/table/TableTitle/TableTitle.styled.tsx @@ -3,5 +3,5 @@ import Heading from 'components/common/heading/Heading.styled'; import styled from 'styled-components'; export const TableTitle = styled((props) => )` - padding: 16px; + padding: 16px 16px 0; `; diff --git a/kafka-ui-react-app/src/components/common/table/__tests__/SortableColumnHeader.spec.tsx b/kafka-ui-react-app/src/components/common/table/__tests__/SortableColumnHeader.spec.tsx deleted file mode 100644 index 14fa1114add..00000000000 --- a/kafka-ui-react-app/src/components/common/table/__tests__/SortableColumnHeader.spec.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import SortableColumnHeader from 'components/common/table/SortableCulumnHeader/SortableColumnHeader'; -import { TopicColumnsToSort } from 'generated-sources'; -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -describe('ListHeader', () => { - const setOrderBy = jest.fn(); - const component = ( - - - - - - -
    - ); - - describe('on column click', () => { - it('calls setOrderBy', () => { - render(component); - userEvent.click(screen.getByRole('columnheader')); - expect(setOrderBy).toHaveBeenCalledTimes(1); - expect(setOrderBy).toHaveBeenCalledWith(TopicColumnsToSort.NAME); - }); - }); -}); diff --git a/kafka-ui-react-app/src/components/common/table/__tests__/TableHeaderCell.spec.tsx b/kafka-ui-react-app/src/components/common/table/__tests__/TableHeaderCell.spec.tsx index 949a75b2ef2..80aceed3284 100644 --- a/kafka-ui-react-app/src/components/common/table/__tests__/TableHeaderCell.spec.tsx +++ b/kafka-ui-react-app/src/components/common/table/__tests__/TableHeaderCell.spec.tsx @@ -5,7 +5,7 @@ import TableHeaderCell, { TableHeaderCellProps, } from 'components/common/table/TableHeaderCell/TableHeaderCell'; import { SortOrder, TopicColumnsToSort } from 'generated-sources'; -import theme from 'theme/theme'; +import { theme } from 'theme/theme'; import userEvent from '@testing-library/user-event'; const SPACE_KEY = ' '; @@ -23,7 +23,7 @@ describe('TableHeaderCell', () => { - ; +
    @@ -61,7 +61,7 @@ describe('TableHeaderCell', () => { expect(title).toHaveStyle(`color: ${theme.table.th.color.active};`); expect(title).toHaveStyle('cursor: pointer;'); }); - it('renders click on title triggers handler', () => { + it('renders click on title triggers handler', async () => { setupComponent({ title: testTitle, orderBy: TopicColumnsToSort.NAME, @@ -69,11 +69,11 @@ describe('TableHeaderCell', () => { handleOrderBy, }); const title = within(getColumnHeader()).getByRole('button'); - userEvent.click(title); + await userEvent.click(title); expect(handleOrderBy.mock.calls.length).toBe(1); }); - it('renders space on title triggers handler', () => { + it('renders space on title triggers handler', async () => { setupComponent({ title: testTitle, orderBy: TopicColumnsToSort.NAME, @@ -81,30 +81,30 @@ describe('TableHeaderCell', () => { handleOrderBy, }); const title = within(getColumnHeader()).getByRole('button'); - userEvent.type(title, SPACE_KEY); + await userEvent.type(title, SPACE_KEY); // userEvent.type clicks and only then presses space expect(handleOrderBy.mock.calls.length).toBe(2); }); - it('click on preview triggers handler', () => { + it('click on preview triggers handler', async () => { setupComponent({ title: testTitle, previewText: testPreviewText, onPreview, }); const preview = within(getColumnHeader()).getByRole('button'); - userEvent.click(preview); + await userEvent.click(preview); expect(onPreview.mock.calls.length).toBe(1); }); - it('click on preview triggers handler', () => { + it('click on preview triggers handler', async () => { setupComponent({ title: testTitle, previewText: testPreviewText, onPreview, }); const preview = within(getColumnHeader()).getByRole('button'); - userEvent.type(preview, SPACE_KEY); + await userEvent.type(preview, SPACE_KEY); expect(onPreview.mock.calls.length).toBe(2); }); diff --git a/kafka-ui-react-app/src/components/contexts/ConfirmContext.tsx b/kafka-ui-react-app/src/components/contexts/ConfirmContext.tsx new file mode 100644 index 00000000000..d68eda2547f --- /dev/null +++ b/kafka-ui-react-app/src/components/contexts/ConfirmContext.tsx @@ -0,0 +1,44 @@ +import React, { useState } from 'react'; + +interface ConfirmContextType { + content: React.ReactNode; + confirm?: () => void; + setContent: React.Dispatch>; + setConfirm: React.Dispatch void) | undefined>>; + cancel: () => void; + dangerButton: boolean; + setDangerButton: React.Dispatch>; +} + +export const ConfirmContext = React.createContext( + null +); + +export const ConfirmContextProvider: React.FC< + React.PropsWithChildren +> = ({ children }) => { + const [content, setContent] = useState(null); + const [confirm, setConfirm] = useState<(() => void) | undefined>(undefined); + const [dangerButton, setDangerButton] = useState(false); + + const cancel = () => { + setContent(null); + setConfirm(undefined); + }; + + return ( + + {children} + + ); +}; diff --git a/kafka-ui-react-app/src/components/contexts/GlobalSettingsContext.tsx b/kafka-ui-react-app/src/components/contexts/GlobalSettingsContext.tsx new file mode 100644 index 00000000000..4de05307b11 --- /dev/null +++ b/kafka-ui-react-app/src/components/contexts/GlobalSettingsContext.tsx @@ -0,0 +1,32 @@ +import { useAppInfo } from 'lib/hooks/api/appConfig'; +import React from 'react'; +import { ApplicationInfoEnabledFeaturesEnum } from 'generated-sources'; + +interface GlobalSettingsContextProps { + hasDynamicConfig: boolean; +} + +export const GlobalSettingsContext = + React.createContext({ + hasDynamicConfig: false, + }); + +export const GlobalSettingsProvider: React.FC< + React.PropsWithChildren +> = ({ children }) => { + const info = useAppInfo(); + const value = React.useMemo(() => { + const features = info.data?.enabledFeatures || []; + return { + hasDynamicConfig: features.includes( + ApplicationInfoEnabledFeaturesEnum.DYNAMIC_CONFIG + ), + }; + }, [info.data]); + + return ( + + {children} + + ); +}; diff --git a/kafka-ui-react-app/src/components/contexts/ThemeModeContext.tsx b/kafka-ui-react-app/src/components/contexts/ThemeModeContext.tsx new file mode 100644 index 00000000000..5011dfe0813 --- /dev/null +++ b/kafka-ui-react-app/src/components/contexts/ThemeModeContext.tsx @@ -0,0 +1,58 @@ +import React, { useMemo } from 'react'; +import type { FC, PropsWithChildren } from 'react'; +import type { ThemeDropDownValue } from 'components/NavBar/NavBar'; + +interface ThemeModeContextProps { + isDarkMode: boolean; + themeMode: ThemeDropDownValue; + setThemeMode: (value: string | number) => void; +} + +export const ThemeModeContext = React.createContext({ + isDarkMode: false, + themeMode: 'auto_theme', + setThemeMode: () => {}, +}); + +export const ThemeModeProvider: FC> = ({ + children, +}) => { + const matchDark = window.matchMedia('(prefers-color-scheme: dark)'); + const [themeMode, setThemeModeState] = + React.useState('auto_theme'); + + React.useLayoutEffect(() => { + const mode = localStorage.getItem('mode'); + setThemeModeState((mode as ThemeDropDownValue) ?? 'auto_theme'); + }, [setThemeModeState]); + + const isDarkMode = React.useMemo(() => { + if (themeMode === 'auto_theme') { + return matchDark.matches; + } + return themeMode === 'dark_theme'; + }, [themeMode]); + + const setThemeMode = React.useCallback( + (value: string | number) => { + setThemeModeState(value as ThemeDropDownValue); + localStorage.setItem('mode', value as string); + }, + [setThemeModeState] + ); + + const contextValue = useMemo( + () => ({ + isDarkMode, + themeMode, + setThemeMode, + }), + [isDarkMode, themeMode, setThemeMode] + ); + + return ( + + {children} + + ); +}; diff --git a/kafka-ui-react-app/src/components/contexts/TopicMessagesContext.ts b/kafka-ui-react-app/src/components/contexts/TopicMessagesContext.ts index 642052b27e8..3ca2ca65521 100644 --- a/kafka-ui-react-app/src/components/contexts/TopicMessagesContext.ts +++ b/kafka-ui-react-app/src/components/contexts/TopicMessagesContext.ts @@ -3,7 +3,6 @@ import { SeekDirection } from 'generated-sources'; export interface ContextProps { seekDirection: SeekDirection; - searchParams: URLSearchParams; changeSeekDirection(val: string): void; isLive: boolean; } diff --git a/kafka-ui-react-app/src/components/contexts/UserInfoRolesAccessContext.tsx b/kafka-ui-react-app/src/components/contexts/UserInfoRolesAccessContext.tsx new file mode 100644 index 00000000000..075b17a38a6 --- /dev/null +++ b/kafka-ui-react-app/src/components/contexts/UserInfoRolesAccessContext.tsx @@ -0,0 +1,39 @@ +import React, { useMemo } from 'react'; +import { useGetUserInfo } from 'lib/hooks/api/roles'; +import { modifyRolesData, RolesModifiedTypes } from 'lib/permissions'; + +export interface UserInfoType { + username: string; + roles: RolesModifiedTypes; + rbacFlag: boolean; +} + +export const UserInfoRolesAccessContext = React.createContext({ + username: '', + roles: new Map() as RolesModifiedTypes, + rbacFlag: true, +}); + +export const UserInfoRolesAccessProvider: React.FC< + React.PropsWithChildren +> = ({ children }) => { + const { data } = useGetUserInfo(); + + const contextValue = useMemo(() => { + const username = data?.userInfo?.username ? data?.userInfo?.username : ''; + + const roles = modifyRolesData(data?.userInfo?.permissions); + + return { + username, + rbacFlag: !!data?.rbacEnabled, + roles, + }; + }, [data]); + + return ( + + {children} + + ); +}; diff --git a/kafka-ui-react-app/src/components/globalCss.ts b/kafka-ui-react-app/src/components/globalCss.ts new file mode 100644 index 00000000000..ebe1e215856 --- /dev/null +++ b/kafka-ui-react-app/src/components/globalCss.ts @@ -0,0 +1,114 @@ +import { createGlobalStyle, css } from 'styled-components'; + +export default createGlobalStyle( + ({ theme }) => css` + html { + font-family: 'Inter', sans-serif; + font-size: 14px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-color: ${theme.default.backgroundColor}; + overflow-x: hidden; + overflow-y: scroll; + text-rendering: optimizeLegibility; + text-size-adjust: 100%; + min-width: 300px; + } + + #root, + body { + width: 100%; + position: relative; + margin: 0; + font-family: 'Inter', sans-serif; + font-size: 14px; + font-weight: 400; + line-height: 20px; + } + + article, + aside, + figure, + footer, + header, + hgroup, + section { + display: block; + } + + body, + button, + input, + optgroup, + select, + textarea { + font-family: inherit; + } + + code, + pre { + font-family: 'Roboto Mono', sans-serif; + -moz-osx-font-smoothing: auto; + -webkit-font-smoothing: auto; + background-color: ${theme.code.backgroundColor}; + color: ${theme.code.color}; + font-size: 12px; + font-weight: 400; + padding: 2px 8px; + border-radius: 5px; + width: fit-content; + } + + pre { + overflow-x: auto; + white-space: pre; + word-wrap: normal; + + code { + background-color: transparent; + color: currentColor; + padding: 0; + } + } + + a { + color: ${theme.link.color}; + cursor: pointer; + text-decoration: none; + &:hover { + color: ${theme.link.hoverColor}; + } + } + + img { + height: auto; + max-width: 100%; + } + + input[type='checkbox'], + input[type='radio'] { + vertical-align: baseline; + } + + hr { + background-color: ${theme.hr.backgroundColor}; + border: none; + display: block; + height: 1px; + margin: 0; + } + + fieldset { + border: none; + } + + @keyframes fadein { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + ` +); diff --git a/kafka-ui-react-app/src/custom.d.ts b/kafka-ui-react-app/src/custom.d.ts deleted file mode 100644 index 9c2d0f2d6ea..00000000000 --- a/kafka-ui-react-app/src/custom.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -type Dictionary = Record; -type IdType = string | number; diff --git a/kafka-ui-react-app/src/index.tsx b/kafka-ui-react-app/src/index.tsx index 7c28aad2217..5ca52f75b8a 100644 --- a/kafka-ui-react-app/src/index.tsx +++ b/kafka-ui-react-app/src/index.tsx @@ -2,11 +2,11 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; -import * as serviceWorker from 'serviceWorker'; +import { ThemeModeProvider } from 'components/contexts/ThemeModeContext'; import App from 'components/App'; import { store } from 'redux/store'; -import 'theme/index.scss'; import 'lib/constants'; +import 'theme/index.scss'; const container = document.getElementById('root') || document.createElement('div'); @@ -15,12 +15,9 @@ const root = createRoot(container); root.render( - + + + ); - -// If you want your app to work offline and load faster, you can change -// unregister() to register() below. Note this comes with some pitfalls. -// Learn more about service workers: https://bit.ly/CRA-PWA -serviceWorker.unregister(); diff --git a/kafka-ui-react-app/src/lib/__test__/dateTimeHelpers.spec.ts b/kafka-ui-react-app/src/lib/__test__/dateTimeHelpers.spec.ts new file mode 100644 index 00000000000..c34371452b6 --- /dev/null +++ b/kafka-ui-react-app/src/lib/__test__/dateTimeHelpers.spec.ts @@ -0,0 +1,51 @@ +import { + passedTime, + calculateTimer, + formatMilliseconds, +} from 'lib/dateTimeHelpers'; + +const startedAt = 1664891890889; + +describe('format Milliseconds', () => { + it('hours > 0', () => { + const result = formatMilliseconds(10000000); + + expect(result).toEqual('2h 46m'); + }); + it('minutes > 0', () => { + const result = formatMilliseconds(1000000); + + expect(result).toEqual('16m 40s'); + }); + + it('seconds > 0', () => { + const result = formatMilliseconds(10000); + + expect(result).toEqual('10s'); + }); + + it('milliseconds > 0', () => { + const result = formatMilliseconds(100); + + expect(result).toEqual('100ms' || '0ms'); + expect(formatMilliseconds()).toEqual('0ms'); + }); +}); + +describe('calculate timer', () => { + it('time value < 10', () => { + expect(passedTime(5)).toBeTruthy(); + }); + + it('time value > 9', () => { + expect(passedTime(10)).toBeTruthy(); + }); + + it('run calculate time', () => { + expect(calculateTimer(startedAt)); + }); + + it('return when startedAt > new Date()', () => { + expect(calculateTimer(1664891890889199)).toBe('00:00'); + }); +}); diff --git a/kafka-ui-react-app/src/lib/__test__/paths.spec.ts b/kafka-ui-react-app/src/lib/__test__/paths.spec.ts index 1bebb66f562..c6abb770742 100644 --- a/kafka-ui-react-app/src/lib/__test__/paths.spec.ts +++ b/kafka-ui-react-app/src/lib/__test__/paths.spec.ts @@ -5,7 +5,10 @@ import { RouteParams } from 'lib/paths'; const clusterName = 'test-cluster-name'; const groupId = 'test-group-id'; const schemaId = 'test-schema-id'; +const schemaIdWithNonAsciiChars = 'test/test'; +const schemaIdWithNonAsciiCharsEncoded = 'test%2Ftest'; const topicId = 'test-topic-id'; +const brokerId = 'test-Broker-id'; const connectName = 'test-connect-name'; const connectorName = 'test-connector-name'; @@ -15,6 +18,10 @@ describe('Paths', () => { `${GIT_REPO_LINK}/commit/1234567gh` ); }); + it('getNonExactPath', () => { + expect(paths.getNonExactPath('')).toEqual('/*'); + expect(paths.getNonExactPath('/clusters')).toEqual('/clusters/*'); + }); it('clusterPath', () => { expect(paths.clusterPath(clusterName)).toEqual( `/ui/clusters/${clusterName}` @@ -30,6 +37,23 @@ describe('Paths', () => { expect(paths.clusterBrokersPath()).toEqual( paths.clusterBrokersPath(RouteParams.clusterName) ); + + expect(paths.clusterBrokerPath(clusterName, brokerId)).toEqual( + `${paths.clusterPath(clusterName)}/brokers/${brokerId}` + ); + expect(paths.clusterBrokerPath()).toEqual( + paths.clusterBrokerPath(RouteParams.clusterName, RouteParams.brokerId) + ); + + expect(paths.clusterBrokerMetricsPath(clusterName, brokerId)).toEqual( + `${paths.clusterPath(clusterName)}/brokers/${brokerId}/metrics` + ); + expect(paths.clusterBrokerMetricsPath()).toEqual( + paths.clusterBrokerMetricsPath( + RouteParams.clusterName, + RouteParams.brokerId + ) + ); }); it('clusterConsumerGroupsPath', () => { expect(paths.clusterConsumerGroupsPath(clusterName)).toEqual( @@ -90,6 +114,13 @@ describe('Paths', () => { expect(paths.clusterSchemaPath()).toEqual( paths.clusterSchemaPath(RouteParams.clusterName, RouteParams.subject) ); + expect( + paths.clusterSchemaPath(clusterName, schemaIdWithNonAsciiChars) + ).toEqual( + `${paths.clusterSchemasPath( + clusterName + )}/${schemaIdWithNonAsciiCharsEncoded}` + ); }); it('clusterSchemaEditPath', () => { expect(paths.clusterSchemaEditPath(clusterName, schemaId)).toEqual( @@ -98,11 +129,27 @@ describe('Paths', () => { expect(paths.clusterSchemaEditPath()).toEqual( paths.clusterSchemaEditPath(RouteParams.clusterName, RouteParams.subject) ); + expect( + paths.clusterSchemaEditPath(clusterName, schemaIdWithNonAsciiChars) + ).toEqual( + `${paths.clusterSchemaPath(clusterName, schemaIdWithNonAsciiChars)}/edit` + ); + }); + it('clusterSchemaComparePath', () => { + expect(paths.clusterSchemaComparePath(clusterName, schemaId)).toEqual( + `${paths.clusterSchemaPath(clusterName, schemaId)}/compare` + ); + expect(paths.clusterSchemaComparePath()).toEqual( + paths.clusterSchemaComparePath( + RouteParams.clusterName, + RouteParams.subject + ) + ); }); it('clusterTopicsPath', () => { expect(paths.clusterTopicsPath(clusterName)).toEqual( - `${paths.clusterPath(clusterName)}/topics` + `${paths.clusterPath(clusterName)}/all-topics` ); expect(paths.clusterTopicsPath()).toEqual( paths.clusterTopicsPath(RouteParams.clusterName) @@ -110,7 +157,7 @@ describe('Paths', () => { }); it('clusterTopicNewPath', () => { expect(paths.clusterTopicNewPath(clusterName)).toEqual( - `${paths.clusterTopicsPath(clusterName)}/create-new` + `${paths.clusterTopicsPath(clusterName)}/create-new-topic` ); expect(paths.clusterTopicNewPath()).toEqual( paths.clusterTopicNewPath(RouteParams.clusterName) @@ -157,17 +204,6 @@ describe('Paths', () => { ) ); }); - it('clusterTopicSendMessagePath', () => { - expect(paths.clusterTopicSendMessagePath(clusterName, topicId)).toEqual( - `${paths.clusterTopicPath(clusterName, topicId)}/message` - ); - expect(paths.clusterTopicSendMessagePath()).toEqual( - paths.clusterTopicSendMessagePath( - RouteParams.clusterName, - RouteParams.topicName - ) - ); - }); it('clusterTopicEditPath', () => { expect(paths.clusterTopicEditPath(clusterName, topicId)).toEqual( `${paths.clusterTopicPath(clusterName, topicId)}/edit` @@ -176,6 +212,25 @@ describe('Paths', () => { paths.clusterTopicEditPath(RouteParams.clusterName, RouteParams.topicName) ); }); + it('clusterTopicCopyPath', () => { + expect(paths.clusterTopicCopyPath(clusterName)).toEqual( + `${paths.clusterTopicsPath(clusterName)}/copy` + ); + expect(paths.clusterTopicCopyPath()).toEqual( + paths.clusterTopicCopyPath(RouteParams.clusterName) + ); + }); + it('clusterTopicStatisticsPath', () => { + expect(paths.clusterTopicStatisticsPath(clusterName, topicId)).toEqual( + `${paths.clusterTopicPath(clusterName, topicId)}/statistics` + ); + expect(paths.clusterTopicStatisticsPath()).toEqual( + paths.clusterTopicStatisticsPath( + RouteParams.clusterName, + RouteParams.topicName + ) + ); + }); it('clusterConnectsPath', () => { expect(paths.clusterConnectsPath(clusterName)).toEqual( @@ -313,4 +368,20 @@ describe('Paths', () => { paths.clusterKsqlDbQueryPath(RouteParams.clusterName) ); }); + it('clusterKsqlDbTablesPath', () => { + expect(paths.clusterKsqlDbTablesPath(clusterName)).toEqual( + `${paths.clusterKsqlDbPath(clusterName)}/tables` + ); + expect(paths.clusterKsqlDbTablesPath()).toEqual( + paths.clusterKsqlDbTablesPath(RouteParams.clusterName) + ); + }); + it('clusterKsqlDbStreamsPath', () => { + expect(paths.clusterKsqlDbStreamsPath(clusterName)).toEqual( + `${paths.clusterKsqlDbPath(clusterName)}/streams` + ); + expect(paths.clusterKsqlDbStreamsPath()).toEqual( + paths.clusterKsqlDbStreamsPath(RouteParams.clusterName) + ); + }); }); diff --git a/kafka-ui-react-app/src/lib/__test__/permission.spec.ts b/kafka-ui-react-app/src/lib/__test__/permission.spec.ts new file mode 100644 index 00000000000..eb790777db8 --- /dev/null +++ b/kafka-ui-react-app/src/lib/__test__/permission.spec.ts @@ -0,0 +1,630 @@ +import { + isPermitted, + isPermittedToCreate, + modifyRolesData, +} from 'lib/permissions'; +import { Action, ResourceType } from 'generated-sources'; + +describe('Permission Helpers', () => { + const clusterName1 = 'local'; + const clusterName2 = 'dev'; + + const userPermissionsMock = [ + { + clusters: [clusterName1], + resource: ResourceType.TOPIC, + actions: [Action.VIEW, Action.CREATE], + value: '.*', + }, + { + clusters: [clusterName1], + resource: ResourceType.KSQL, + actions: [Action.EXECUTE], + }, + { + clusters: [clusterName1, clusterName2], + resource: ResourceType.SCHEMA, + actions: [Action.VIEW], + value: '.*', + }, + { + clusters: [clusterName1, clusterName2], + resource: ResourceType.CONNECT, + actions: [Action.VIEW], + value: '.*', + }, + { + clusters: [clusterName1], + resource: ResourceType.APPLICATIONCONFIG, + actions: [Action.EDIT], + }, + { + clusters: [clusterName1], + resource: ResourceType.CLUSTERCONFIG, + actions: [Action.EDIT], + }, + { + clusters: [clusterName1], + resource: ResourceType.CONSUMER, + actions: [Action.DELETE], + value: '.*', + }, + { + clusters: [clusterName1], + resource: ResourceType.SCHEMA, + actions: [Action.EDIT, Action.DELETE, Action.CREATE], + value: '123.*', + }, + { + clusters: [clusterName1], + resource: ResourceType.ACL, + actions: [Action.VIEW], + }, + { + clusters: [clusterName1], + resource: ResourceType.AUDIT, + actions: [Action.VIEW], + }, + { + clusters: [clusterName1, clusterName2], + resource: ResourceType.TOPIC, + value: 'test.*', + actions: [Action.MESSAGES_DELETE], + }, + { + clusters: [clusterName1, clusterName2], + resource: ResourceType.TOPIC, + value: '.*', + actions: [Action.EDIT, Action.DELETE], + }, + { + clusters: [clusterName1, clusterName2], + resource: ResourceType.TOPIC, + value: 'bobross.*', + actions: [Action.VIEW, Action.MESSAGES_READ], + }, + ]; + + const roles = modifyRolesData(userPermissionsMock); + + describe('modifyRoles', () => { + it('should check if it transforms the data in a correct format to normal keys', () => { + const result = modifyRolesData(userPermissionsMock); + expect(result.keys()).toContain(clusterName1); + expect(result.keys()).toContain(clusterName2); + + const cluster1Map = result.get(clusterName1); + const cluster2Map = result.get(clusterName2); + + expect(cluster1Map).toBeDefined(); + expect(cluster2Map).toBeDefined(); + + // first cluster + expect(cluster1Map?.has(ResourceType.CLUSTERCONFIG)).toBeTruthy(); + expect(cluster1Map?.has(ResourceType.CLUSTERCONFIG)).toBeTruthy(); + expect(cluster1Map?.has(ResourceType.CONSUMER)).toBeTruthy(); + expect(cluster1Map?.has(ResourceType.CONNECT)).toBeTruthy(); + expect(cluster1Map?.has(ResourceType.KSQL)).toBeTruthy(); + expect(cluster1Map?.has(ResourceType.TOPIC)).toBeTruthy(); + + // second cluster + expect(cluster2Map?.has(ResourceType.SCHEMA)).toBeTruthy(); + expect(cluster2Map?.has(ResourceType.CONNECT)).toBeTruthy(); + expect(cluster2Map?.has(ResourceType.TOPIC)).toBeTruthy(); + expect(cluster2Map?.has(ResourceType.CLUSTERCONFIG)).toBeFalsy(); + + expect(cluster2Map?.has(ResourceType.CONSUMER)).toBeFalsy(); + expect(cluster2Map?.has(ResourceType.KSQL)).toBeFalsy(); + }); + + it('should check if it transforms the data length in keys are correct', () => { + const result = modifyRolesData(userPermissionsMock); + + const cluster1Map = result.get(clusterName1); + const cluster2Map = result.get(clusterName2); + + expect(result.size).toBe(2); + + expect(cluster1Map?.size).toBe(9); + expect(cluster2Map?.size).toBe(3); + + // clusterMap1 + expect(cluster1Map?.get(ResourceType.TOPIC)).toHaveLength(4); + expect(cluster1Map?.get(ResourceType.SCHEMA)).toHaveLength(2); + expect(cluster1Map?.get(ResourceType.CONSUMER)).toHaveLength(1); + expect(cluster1Map?.get(ResourceType.CLUSTERCONFIG)).toHaveLength(1); + expect(cluster1Map?.get(ResourceType.CONNECT)).toHaveLength(1); + expect(cluster1Map?.get(ResourceType.CLUSTERCONFIG)).toHaveLength(1); + + // clusterMap2 + expect(cluster2Map?.get(ResourceType.SCHEMA)).toHaveLength(1); + }); + }); + + describe('isPermitted', () => { + it('should check if the isPermitted returns the correct when there is no roles or clusters', () => { + expect( + isPermitted({ + clusterName: clusterName1, + resource: ResourceType.TOPIC, + action: Action.VIEW, + rbacFlag: true, + }) + ).toBeFalsy(); + + expect( + isPermitted({ + clusterName: 'unFoundCluster', + resource: ResourceType.TOPIC, + action: Action.VIEW, + rbacFlag: true, + }) + ).toBeFalsy(); + + expect( + isPermitted({ + roles, + clusterName: 'unFoundCluster', + resource: ResourceType.TOPIC, + action: Action.VIEW, + rbacFlag: true, + }) + ).toBeFalsy(); + + expect( + isPermitted({ + roles, + clusterName: '', + resource: ResourceType.TOPIC, + action: Action.VIEW, + rbacFlag: true, + }) + ).toBeFalsy(); + + expect( + isPermitted({ + roles: new Map(), + clusterName: 'unFoundCluster', + resource: ResourceType.TOPIC, + action: Action.VIEW, + rbacFlag: true, + }) + ).toBeFalsy(); + + expect( + isPermitted({ + roles: new Map(), + clusterName: clusterName1, + resource: ResourceType.TOPIC, + action: Action.VIEW, + rbacFlag: true, + }) + ).toBeFalsy(); + }); + + it('should check if the isPermitted returns the correct value without resource values (exempt list)', () => { + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.KSQL, + action: Action.EXECUTE, + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.CLUSTERCONFIG, + action: Action.EDIT, + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.APPLICATIONCONFIG, + action: Action.EDIT, + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.ACL, + action: Action.VIEW, + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.AUDIT, + action: Action.VIEW, + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.TOPIC, + action: Action.VIEW, + rbacFlag: true, + }) + ).toBeFalsy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.SCHEMA, + action: Action.VIEW, + rbacFlag: true, + }) + ).toBeFalsy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.CONSUMER, + action: Action.VIEW, + rbacFlag: true, + }) + ).toBeFalsy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.CONNECT, + action: Action.VIEW, + rbacFlag: true, + }) + ).toBeFalsy(); + }); + + it('should check if the isPermitted returns the correct value with name values', () => { + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.SCHEMA, + action: Action.EDIT, + value: '123456', + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.SCHEMA, + action: Action.EDIT, + value: '123', + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.SCHEMA, + action: Action.EDIT, + value: 'some_wrong_value', + rbacFlag: true, + }) + ).toBeFalsy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName2, + resource: ResourceType.TOPIC, + action: Action.MESSAGES_DELETE, + value: 'test_something', + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.TOPIC, + action: Action.MESSAGES_DELETE, + value: 'test_something', + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName2, + resource: ResourceType.TOPIC, + action: Action.EDIT, + value: 'any_text', + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName2, + resource: ResourceType.TOPIC, + action: Action.EDIT, + value: 'any_text', + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.TOPIC, + action: Action.DELETE, + value: 'some_other', + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName2, + resource: ResourceType.TOPIC, + action: Action.DELETE, + value: 'some_other', + rbacFlag: true, + }) + ).toBeTruthy(); + }); + + it('should test the algorithmic worse case when the input is multiple actions', () => { + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.SCHEMA, + action: [Action.EDIT, Action.DELETE], + value: '123456', + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.SCHEMA, + action: [Action.EDIT], + value: '123456', + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.SCHEMA, + action: [Action.EDIT], + value: '123456', + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.SCHEMA, + action: [Action.DELETE], + value: '123456', + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.SCHEMA, + action: [Action.DELETE, Action.EDIT], + value: '123456', + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.SCHEMA, + action: [Action.EDIT, Action.VIEW], + value: '123456', + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.SCHEMA, + action: [Action.EDIT, Action.VIEW], + value: 'notFound', + rbacFlag: true, + }) + ).toBeFalsy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.SCHEMA, + action: [], + value: '123456', + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.TOPIC, + action: [Action.MESSAGES_READ], + value: 'bobross-test', + rbacFlag: true, + }) + ).toBeTruthy(); + }); + + it('should check the rbac flag and works with permissions accordingly', () => { + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.SCHEMA, + action: [], + value: '123456', + rbacFlag: false, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.SCHEMA, + action: [Action.EDIT, Action.VIEW], + value: '123456', + rbacFlag: false, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles, + clusterName: clusterName1, + resource: ResourceType.SCHEMA, + action: [Action.EDIT, Action.VIEW], + value: 'notFound', + rbacFlag: false, + }) + ).toBeTruthy(); + + expect( + isPermitted({ + roles: new Map(), + clusterName: clusterName1, + resource: ResourceType.SCHEMA, + action: [Action.EDIT, Action.VIEW], + value: 'notFound', + rbacFlag: false, + }) + ).toBeTruthy(); + }); + }); + + describe('isPermittedToCreate', () => { + it('should check if the isPermitted returns the correct when there is no roles or clusters', () => { + expect( + isPermittedToCreate({ + roles, + clusterName: clusterName1, + resource: ResourceType.TOPIC, + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermittedToCreate({ + roles, + clusterName: clusterName2, + resource: ResourceType.TOPIC, + rbacFlag: true, + }) + ).toBeFalsy(); + + expect( + isPermittedToCreate({ + roles, + clusterName: clusterName1, + resource: ResourceType.TOPIC, + rbacFlag: false, + }) + ).toBeTruthy(); + + expect( + isPermittedToCreate({ + roles, + clusterName: clusterName2, + resource: ResourceType.TOPIC, + rbacFlag: false, + }) + ).toBeTruthy(); + + expect( + isPermittedToCreate({ + roles, + clusterName: clusterName1, + resource: ResourceType.SCHEMA, + rbacFlag: true, + }) + ).toBeTruthy(); + + expect( + isPermittedToCreate({ + roles, + clusterName: clusterName1, + resource: ResourceType.CONNECT, + rbacFlag: true, + }) + ).toBeFalsy(); + + expect( + isPermittedToCreate({ + roles: new Map(), + clusterName: 'unFoundCluster', + resource: ResourceType.TOPIC, + rbacFlag: true, + }) + ).toBeFalsy(); + + expect( + isPermittedToCreate({ + roles, + clusterName: 'unFoundCluster', + resource: ResourceType.TOPIC, + rbacFlag: true, + }) + ).toBeFalsy(); + + expect( + isPermittedToCreate({ + roles: new Map(), + clusterName: clusterName1, + resource: ResourceType.TOPIC, + rbacFlag: true, + }) + ).toBeFalsy(); + }); + }); +}); diff --git a/kafka-ui-react-app/src/lib/__test__/propertyLookup.spec.ts b/kafka-ui-react-app/src/lib/__test__/propertyLookup.spec.ts deleted file mode 100644 index 45f89b0a08f..00000000000 --- a/kafka-ui-react-app/src/lib/__test__/propertyLookup.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { propertyLookup } from 'lib/propertyLookup'; - -describe('Property Lookup', () => { - const entityObject = { - prop: { - nestedProp: 1, - }, - }; - it('returns undefined if property not found', () => { - expect( - propertyLookup('prop.nonExistingProp', entityObject) - ).toBeUndefined(); - }); - - it('returns value of nested property if it exists', () => { - expect(propertyLookup('prop.nestedProp', entityObject)).toBe(1); - }); -}); diff --git a/kafka-ui-react-app/src/lib/api.ts b/kafka-ui-react-app/src/lib/api.ts new file mode 100644 index 00000000000..19423d2ac3e --- /dev/null +++ b/kafka-ui-react-app/src/lib/api.ts @@ -0,0 +1,29 @@ +import { + KsqlApi, + TopicsApi, + SchemasApi, + BrokersApi, + MessagesApi, + ClustersApi, + Configuration, + KafkaConnectApi, + ConsumerGroupsApi, + AuthorizationApi, + ApplicationConfigApi, + AclsApi, +} from 'generated-sources'; +import { BASE_PARAMS } from 'lib/constants'; + +const apiClientConf = new Configuration(BASE_PARAMS); + +export const ksqlDbApiClient = new KsqlApi(apiClientConf); +export const topicsApiClient = new TopicsApi(apiClientConf); +export const brokersApiClient = new BrokersApi(apiClientConf); +export const schemasApiClient = new SchemasApi(apiClientConf); +export const messagesApiClient = new MessagesApi(apiClientConf); +export const clustersApiClient = new ClustersApi(apiClientConf); +export const kafkaConnectApiClient = new KafkaConnectApi(apiClientConf); +export const consumerGroupsApiClient = new ConsumerGroupsApi(apiClientConf); +export const authApiClient = new AuthorizationApi(apiClientConf); +export const appConfigApiClient = new ApplicationConfigApi(apiClientConf); +export const aclApiClient = new AclsApi(apiClientConf); diff --git a/kafka-ui-react-app/src/lib/constants.ts b/kafka-ui-react-app/src/lib/constants.ts index 37aea99b518..a3e622550b8 100644 --- a/kafka-ui-react-app/src/lib/constants.ts +++ b/kafka-ui-react-app/src/lib/constants.ts @@ -1,5 +1,5 @@ -import { ConfigurationParameters } from 'generated-sources'; -import { BreadcrumbDefinitions } from 'components/common/Breadcrumb/Breadcrumb'; +import { SelectOption } from 'components/common/Select/Select'; +import { ConfigurationParameters, ConsumerGroupState } from 'generated-sources'; declare global { interface Window { @@ -15,7 +15,7 @@ export const BASE_PARAMS: ConfigurationParameters = { }, }; -export const TOPIC_NAME_VALIDATION_PATTERN = /^[.,A-Za-z0-9_-]+$/; +export const TOPIC_NAME_VALIDATION_PATTERN = /^[a-zA-Z0-9._-]+$/; export const SCHEMA_NAME_VALIDATION_PATTERN = /^[.,A-Za-z0-9_/-]+$/; export const TOPIC_CUSTOM_PARAMS_PREFIX = 'customParams'; @@ -50,18 +50,16 @@ export const MILLISECONDS_IN_SECOND = 1_000; export const NOT_SET = -1; export const BYTES_IN_GB = 1_073_741_824; +export const BUILD_VERSION_PATTERN = /v\d.\d.\d/; export const PER_PAGE = 25; +export const MESSAGES_PER_PAGE = '100'; export const GIT_REPO_LINK = 'https://github.com/provectus/kafka-ui'; export const GIT_REPO_LATEST_RELEASE_LINK = 'https://api.github.com/repos/provectus/kafka-ui/releases/latest'; -export const GIT_TAG = process.env.REACT_APP_TAG; -export const GIT_COMMIT = process.env.REACT_APP_COMMIT; -export const BREADCRUMB_DEFINITIONS: BreadcrumbDefinitions = { - Ksqldb: 'ksqlDB', -}; +export const LOCAL_STORAGE_KEY_PREFIX = 'kafka-ui'; export enum AsyncRequestStatus { initial = 'initial', @@ -69,3 +67,43 @@ export enum AsyncRequestStatus { fulfilled = 'fulfilled', rejected = 'rejected', } + +export const QUERY_REFETCH_OFF_OPTIONS = { + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchIntervalInBackground: false, +}; + +// Cluster Form Constants +export const AUTH_OPTIONS: SelectOption[] = [ + { value: 'SASL/JAAS', label: 'SASL/JAAS' }, + { value: 'SASL/GSSAPI', label: 'SASL/GSSAPI' }, + { value: 'SASL/OAUTHBEARER', label: 'SASL/OAUTHBEARER' }, + { value: 'SASL/PLAIN', label: 'SASL/PLAIN' }, + { value: 'SASL/SCRAM-256', label: 'SASL/SCRAM-256' }, + { value: 'SASL/SCRAM-512', label: 'SASL/SCRAM-512' }, + { value: 'Delegation tokens', label: 'Delegation tokens' }, + { value: 'SASL/LDAP', label: 'SASL/LDAP' }, + { value: 'SASL/AWS IAM', label: 'SASL/AWS IAM' }, + { value: 'mTLS', label: 'mTLS' }, +]; + +export const SECURITY_PROTOCOL_OPTIONS: SelectOption[] = [ + { value: 'SASL_SSL', label: 'SASL_SSL' }, + { value: 'SASL_PLAINTEXT', label: 'SASL_PLAINTEXT' }, +]; +export const METRICS_OPTIONS: SelectOption[] = [ + { value: 'JMX', label: 'JMX' }, + { value: 'PROMETHEUS', label: 'PROMETHEUS' }, +]; + +export const CONSUMER_GROUP_STATE_TOOLTIPS: Record = + { + EMPTY: 'The group exists but has no members.', + STABLE: 'Consumers are happily consuming and have assigned partitions.', + PREPARING_REBALANCE: + 'Something has changed, and the reassignment of partitions is required.', + COMPLETING_REBALANCE: 'Partition reassignment is in progress.', + DEAD: 'The group is going to be removed. It might be due to the inactivity, or the group is being migrated to different group coordinator.', + UNKNOWN: '', + } as const; diff --git a/kafka-ui-react-app/src/lib/dateTimeHelpers.ts b/kafka-ui-react-app/src/lib/dateTimeHelpers.ts new file mode 100644 index 00000000000..148a70d2a3d --- /dev/null +++ b/kafka-ui-react-app/src/lib/dateTimeHelpers.ts @@ -0,0 +1,53 @@ +export const formatTimestamp = ( + timestamp?: number | string | Date, + format: Intl.DateTimeFormatOptions = { hourCycle: 'h23' } +): string => { + if (!timestamp) { + return ''; + } + + // empty array gets the default one from the browser + const date = new Date(timestamp); + // invalid date + if (Number.isNaN(date.getTime())) { + return ''; + } + + // browser support + const language = navigator.language || navigator.languages[0]; + return date.toLocaleString(language || [], format); +}; + +export const formatMilliseconds = (input = 0) => { + const milliseconds = Math.max(input || 0, 0); + + const seconds = Math.floor(milliseconds / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + if (hours > 0) { + return `${hours}h ${minutes % 60}m`; + } + + if (minutes > 0) { + return `${minutes}m ${seconds % 60}s`; + } + + if (seconds > 0) { + return `${seconds}s`; + } + + return `${milliseconds}ms`; +}; + +export const passedTime = (value: number) => (value < 10 ? `0${value}` : value); + +export const calculateTimer = (startedAt: number) => { + const nowDate = new Date(); + const now = nowDate.getTime(); + const newDate = now - startedAt; + const minutes = nowDate.getMinutes(); + const second = nowDate.getSeconds(); + + return newDate > 0 ? `${passedTime(minutes)}:${passedTime(second)}` : '00:00'; +}; diff --git a/kafka-ui-react-app/src/lib/errorHandling.ts b/kafka-ui-react-app/src/lib/errorHandling.ts deleted file mode 100644 index 1240a666c73..00000000000 --- a/kafka-ui-react-app/src/lib/errorHandling.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ServerResponse } from 'redux/interfaces'; - -export const getResponse = async ( - response: Response -): Promise => { - let body; - try { - body = await response.json(); - } catch (e) { - // do nothing; - } - return { - status: response.status, - statusText: response.statusText, - url: response.url, - message: body?.message, - }; -}; diff --git a/kafka-ui-react-app/src/lib/errorHandling.tsx b/kafka-ui-react-app/src/lib/errorHandling.tsx new file mode 100644 index 00000000000..4079fd8ac50 --- /dev/null +++ b/kafka-ui-react-app/src/lib/errorHandling.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import Alert from 'components/common/Alert/Alert'; +import toast, { ToastType } from 'react-hot-toast'; +import { ErrorResponse } from 'generated-sources'; + +interface ServerResponse { + status: number; + statusText: string; + url?: string; + message?: ErrorResponse['message']; +} +export type ToastTypes = ToastType | 'warning'; + +export const getResponse = async ( + response: Response +): Promise => { + let body; + try { + body = await response.json(); + } catch (e) { + // do nothing; + } + return { + status: response.status, + statusText: response.statusText, + url: response.url, + message: body?.message, + }; +}; + +interface AlertOptions { + id?: string; + title?: string; + message: React.ReactNode; +} + +export const showAlert = ( + type: ToastTypes, + { title, message, id }: AlertOptions +) => { + toast.custom( + (t) => ( + toast.remove(t.id)} + /> + ), + { id } + ); +}; + +export const showSuccessAlert = (options: AlertOptions) => { + showAlert('success', { + ...options, + title: options.title || 'Success', + }); +}; + +export const showServerError = async ( + response: Response, + options?: AlertOptions +) => { + let body: Record = {}; + try { + body = await response.json(); + } catch (e) { + // do nothing; + } + if (response.status) { + showAlert('error', { + id: response.url, + title: `${response.status} ${response.statusText}`, + message: body?.message || 'An error occurred', + ...options, + }); + } else { + showAlert('error', { + id: 'server-error', + title: `Something went wrong`, + message: 'An error occurred', + ...options, + }); + } +}; diff --git a/kafka-ui-react-app/src/lib/fixtures/acls.ts b/kafka-ui-react-app/src/lib/fixtures/acls.ts new file mode 100644 index 00000000000..3eecf7cea84 --- /dev/null +++ b/kafka-ui-react-app/src/lib/fixtures/acls.ts @@ -0,0 +1,37 @@ +import { + KafkaAcl, + KafkaAclResourceType, + KafkaAclNamePatternType, + KafkaAclPermissionEnum, + KafkaAclOperationEnum, +} from 'generated-sources'; + +export const aclPayload: KafkaAcl[] = [ + { + principal: 'User 1', + resourceName: 'Topic', + resourceType: KafkaAclResourceType.TOPIC, + host: '_host1', + namePatternType: KafkaAclNamePatternType.LITERAL, + permission: KafkaAclPermissionEnum.ALLOW, + operation: KafkaAclOperationEnum.READ, + }, + { + principal: 'User 2', + resourceName: 'Topic', + resourceType: KafkaAclResourceType.TOPIC, + host: '_host1', + namePatternType: KafkaAclNamePatternType.PREFIXED, + permission: KafkaAclPermissionEnum.ALLOW, + operation: KafkaAclOperationEnum.READ, + }, + { + principal: 'User 3', + resourceName: 'Topic', + resourceType: KafkaAclResourceType.TOPIC, + host: '_host1', + namePatternType: KafkaAclNamePatternType.LITERAL, + permission: KafkaAclPermissionEnum.DENY, + operation: KafkaAclOperationEnum.READ, + }, +]; diff --git a/kafka-ui-react-app/src/lib/fixtures/brokers.ts b/kafka-ui-react-app/src/lib/fixtures/brokers.ts new file mode 100644 index 00000000000..1a76f73fe37 --- /dev/null +++ b/kafka-ui-react-app/src/lib/fixtures/brokers.ts @@ -0,0 +1,118 @@ +import { BrokerConfig, BrokersLogdirs, ConfigSource } from 'generated-sources'; + +export const brokersPayload = [ + { id: 100, host: 'b-1.test.kafka.amazonaws.com', port: 9092 }, + { id: 200, host: 'b-2.test.kafka.amazonaws.com', port: 9092 }, +]; + +const partition = { + broker: 2, + offsetLag: 0, + partition: 2, + size: 0, +}; +const topics = { + name: '_confluent-ksql-devquery_CTAS_NUMBER_OF_TESTS_59-Aggregate-Aggregate-Materialize-changelog', + partitions: [partition], +}; + +export const brokerLogDirsPayload: BrokersLogdirs[] = [ + { + error: 'NONE', + name: '/opt/kafka/data-0/logs', + topics: [ + { + ...topics, + partitions: [partition, partition, partition], + }, + topics, + { + ...topics, + partitions: [], + }, + ], + }, + { + error: 'NONE', + name: '/opt/kafka/data-1/logs', + }, +]; + +export const brokerConfigPayload: BrokerConfig[] = [ + { + name: 'compression.type', + value: 'producer', + source: ConfigSource.DYNAMIC_BROKER_CONFIG, + isSensitive: false, + isReadOnly: false, + synonyms: [ + { + name: 'compression.type', + value: 'producer', + source: ConfigSource.DYNAMIC_BROKER_CONFIG, + }, + { + name: 'compression.type', + value: 'producer', + source: ConfigSource.DEFAULT_CONFIG, + }, + ], + }, + { + name: 'confluent.value.schema.validation', + value: 'false', + source: ConfigSource.DEFAULT_CONFIG, + isSensitive: false, + isReadOnly: false, + synonyms: [], + }, + { + name: 'leader.replication.throttled.replicas', + value: '', + source: ConfigSource.DEFAULT_CONFIG, + isSensitive: false, + isReadOnly: false, + synonyms: [], + }, + { + name: 'confluent.key.subject.name.strategy', + value: 'io.confluent.kafka.serializers.subject.TopicNameStrategy', + source: ConfigSource.DEFAULT_CONFIG, + isSensitive: false, + isReadOnly: false, + synonyms: [], + }, + { + name: 'message.downconversion.enable', + value: 'true', + source: ConfigSource.DEFAULT_CONFIG, + isSensitive: false, + isReadOnly: false, + synonyms: [ + { + name: 'log.message.downconversion.enable', + value: 'true', + source: ConfigSource.DEFAULT_CONFIG, + }, + ], + }, + { + name: 'min.insync.replicas', + value: '1', + source: ConfigSource.DYNAMIC_BROKER_CONFIG, + isSensitive: false, + isReadOnly: false, + synonyms: [ + { + name: 'min.insync.replicas', + value: '1', + source: ConfigSource.DYNAMIC_BROKER_CONFIG, + }, + { + name: 'min.insync.replicas', + value: '1', + source: ConfigSource.DEFAULT_CONFIG, + }, + ], + }, +]; diff --git a/kafka-ui-react-app/src/lib/fixtures/clusters.ts b/kafka-ui-react-app/src/lib/fixtures/clusters.ts new file mode 100644 index 00000000000..3f4476bae8a --- /dev/null +++ b/kafka-ui-react-app/src/lib/fixtures/clusters.ts @@ -0,0 +1,46 @@ +import { Cluster, ServerStatus } from 'generated-sources'; + +export const onlineClusterPayload: Cluster = { + name: 'secondLocal', + defaultCluster: true, + status: ServerStatus.ONLINE, + brokerCount: 1, + onlinePartitionCount: 6, + topicCount: 3, + bytesInPerSec: 1.55, + bytesOutPerSec: 9.314, + readOnly: false, + features: [], +}; +export const offlineClusterPayload: Cluster = { + name: 'local', + defaultCluster: false, + status: ServerStatus.OFFLINE, + brokerCount: 1, + onlinePartitionCount: 2, + topicCount: 2, + bytesInPerSec: 3.42, + bytesOutPerSec: 4.14, + features: [], + readOnly: true, +}; + +export const clustersPayload: Cluster[] = [ + onlineClusterPayload, + offlineClusterPayload, +]; + +export const clusterStatsPayload = { + brokerCount: 2, + activeControllers: 100, + onlinePartitionCount: 138, + offlinePartitionCount: 0, + inSyncReplicasCount: 239, + outOfSyncReplicasCount: 0, + underReplicatedPartitionCount: 0, + diskUsage: [ + { brokerId: 100, segmentSize: 334567, segmentCount: 245 }, + { brokerId: 200, segmentSize: 12345678, segmentCount: 121 }, + ], + version: '2.2.1', +}; diff --git a/kafka-ui-react-app/src/lib/fixtures/consumerGroups.ts b/kafka-ui-react-app/src/lib/fixtures/consumerGroups.ts new file mode 100644 index 00000000000..d282a65b4e2 --- /dev/null +++ b/kafka-ui-react-app/src/lib/fixtures/consumerGroups.ts @@ -0,0 +1,53 @@ +import { ConsumerGroupState } from 'generated-sources'; + +export const consumerGroupPayload = { + groupId: 'amazon.msk.canary.group.broker-1', + members: 0, + topics: 2, + simple: false, + partitionAssignor: '', + state: ConsumerGroupState.EMPTY, + coordinator: { + id: 2, + host: 'b-2.kad-msk.st2jzq.c6.kafka.eu-west-1.amazonaws.com', + }, + consumerLag: 0, + partitions: [ + { + topic: '__amazon_msk_canary', + partition: 1, + currentOffset: 0, + endOffset: 0, + consumerLag: 0, + consumerId: undefined, + host: undefined, + }, + { + topic: '__amazon_msk_canary', + partition: 0, + currentOffset: 56932, + endOffset: 56932, + consumerLag: 0, + consumerId: undefined, + host: undefined, + }, + { + topic: 'other_topic', + partition: 3, + currentOffset: 56932, + endOffset: 56932, + consumerLag: 0, + consumerId: undefined, + host: undefined, + }, + { + topic: 'other_topic', + partition: 4, + currentOffset: 56932, + endOffset: 56932, + consumerLag: 0, + consumerId: undefined, + host: undefined, + }, + ], +}; diff --git a/kafka-ui-react-app/src/lib/fixtures/kafkaConnect.ts b/kafka-ui-react-app/src/lib/fixtures/kafkaConnect.ts new file mode 100644 index 00000000000..8a79760e661 --- /dev/null +++ b/kafka-ui-react-app/src/lib/fixtures/kafkaConnect.ts @@ -0,0 +1,120 @@ +import { + Connect, + Connector, + ConnectorState, + ConnectorTaskStatus, + ConnectorType, + FullConnectorInfo, + Task, +} from 'generated-sources'; + +export const connects: Connect[] = [ + { name: 'first', address: 'localhost:8083' }, + { name: 'second', address: 'localhost:8084' }, +]; + +export const connectors: FullConnectorInfo[] = [ + { + connect: 'first', + name: 'hdfs-source-connector', + connectorClass: 'FileStreamSource', + type: ConnectorType.SOURCE, + topics: ['a', 'b', 'c'], + status: { + state: ConnectorState.RUNNING, + }, + tasksCount: 2, + failedTasksCount: 0, + }, + { + connect: 'second', + name: 'hdfs2-source-connector', + connectorClass: 'FileStreamSource', + type: ConnectorType.SINK, + topics: ['test-topic'], + status: { + state: ConnectorState.FAILED, + }, + tasksCount: 3, + failedTasksCount: 1, + }, +]; + +export const connector: Connector = { + connect: 'first', + name: 'hdfs-source-connector', + type: ConnectorType.SOURCE, + status: { + state: ConnectorState.RUNNING, + workerId: 'kafka-connect0:8083', + }, + config: { + 'connector.class': 'FileStreamSource', + 'tasks.max': '10', + topic: 'test-topic', + file: '/some/file', + }, + tasks: [{ connector: 'first', task: 1 }], +}; + +export const tasks: Task[] = [ + { + id: { connector: 'first', task: 1 }, + status: { + id: 1, + state: ConnectorTaskStatus.RUNNING, + workerId: 'kafka-connect0:8083', + }, + config: { + 'batch.size': '2000', + file: '/some/file', + 'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask', + topic: 'test-topic', + }, + }, + { + id: { connector: 'first', task: 2 }, + status: { + id: 2, + state: ConnectorTaskStatus.FAILED, + trace: 'Failure 1', + workerId: 'kafka-connect0:8083', + }, + config: { + 'batch.size': '1000', + file: '/some/file2', + 'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask', + topic: 'test-topic', + }, + }, + { + id: { connector: 'first', task: 3 }, + status: { + id: 3, + state: ConnectorTaskStatus.RUNNING, + workerId: 'kafka-connect0:8083', + trace: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + }, + config: { + 'batch.size': '3000', + file: '/some/file3', + 'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask', + topic: 'test-topic', + }, + }, + { + id: { connector: 'first', task: 4 }, + status: { + id: 4, + state: ConnectorTaskStatus.PAUSED, + workerId: 'kafka-connect0:8083', + }, + config: { + 'batch.size': '3000', + file: '/some/file3', + 'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask', + topic: 'test-topic', + }, + }, +]; diff --git a/kafka-ui-react-app/src/lib/fixtures/latestVersion.ts b/kafka-ui-react-app/src/lib/fixtures/latestVersion.ts new file mode 100644 index 00000000000..d1e62da6b80 --- /dev/null +++ b/kafka-ui-react-app/src/lib/fixtures/latestVersion.ts @@ -0,0 +1,16 @@ +export const deprecatedVersionPayload = { + build: { + buildTime: '2023-04-14T09:47:35.463Z', + commitId: '96a577a', + isLatestRelease: false, + version: '96a577a98c6069376c5d22ed49cffd3739f1bbdc', + }, +}; +export const latestVersionPayload = { + build: { + buildTime: '2023-04-14T09:47:35.463Z', + commitId: '96a577a', + isLatestRelease: true, + version: '96a577a98c6069376c5d22ed49cffd3739f1bbdc', + }, +}; diff --git a/kafka-ui-react-app/src/lib/fixtures/topicMessages.ts b/kafka-ui-react-app/src/lib/fixtures/topicMessages.ts new file mode 100644 index 00000000000..3288cc7e825 --- /dev/null +++ b/kafka-ui-react-app/src/lib/fixtures/topicMessages.ts @@ -0,0 +1,38 @@ +import { TopicSerdeSuggestion } from 'generated-sources'; + +export const serdesPayload: TopicSerdeSuggestion = { + key: [ + { + name: 'String', + description: undefined, + preferred: false, + schema: undefined, + additionalProperties: undefined, + }, + { + name: 'Int32', + description: undefined, + preferred: true, + schema: + '{ "type" : "integer", "minimum" : -2147483648, "maximum" : 2147483647 }', + additionalProperties: {}, + }, + ], + value: [ + { + name: 'String', + description: undefined, + preferred: false, + schema: undefined, + additionalProperties: undefined, + }, + { + name: 'Int64', + description: undefined, + preferred: true, + schema: + '{ "type" : "integer", "minimum" : -9223372036854775808, "maximum" : 9223372036854775807 }', + additionalProperties: {}, + }, + ], +}; diff --git a/kafka-ui-react-app/src/lib/fixtures/topics.ts b/kafka-ui-react-app/src/lib/fixtures/topics.ts new file mode 100644 index 00000000000..cff119077a8 --- /dev/null +++ b/kafka-ui-react-app/src/lib/fixtures/topics.ts @@ -0,0 +1,235 @@ +import { + ConfigSource, + ConsumerGroup, + ConsumerGroupState, + Topic, + TopicConfig, + TopicAnalysis, +} from 'generated-sources'; + +export const internalTopicPayload = { + name: '__internal.topic', + internal: true, + partitionCount: 1, + replicationFactor: 1, + replicas: 1, + inSyncReplicas: 1, + segmentSize: 0, + segmentCount: 1, + underReplicatedPartitions: 0, + partitions: [ + { + partition: 0, + leader: 1, + replicas: [{ broker: 1, leader: false, inSync: true }], + offsetMax: 0, + offsetMin: 0, + }, + ], +}; + +export const externalTopicPayload = { + name: 'external.topic', + internal: false, + partitionCount: 1, + replicationFactor: 1, + replicas: 1, + inSyncReplicas: 1, + segmentSize: 1263, + segmentCount: 1, + underReplicatedPartitions: 0, + partitions: [ + { + partition: 0, + leader: 1, + replicas: [{ broker: 1, leader: false, inSync: true }], + offsetMax: 0, + offsetMin: 0, + }, + ], +}; + +export const topicsPayload: Topic[] = [ + internalTopicPayload, + externalTopicPayload, +]; + +export const topicConsumerGroups: ConsumerGroup[] = [ + { + groupId: 'amazon.msk.canary.group.broker-7', + topics: 0, + members: 0, + simple: false, + partitionAssignor: '', + state: ConsumerGroupState.UNKNOWN, + coordinator: { id: 1 }, + consumerLag: 9, + }, + { + groupId: 'amazon.msk.canary.group.broker-4', + topics: 0, + members: 0, + simple: false, + partitionAssignor: '', + state: ConsumerGroupState.COMPLETING_REBALANCE, + coordinator: { id: 1 }, + consumerLag: 9, + }, +]; + +export const topicConfigPayload: TopicConfig[] = [ + { + name: 'compression.type', + value: 'producer', + defaultValue: 'producer', + source: ConfigSource.DYNAMIC_TOPIC_CONFIG, + isSensitive: false, + isReadOnly: false, + synonyms: [ + { + name: 'compression.type', + value: 'producer', + source: ConfigSource.DYNAMIC_TOPIC_CONFIG, + }, + { + name: 'compression.type', + value: 'producer', + source: ConfigSource.DEFAULT_CONFIG, + }, + ], + }, + { + name: 'confluent.value.schema.validation', + value: 'false', + source: ConfigSource.DEFAULT_CONFIG, + isSensitive: false, + isReadOnly: false, + synonyms: [], + }, + { + name: 'leader.replication.throttled.replicas', + value: '', + defaultValue: '', + source: ConfigSource.DEFAULT_CONFIG, + isSensitive: false, + isReadOnly: false, + synonyms: [], + }, + { + name: 'confluent.key.subject.name.strategy', + value: 'io.confluent.kafka.serializers.subject.TopicNameStrategy', + source: ConfigSource.DEFAULT_CONFIG, + isSensitive: false, + isReadOnly: false, + synonyms: [], + }, + { + name: 'message.downconversion.enable', + value: 'true', + defaultValue: 'true', + source: ConfigSource.DEFAULT_CONFIG, + isSensitive: false, + isReadOnly: false, + synonyms: [ + { + name: 'log.message.downconversion.enable', + value: 'true', + source: ConfigSource.DEFAULT_CONFIG, + }, + ], + }, + { + name: 'min.insync.replicas', + value: '1', + defaultValue: '1', + source: ConfigSource.DYNAMIC_TOPIC_CONFIG, + isSensitive: false, + isReadOnly: false, + synonyms: [ + { + name: 'min.insync.replicas', + value: '1', + source: ConfigSource.DYNAMIC_TOPIC_CONFIG, + }, + { + name: 'min.insync.replicas', + value: '1', + source: ConfigSource.DEFAULT_CONFIG, + }, + ], + }, +]; + +const topicStatsSize = { + sum: 0, + avg: 0, + prctl50: 0, + prctl75: 0, + prctl95: 0, + prctl99: 0, + prctl999: 0, +}; +export const topicStatsPayload: TopicAnalysis = { + progress: { + startedAt: 1659984559167, + completenessPercent: 43, + msgsScanned: 18077002, + bytesScanned: 6750901718, + }, + result: { + startedAt: 1659984559095, + finishedAt: 1659984617816, + totalStats: { + totalMsgs: 18194715, + minOffset: 98869591, + maxOffset: 100576010, + minTimestamp: 1659719759485, + maxTimestamp: 1659984603419, + nullKeys: 18194715, + nullValues: 0, + approxUniqKeys: 0, + approxUniqValues: 17817283, + keySize: topicStatsSize, + valueSize: topicStatsSize, + hourlyMsgCounts: [ + { hourStart: 1659718800000, count: 16157 }, + { hourStart: 1659722400000, count: 225790 }, + ], + }, + partitionStats: [ + { + partition: 0, + totalMsgs: 1515285, + minOffset: 99060726, + maxOffset: 100576010, + minTimestamp: 1659722684090, + maxTimestamp: 1659984603419, + nullKeys: 1515285, + nullValues: 0, + approxUniqKeys: 0, + approxUniqValues: 1515285, + keySize: topicStatsSize, + valueSize: topicStatsSize, + hourlyMsgCounts: [ + { hourStart: 1659722400000, count: 18040 }, + { hourStart: 1659726000000, count: 20070 }, + ], + }, + { + partition: 1, + totalMsgs: 1534422, + minOffset: 98897827, + maxOffset: 100432248, + minTimestamp: 1659722803993, + maxTimestamp: 1659984603416, + nullKeys: 1534422, + nullValues: 0, + approxUniqKeys: 0, + approxUniqValues: 1516431, + keySize: topicStatsSize, + valueSize: topicStatsSize, + hourlyMsgCounts: [{ hourStart: 1659722400000, count: 19058 }], + }, + ], + }, +}; diff --git a/kafka-ui-react-app/src/lib/hooks/__tests__/dateTimeHelpers.spec.ts b/kafka-ui-react-app/src/lib/hooks/__tests__/dateTimeHelpers.spec.ts new file mode 100644 index 00000000000..ad5346283c6 --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/__tests__/dateTimeHelpers.spec.ts @@ -0,0 +1,23 @@ +import { formatTimestamp } from 'lib/dateTimeHelpers'; + +describe('dateTimeHelpers', () => { + describe('formatTimestamp', () => { + it('should check the empty case', () => { + expect(formatTimestamp('')).toBe(''); + }); + + it('should check the invalid case', () => { + expect(formatTimestamp('invalid')).toBe(''); + }); + + it('should output the correct date', () => { + const date = new Date(); + expect(formatTimestamp(date)).toBe( + date.toLocaleString([], { hourCycle: 'h23' }) + ); + expect(formatTimestamp(date.getTime())).toBe( + date.toLocaleString([], { hourCycle: 'h23' }) + ); + }); + }); +}); diff --git a/kafka-ui-react-app/src/lib/hooks/__tests__/fixtures.ts b/kafka-ui-react-app/src/lib/hooks/__tests__/fixtures.ts new file mode 100644 index 00000000000..a6bdab43661 --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/__tests__/fixtures.ts @@ -0,0 +1,33 @@ +import { Action, ResourceType } from 'generated-sources'; +import { modifyRolesData } from 'lib/permissions'; + +export const clusterName1 = 'local'; +export const clusterName2 = 'dev'; + +const userPermissionsMock = [ + { + clusters: [clusterName1], + resource: ResourceType.TOPIC, + actions: [Action.CREATE], + }, + { + clusters: [clusterName1], + resource: ResourceType.SCHEMA, + actions: [Action.EDIT, Action.DELETE], + value: '123.*', + }, + { + clusters: [clusterName1, clusterName2], + resource: ResourceType.TOPIC, + value: 'test.*', + actions: [Action.MESSAGES_DELETE], + }, + { + clusters: [clusterName1, clusterName2], + resource: ResourceType.TOPIC, + value: '.*', + actions: [Action.EDIT, Action.DELETE], + }, +]; + +export const modifiedData = modifyRolesData(userPermissionsMock); diff --git a/kafka-ui-react-app/src/lib/hooks/__tests__/useBoolean.spec.ts b/kafka-ui-react-app/src/lib/hooks/__tests__/useBoolean.spec.ts new file mode 100644 index 00000000000..12ec120752d --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/__tests__/useBoolean.spec.ts @@ -0,0 +1,66 @@ +import { renderHook, act } from '@testing-library/react'; +import useBoolean from 'lib/hooks/useBoolean'; + +describe('useBoolean CustomHook', () => { + it('should check true initial values', () => { + let initialValue = true; + const { result, rerender } = renderHook(() => useBoolean(initialValue)); + expect(result.current.value).toBe(initialValue); + initialValue = false; + rerender(); + // because state is in useState + expect(result.current.value).not.toBe(initialValue); + }); + + it('should check false initial values', () => { + let initialValue = false; + const { result, rerender } = renderHook(() => useBoolean(initialValue)); + expect(result.current.value).toBe(initialValue); + + initialValue = true; + rerender(); + // because state is in useState + expect(result.current.value).not.toBe(initialValue); + }); + + it('should check setTrue function', () => { + const { result } = renderHook(() => useBoolean()); + expect(result.current.value).toBeFalsy(); + act(() => { + result.current.setTrue(); + }); + expect(result.current.value).toBeTruthy(); + }); + + it('should check setFalse function', () => { + const { result } = renderHook(() => useBoolean()); + + expect(result.current.value).toBeFalsy(); + act(() => { + result.current.setTrue(); + }); + + expect(result.current.value).toBeTruthy(); + + act(() => { + result.current.setFalse(); + }); + expect(result.current.value).toBeFalsy(); + }); + + it('should check setToggle function', () => { + const { result } = renderHook(() => useBoolean()); + + expect(result.current.value).toBeFalsy(); + act(() => { + result.current.toggle(); + }); + + expect(result.current.value).toBeTruthy(); + + act(() => { + result.current.toggle(); + }); + expect(result.current.value).toBeFalsy(); + }); +}); diff --git a/kafka-ui-react-app/src/lib/hooks/__tests__/useCreatePermission.spec.tsx b/kafka-ui-react-app/src/lib/hooks/__tests__/useCreatePermission.spec.tsx new file mode 100644 index 00000000000..56161a0c697 --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/__tests__/useCreatePermission.spec.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { renderHook } from '@testing-library/react'; +import { isPermittedToCreate } from 'lib/permissions'; +import { Action, ResourceType } from 'generated-sources'; +import { + UserInfoRolesAccessContext, + UserInfoType, +} from 'components/contexts/UserInfoRolesAccessContext'; +import { useCreatePermission } from 'lib/hooks/useCreatePermisson'; + +import { modifiedData, clusterName1, clusterName2 } from './fixtures'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn(), +})); + +describe('useCreatePermission', () => { + const customRenderer = ({ + resource, + userInfo, + }: { + resource: ResourceType; + userInfo: UserInfoType; + }) => + renderHook(() => useCreatePermission(resource), { + wrapper: ({ children }) => ( + // eslint-disable-next-line react/react-in-jsx-scope + + // issue in initialProps of wrapper + + {children} + + ), + }); + + it('should check if the hook renders the same value as the isPermittedToCreate Headless logic method', () => { + const permissionConfig = { + resource: ResourceType.TOPIC, + userInfo: { + roles: modifiedData, + rbacFlag: true, + username: '', + }, + }; + + (useParams as jest.Mock).mockImplementation(() => ({ + clusterName: clusterName1, + })); + + const { result } = customRenderer(permissionConfig); + + expect(result.current).toEqual( + isPermittedToCreate({ + ...permissionConfig, + roles: modifiedData, + clusterName: clusterName1, + rbacFlag: true, + }) + ); + }); + + it('should check if the hook renders the same value as the isPermittedToCreate Headless logic method for Schema', () => { + const permissionConfig = { + resource: ResourceType.SCHEMA, + action: Action.CREATE, + userInfo: { + roles: modifiedData, + rbacFlag: false, + username: '', + }, + }; + + (useParams as jest.Mock).mockImplementation(() => ({ + clusterName: clusterName1, + })); + + const { result } = customRenderer(permissionConfig); + + expect(result.current).toEqual( + isPermittedToCreate({ + ...permissionConfig, + roles: modifiedData, + clusterName: clusterName1, + rbacFlag: false, + }) + ); + }); + + it('should check if the hook renders the same value as the isPermittedToCreate Headless logic method for another Cluster', () => { + const permissionConfig = { + resource: ResourceType.SCHEMA, + action: Action.CREATE, + userInfo: { + roles: modifiedData, + rbacFlag: true, + username: '', + }, + }; + + (useParams as jest.Mock).mockImplementation(() => ({ + clusterName: clusterName2, + })); + + const { result } = customRenderer(permissionConfig); + + expect(result.current).toEqual( + isPermittedToCreate({ + ...permissionConfig, + roles: modifiedData, + clusterName: clusterName2, + rbacFlag: true, + }) + ); + }); +}); diff --git a/kafka-ui-react-app/src/lib/hooks/__tests__/useDataSaver.spec.tsx b/kafka-ui-react-app/src/lib/hooks/__tests__/useDataSaver.spec.tsx index fb53b95cf31..9b125575d97 100644 --- a/kafka-ui-react-app/src/lib/hooks/__tests__/useDataSaver.spec.tsx +++ b/kafka-ui-react-app/src/lib/hooks/__tests__/useDataSaver.spec.tsx @@ -1,7 +1,12 @@ import React, { useEffect } from 'react'; -import { render } from 'lib/testHelpers'; import useDataSaver from 'lib/hooks/useDataSaver'; +import { render } from '@testing-library/react'; +import { showAlert } from 'lib/errorHandling'; +jest.mock('lib/errorHandling', () => ({ + ...jest.requireActual('lib/errorHandling'), + showAlert: jest.fn(), +})); describe('useDataSaver hook', () => { const content = { title: 'title', @@ -9,40 +14,14 @@ describe('useDataSaver hook', () => { describe('Save as file', () => { beforeAll(() => { - jest.useFakeTimers('modern'); + jest.useFakeTimers(); jest.setSystemTime(new Date('Wed Mar 24 2021 03:19:56 GMT-0700')); }); afterAll(() => jest.useRealTimers()); - it('downloads json file', () => { - const link: HTMLAnchorElement = document.createElement('a'); - link.click = jest.fn(); - - const mockCreate = jest - .spyOn(document, 'createElement') - .mockImplementation(() => link); - - const HookWrapper: React.FC = () => { - const { saveFile } = useDataSaver('message', content); - useEffect(() => saveFile(), [saveFile]); - return null; - }; - - render(); - expect(mockCreate).toHaveBeenCalledTimes(2); - expect(link.download).toEqual('message_1616581196000.json'); - expect(link.href).toEqual( - `data:text/json;charset=utf-8,${encodeURIComponent( - JSON.stringify(content) - )}` - ); - expect(link.click).toHaveBeenCalledTimes(1); - - mockCreate.mockRestore(); - }); - it('downloads txt file', () => { + global.URL.createObjectURL = jest.fn(); const link: HTMLAnchorElement = document.createElement('a'); link.click = jest.fn(); @@ -58,18 +37,12 @@ describe('useDataSaver hook', () => { render(); expect(mockCreate).toHaveBeenCalledTimes(2); - expect(link.download).toEqual('message_1616581196000.txt'); - expect(link.href).toEqual( - `data:text/json;charset=utf-8,${encodeURIComponent( - JSON.stringify('content') - )}` - ); + expect(link.download).toEqual('message'); expect(link.click).toHaveBeenCalledTimes(1); mockCreate.mockRestore(); }); }); - describe('copies the data to the clipboard', () => { Object.assign(navigator, { clipboard: { @@ -105,4 +78,29 @@ describe('useDataSaver hook', () => { ); }); }); + describe('navigator clipboard is undefined', () => { + it('calls showAlert with the correct parameters when clipboard API is unavailable', () => { + Object.assign(navigator, { + clipboard: undefined, + }); + + const HookWrapper: React.FC = () => { + const { copyToClipboard } = useDataSaver('topic', content); + useEffect(() => { + copyToClipboard(); + }, [copyToClipboard]); + return null; + }; + + render(); + + expect(showAlert).toHaveBeenCalledTimes(1); + expect(showAlert).toHaveBeenCalledWith('warning', { + id: 'topic', + title: 'Warning', + message: + 'Copying to clipboard is unavailable due to unsecured (non-HTTPS) connection', + }); + }); + }); }); diff --git a/kafka-ui-react-app/src/lib/hooks/__tests__/useModal.spec.ts b/kafka-ui-react-app/src/lib/hooks/__tests__/useModal.spec.ts deleted file mode 100644 index 85064e4ea83..00000000000 --- a/kafka-ui-react-app/src/lib/hooks/__tests__/useModal.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { renderHook, act } from '@testing-library/react'; -import useModal from 'lib/hooks/useModal'; - -describe('useModal CustomHook', () => { - it('should check true initial values', () => { - let initialValue = true; - const { result, rerender } = renderHook(() => useModal(initialValue)); - expect(result.current.isOpen).toBe(initialValue); - initialValue = false; - rerender(); - // because state is in useState - expect(result.current.isOpen).not.toBe(initialValue); - }); - - it('should check false initial values', () => { - let initialValue = false; - const { result, rerender } = renderHook(() => useModal(initialValue)); - expect(result.current.isOpen).toBe(initialValue); - - initialValue = true; - rerender(); - // because state is in useState - expect(result.current.isOpen).not.toBe(initialValue); - }); - - it('should check setOpen function', () => { - const { result } = renderHook(() => useModal()); - expect(result.current.isOpen).toBeFalsy(); - act(() => { - result.current.setOpen(); - }); - expect(result.current.isOpen).toBeTruthy(); - }); - - it('should check setClose function', () => { - const { result } = renderHook(() => useModal()); - - expect(result.current.isOpen).toBeFalsy(); - act(() => { - result.current.setOpen(); - }); - - expect(result.current.isOpen).toBeTruthy(); - - act(() => { - result.current.setClose(); - }); - expect(result.current.isOpen).toBeFalsy(); - }); - - it('should check setToggle function', () => { - const { result } = renderHook(() => useModal()); - - expect(result.current.isOpen).toBeFalsy(); - act(() => { - result.current.toggle(); - }); - - expect(result.current.isOpen).toBeTruthy(); - - act(() => { - result.current.toggle(); - }); - expect(result.current.isOpen).toBeFalsy(); - }); -}); diff --git a/kafka-ui-react-app/src/lib/hooks/__tests__/usePermission.spec.tsx b/kafka-ui-react-app/src/lib/hooks/__tests__/usePermission.spec.tsx new file mode 100644 index 00000000000..94b0572e117 --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/__tests__/usePermission.spec.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { renderHook } from '@testing-library/react'; +import { usePermission } from 'lib/hooks/usePermission'; +import { isPermitted } from 'lib/permissions'; +import { Action, ResourceType } from 'generated-sources'; +import { + UserInfoRolesAccessContext, + UserInfoType, +} from 'components/contexts/UserInfoRolesAccessContext'; + +import { clusterName1, modifiedData, clusterName2 } from './fixtures'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn(), +})); + +describe('usePermission', () => { + const customRenderer = ({ + resource, + action, + value, + userInfo, + }: { + resource: ResourceType; + action: Action; + value?: string; + userInfo: UserInfoType; + }) => + renderHook(() => usePermission(resource, action, value), { + wrapper: ({ children }) => ( + // eslint-disable-next-line react/react-in-jsx-scope + + // issue in initialProps of wrapper + + {children} + + ), + }); + + it('should check if the hook renders the same value as the isPermitted Headless logic method', () => { + const permissionConfig = { + resource: ResourceType.TOPIC, + action: Action.CREATE, + userInfo: { + roles: modifiedData, + rbacFlag: true, + username: '', + }, + }; + + (useParams as jest.Mock).mockImplementation(() => ({ + clusterName: clusterName1, + })); + + const { result } = customRenderer(permissionConfig); + + expect(result.current).toEqual( + isPermitted({ + ...permissionConfig, + roles: modifiedData, + clusterName: clusterName1, + rbacFlag: true, + }) + ); + }); + + it('should check if the hook renders the same value as the isPermitted Headless logic method for Schema', () => { + const permissionConfig = { + resource: ResourceType.SCHEMA, + action: Action.CREATE, + userInfo: { + roles: modifiedData, + rbacFlag: true, + username: '', + }, + }; + + (useParams as jest.Mock).mockImplementation(() => ({ + clusterName: clusterName1, + })); + + const { result } = customRenderer(permissionConfig); + + expect(result.current).toEqual( + isPermitted({ + ...permissionConfig, + roles: modifiedData, + clusterName: clusterName1, + rbacFlag: true, + }) + ); + }); + + it('should check if the hook renders the same value as the isPermitted Headless logic method for another Cluster', () => { + const permissionConfig = { + resource: ResourceType.SCHEMA, + action: Action.CREATE, + userInfo: { + roles: modifiedData, + rbacFlag: true, + username: '', + }, + }; + + (useParams as jest.Mock).mockImplementation(() => ({ + clusterName: clusterName2, + })); + + const { result } = customRenderer(permissionConfig); + + expect(result.current).toEqual( + isPermitted({ + ...permissionConfig, + roles: modifiedData, + clusterName: clusterName2, + rbacFlag: true, + }) + ); + }); +}); diff --git a/kafka-ui-react-app/src/lib/hooks/api/__tests__/brokers.spec.ts b/kafka-ui-react-app/src/lib/hooks/api/__tests__/brokers.spec.ts new file mode 100644 index 00000000000..3b90637c98c --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/api/__tests__/brokers.spec.ts @@ -0,0 +1,37 @@ +import { expectQueryWorks, renderQueryHook } from 'lib/testHelpers'; +import * as hooks from 'lib/hooks/api/brokers'; +import fetchMock from 'fetch-mock'; + +const clusterName = 'test-cluster'; +const brokerId = 1; +const brokersPath = `/api/clusters/${clusterName}/brokers`; +const brokerPath = `${brokersPath}/${brokerId}`; + +describe('Brokers hooks', () => { + beforeEach(() => fetchMock.restore()); + describe('useBrokers', () => { + it('useBrokers', async () => { + const mock = fetchMock.getOnce(brokersPath, []); + const { result } = renderQueryHook(() => hooks.useBrokers(clusterName)); + await expectQueryWorks(mock, result); + }); + }); + describe('useBrokerMetrics', () => { + it('useBrokerMetrics', async () => { + const mock = fetchMock.getOnce(`${brokerPath}/metrics`, {}); + const { result } = renderQueryHook(() => + hooks.useBrokerMetrics(clusterName, brokerId) + ); + await expectQueryWorks(mock, result); + }); + }); + describe('useBrokerLogDirs', () => { + it('useBrokerLogDirs', async () => { + const mock = fetchMock.getOnce(`${brokersPath}/logdirs?broker=1`, []); + const { result } = renderQueryHook(() => + hooks.useBrokerLogDirs(clusterName, brokerId) + ); + await expectQueryWorks(mock, result); + }); + }); +}); diff --git a/kafka-ui-react-app/src/lib/hooks/api/__tests__/clusters.spec.ts b/kafka-ui-react-app/src/lib/hooks/api/__tests__/clusters.spec.ts new file mode 100644 index 00000000000..7112d945c3c --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/api/__tests__/clusters.spec.ts @@ -0,0 +1,29 @@ +import { expectQueryWorks, renderQueryHook } from 'lib/testHelpers'; +import * as hooks from 'lib/hooks/api/clusters'; +import fetchMock from 'fetch-mock'; +import { clustersPayload } from 'lib/fixtures/clusters'; + +const clusterName = 'test-cluster'; + +describe('Clusters hooks', () => { + beforeEach(() => fetchMock.restore()); + describe('useClusters', () => { + it('returns the correct data', async () => { + const mock = fetchMock.getOnce('/api/clusters', clustersPayload); + const { result } = renderQueryHook(() => hooks.useClusters()); + await expectQueryWorks(mock, result); + }); + }); + describe('useClusterStats', () => { + it('returns the correct data', async () => { + const mock = fetchMock.getOnce( + `/api/clusters/${clusterName}/stats`, + clustersPayload + ); + const { result } = renderQueryHook(() => + hooks.useClusterStats(clusterName) + ); + await expectQueryWorks(mock, result); + }); + }); +}); diff --git a/kafka-ui-react-app/src/lib/hooks/api/__tests__/kafkaConnect.spec.ts b/kafka-ui-react-app/src/lib/hooks/api/__tests__/kafkaConnect.spec.ts new file mode 100644 index 00000000000..96d280f7d1c --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/api/__tests__/kafkaConnect.spec.ts @@ -0,0 +1,162 @@ +import { act, renderHook, waitFor } from '@testing-library/react'; +import { + expectQueryWorks, + renderQueryHook, + TestQueryClientProvider, +} from 'lib/testHelpers'; +import * as hooks from 'lib/hooks/api/kafkaConnect'; +import fetchMock from 'fetch-mock'; +import { connectors, connects, tasks } from 'lib/fixtures/kafkaConnect'; +import { ConnectorAction } from 'generated-sources'; + +const clusterName = 'test-cluster'; +const connectName = 'test-connect'; +const connectorName = 'test-connector'; + +const connectsPath = `/api/clusters/${clusterName}/connects`; +const connectorsPath = `/api/clusters/${clusterName}/connectors`; +const connectorPath = `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`; + +const connectorProps = { + clusterName, + connectName, + connectorName, +}; + +describe('kafkaConnect hooks', () => { + beforeEach(() => fetchMock.restore()); + describe('useConnects', () => { + it('returns the correct data', async () => { + const mock = fetchMock.getOnce(connectsPath, connects); + const { result } = renderQueryHook(() => hooks.useConnects(clusterName)); + await expectQueryWorks(mock, result); + }); + }); + describe('useConnectors', () => { + it('returns the correct data', async () => { + const mock = fetchMock.getOnce(connectorsPath, connectors); + const { result } = renderQueryHook(() => + hooks.useConnectors(clusterName) + ); + await expectQueryWorks(mock, result); + }); + + it('returns the correct data for request with search criteria', async () => { + const search = 'test-search'; + const mock = fetchMock.getOnce( + `${connectorsPath}?search=${search}`, + connectors + ); + const { result } = renderQueryHook(() => + hooks.useConnectors(clusterName, search) + ); + await expectQueryWorks(mock, result); + }); + }); + describe('useConnector', () => { + it('returns the correct data', async () => { + const mock = fetchMock.getOnce(connectorPath, connectors[0]); + const { result } = renderQueryHook(() => + hooks.useConnector(connectorProps) + ); + await expectQueryWorks(mock, result); + }); + }); + describe('useConnectorTasks', () => { + it('returns the correct data', async () => { + const mock = fetchMock.getOnce(`${connectorPath}/tasks`, tasks); + const { result } = renderQueryHook(() => + hooks.useConnectorTasks(connectorProps) + ); + await expectQueryWorks(mock, result); + }); + }); + describe('useConnectorConfig', () => { + it('returns the correct data', async () => { + const mock = fetchMock.getOnce(`${connectorPath}/config`, {}); + const { result } = renderQueryHook(() => + hooks.useConnectorConfig(connectorProps) + ); + await expectQueryWorks(mock, result); + }); + }); + + describe('mutatations', () => { + describe('useUpdateConnectorState', () => { + it('returns the correct data', async () => { + const action = ConnectorAction.RESTART; + const uri = `${connectorPath}/action/${action}`; + const mock = fetchMock.postOnce(uri, connectors[0]); + const { result } = renderHook( + () => hooks.useUpdateConnectorState(connectorProps), + { wrapper: TestQueryClientProvider } + ); + await act(() => result.current.mutateAsync(action)); + await waitFor(() => expect(result.current.isSuccess).toBeTruthy()); + expect(mock.calls()).toHaveLength(1); + }); + }); + describe('useRestartConnectorTask', () => { + it('returns the correct data', async () => { + const taskId = 123456; + const uri = `${connectorPath}/tasks/${taskId}/action/restart`; + const mock = fetchMock.postOnce(uri, {}); + const { result } = renderHook( + () => hooks.useRestartConnectorTask(connectorProps), + { wrapper: TestQueryClientProvider } + ); + await act(() => result.current.mutateAsync(taskId)); + await waitFor(() => expect(result.current.isSuccess).toBeTruthy()); + expect(mock.calls()).toHaveLength(1); + }); + }); + describe('useUpdateConnectorConfig', () => { + it('returns the correct data', async () => { + const mock = fetchMock.putOnce(`${connectorPath}/config`, {}); + const { result } = renderHook( + () => hooks.useUpdateConnectorConfig(connectorProps), + { wrapper: TestQueryClientProvider } + ); + await act(async () => { + await result.current.mutateAsync({ config: 1 }); + }); + await waitFor(() => expect(result.current.isSuccess).toBeTruthy()); + expect(mock.calls()).toHaveLength(1); + }); + }); + describe('useCreateConnector', () => { + it('returns the correct data', async () => { + const mock = fetchMock.postOnce( + `${connectsPath}/${connectName}/connectors`, + {} + ); + const { result } = renderHook( + () => hooks.useCreateConnector(clusterName), + { wrapper: TestQueryClientProvider } + ); + await act(async () => { + await result.current.mutateAsync({ + connectName, + newConnector: { name: connectorName, config: { a: 1 } }, + }); + }); + await waitFor(() => expect(result.current.isSuccess).toBeTruthy()); + expect(mock.calls()).toHaveLength(1); + }); + }); + describe('useDeleteConnector', () => { + it('returns the correct data', async () => { + const mock = fetchMock.deleteOnce(connectorPath, {}); + const { result } = renderHook( + () => hooks.useDeleteConnector(connectorProps), + { wrapper: TestQueryClientProvider } + ); + await act(async () => { + await result.current.mutateAsync(); + }); + await waitFor(() => expect(result.current.isSuccess).toBeTruthy()); + expect(mock.calls()).toHaveLength(1); + }); + }); + }); +}); diff --git a/kafka-ui-react-app/src/lib/hooks/api/__tests__/latestVersion.spec.ts b/kafka-ui-react-app/src/lib/hooks/api/__tests__/latestVersion.spec.ts new file mode 100644 index 00000000000..a12f2629950 --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/api/__tests__/latestVersion.spec.ts @@ -0,0 +1,17 @@ +import fetchMock from 'fetch-mock'; +import { expectQueryWorks, renderQueryHook } from 'lib/testHelpers'; +import { latestVersionPayload } from 'lib/fixtures/latestVersion'; +import { useLatestVersion } from 'lib/hooks/api/latestVersion'; + +const latestVersionPath = '/api/info'; + +describe('Latest version hooks', () => { + beforeEach(() => fetchMock.restore()); + describe('useLatestVersion', () => { + it('returns the correct data', async () => { + const mock = fetchMock.getOnce(latestVersionPath, latestVersionPayload); + const { result } = renderQueryHook(() => useLatestVersion()); + await expectQueryWorks(mock, result); + }); + }); +}); diff --git a/kafka-ui-react-app/src/lib/hooks/api/__tests__/topicMessages.spec.ts b/kafka-ui-react-app/src/lib/hooks/api/__tests__/topicMessages.spec.ts new file mode 100644 index 00000000000..49a143f227b --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/api/__tests__/topicMessages.spec.ts @@ -0,0 +1,36 @@ +import { waitFor } from '@testing-library/react'; +import { renderQueryHook } from 'lib/testHelpers'; +import * as hooks from 'lib/hooks/api/topicMessages'; +import fetchMock from 'fetch-mock'; +import { UseQueryResult } from '@tanstack/react-query'; +import { SerdeUsage } from 'generated-sources'; + +const clusterName = 'test-cluster'; +const topicName = 'test-topic'; + +const expectQueryWorks = async ( + mock: fetchMock.FetchMockStatic, + result: { current: UseQueryResult } +) => { + await waitFor(() => expect(result.current.isFetched).toBeTruthy()); + expect(mock.calls()).toHaveLength(1); + expect(result.current.data).toBeDefined(); +}; + +jest.mock('lib/errorHandling', () => ({ + ...jest.requireActual('lib/errorHandling'), + showServerError: jest.fn(), +})); + +describe('Topic Messages hooks', () => { + beforeEach(() => fetchMock.restore()); + it('handles useSerdes', async () => { + const path = `/api/clusters/${clusterName}/topic/${topicName}/serdes?use=SERIALIZE`; + + const mock = fetchMock.getOnce(path, {}); + const { result } = renderQueryHook(() => + hooks.useSerdes({ clusterName, topicName, use: SerdeUsage.SERIALIZE }) + ); + await expectQueryWorks(mock, result); + }); +}); diff --git a/kafka-ui-react-app/src/lib/hooks/api/__tests__/topics.spec.ts b/kafka-ui-react-app/src/lib/hooks/api/__tests__/topics.spec.ts new file mode 100644 index 00000000000..34b864fd4a7 --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/api/__tests__/topics.spec.ts @@ -0,0 +1,203 @@ +import { act, renderHook, waitFor } from '@testing-library/react'; +import { + expectQueryWorks, + renderQueryHook, + TestQueryClientProvider, +} from 'lib/testHelpers'; +import * as hooks from 'lib/hooks/api/topics'; +import fetchMock from 'fetch-mock'; +import { externalTopicPayload, topicConfigPayload } from 'lib/fixtures/topics'; +import { TopicFormData, TopicFormDataRaw } from 'redux/interfaces'; +import { CreateTopicMessage } from 'generated-sources'; + +const clusterName = 'test-cluster'; +const topicName = 'test-topic'; + +const topicsPath = `/api/clusters/${clusterName}/topics`; +const topicPath = `${topicsPath}/${topicName}`; + +const topicParams = { clusterName, topicName }; + +jest.mock('lib/errorHandling', () => ({ + ...jest.requireActual('lib/errorHandling'), + showServerError: jest.fn(), +})); + +describe('Topics hooks', () => { + beforeEach(() => fetchMock.restore()); + it('handles useTopics', async () => { + const mock = fetchMock.getOnce(topicsPath, []); + const { result } = renderQueryHook(() => hooks.useTopics({ clusterName })); + await expectQueryWorks(mock, result); + }); + it('handles useTopicDetails', async () => { + const mock = fetchMock.getOnce(topicPath, externalTopicPayload); + const { result } = renderQueryHook(() => + hooks.useTopicDetails(topicParams) + ); + await expectQueryWorks(mock, result); + }); + it('handles useTopicConfig', async () => { + const mock = fetchMock.getOnce(`${topicPath}/config`, topicConfigPayload); + const { result } = renderQueryHook(() => hooks.useTopicConfig(topicParams)); + await expectQueryWorks(mock, result); + }); + it('handles useTopicConsumerGroups', async () => { + const mock = fetchMock.getOnce(`${topicPath}/consumer-groups`, []); + const { result } = renderQueryHook(() => + hooks.useTopicConsumerGroups(topicParams) + ); + await expectQueryWorks(mock, result); + }); + describe('useTopicAnalysis', () => { + it('handles useTopicAnalysis', async () => { + const mock = fetchMock.getOnce(`${topicPath}/analysis`, {}); + const { result } = renderQueryHook(() => + hooks.useTopicAnalysis(topicParams) + ); + await expectQueryWorks(mock, result); + }); + it('disables useTopicAnalysis', async () => { + const mock = fetchMock.getOnce(`${topicPath}/analysis`, {}); + renderQueryHook(() => hooks.useTopicAnalysis(topicParams, false)); + expect(mock.calls()).toHaveLength(0); + }); + }); + + describe('mutatations', () => { + it('useCreateTopic', async () => { + const mock = fetchMock.postOnce(topicsPath, {}); + const { result } = renderHook(() => hooks.useCreateTopic(clusterName), { + wrapper: TestQueryClientProvider, + }); + const formData: TopicFormData = { + name: 'Topic Name', + partitions: 0, + replicationFactor: 0, + minInSyncReplicas: 0, + cleanupPolicy: '', + retentionMs: 0, + retentionBytes: 0, + maxMessageBytes: 0, + customParams: [], + }; + await act(() => { + result.current.mutateAsync(formData); + }); + await waitFor(() => expect(result.current.isSuccess).toBeTruthy()); + expect(mock.calls()).toHaveLength(1); + }); + + it('useUpdateTopic', async () => { + const mock = fetchMock.patchOnce(topicPath, {}); + const { result } = renderHook(() => hooks.useUpdateTopic(topicParams), { + wrapper: TestQueryClientProvider, + }); + const formData: TopicFormDataRaw = { + name: 'Topic Name', + partitions: 0, + replicationFactor: 0, + minInSyncReplicas: 0, + cleanupPolicy: '', + retentionMs: 0, + retentionBytes: 0, + maxMessageBytes: 0, + customParams: { + byIndex: {}, + allIndexes: [], + }, + }; + await act(() => { + result.current.mutateAsync(formData); + }); + await waitFor(() => expect(result.current.isSuccess).toBeTruthy()); + expect(mock.calls()).toHaveLength(1); + }); + it('useIncreaseTopicPartitionsCount', async () => { + const mock = fetchMock.patchOnce(`${topicPath}/partitions`, {}); + const { result } = renderHook( + () => hooks.useIncreaseTopicPartitionsCount(topicParams), + { wrapper: TestQueryClientProvider } + ); + await act(() => { + result.current.mutateAsync(3); + }); + await waitFor(() => expect(result.current.isSuccess).toBeTruthy()); + expect(mock.calls()).toHaveLength(1); + }); + it('useUpdateTopicReplicationFactor', async () => { + const mock = fetchMock.patchOnce(`${topicPath}/replications`, {}); + const { result } = renderHook( + () => hooks.useUpdateTopicReplicationFactor(topicParams), + { wrapper: TestQueryClientProvider } + ); + await act(() => { + result.current.mutateAsync(3); + }); + await waitFor(() => expect(result.current.isSuccess).toBeTruthy()); + expect(mock.calls()).toHaveLength(1); + }); + it('useDeleteTopic', async () => { + const mock = fetchMock.deleteOnce(topicPath, {}); + const { result } = renderHook(() => hooks.useDeleteTopic(clusterName), { + wrapper: TestQueryClientProvider, + }); + await act(() => { + result.current.mutateAsync(topicName); + }); + await waitFor(() => expect(result.current.isSuccess).toBeTruthy()); + expect(mock.calls()).toHaveLength(1); + }); + it('useRecreateTopic', async () => { + const mock = fetchMock.postOnce(topicPath, {}); + const { result } = renderHook(() => hooks.useRecreateTopic(topicParams), { + wrapper: TestQueryClientProvider, + }); + await act(() => { + result.current.mutateAsync(); + }); + await waitFor(() => expect(result.current.isSuccess).toBeTruthy()); + expect(mock.calls()).toHaveLength(1); + }); + it('useSendMessage', async () => { + const mock = fetchMock.postOnce(`${topicPath}/messages`, {}); + const { result } = renderHook(() => hooks.useSendMessage(topicParams), { + wrapper: TestQueryClientProvider, + }); + const message: CreateTopicMessage = { + partition: 0, + content: 'Hello World', + }; + await act(() => { + result.current.mutateAsync(message); + }); + await waitFor(() => expect(result.current.isSuccess).toBeTruthy()); + expect(mock.calls()).toHaveLength(1); + }); + it('useAnalyzeTopic', async () => { + const mock = fetchMock.postOnce(`${topicPath}/analysis`, {}); + const { result } = renderHook(() => hooks.useAnalyzeTopic(topicParams), { + wrapper: TestQueryClientProvider, + }); + await act(() => { + result.current.mutateAsync(); + }); + await waitFor(() => expect(result.current.isSuccess).toBeTruthy()); + expect(mock.calls()).toHaveLength(1); + }); + it('useCancelTopicAnalysis', async () => { + const mock = fetchMock.deleteOnce(`${topicPath}/analysis`, {}); + const { result } = renderHook( + () => hooks.useCancelTopicAnalysis(topicParams), + { + wrapper: TestQueryClientProvider, + } + ); + await act(() => { + result.current.mutateAsync(); + }); + await waitFor(() => expect(result.current.isSuccess).toBeTruthy()); + expect(mock.calls()).toHaveLength(1); + }); + }); +}); diff --git a/kafka-ui-react-app/src/lib/hooks/api/acl.ts b/kafka-ui-react-app/src/lib/hooks/api/acl.ts new file mode 100644 index 00000000000..da6a463ffff --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/api/acl.ts @@ -0,0 +1,67 @@ +import { aclApiClient as api } from 'lib/api'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { ClusterName } from 'redux/interfaces'; +import { showSuccessAlert } from 'lib/errorHandling'; +import { KafkaAcl } from 'generated-sources'; + +export function useAcls(clusterName: ClusterName) { + return useQuery( + ['clusters', clusterName, 'acls'], + () => api.listAcls({ clusterName }), + { + suspense: false, + } + ); +} + +export function useCreateAclMutation(clusterName: ClusterName) { + return useMutation( + (data: KafkaAcl) => + api.createAcl({ + clusterName, + kafkaAcl: data, + }), + { + onSuccess() { + showSuccessAlert({ + message: 'Your ACL was created successfully', + }); + }, + } + ); +} + +export function useCreateAcl(clusterName: ClusterName) { + const mutate = useCreateAclMutation(clusterName); + + return { + createResource: async (param: KafkaAcl) => { + return mutate.mutateAsync(param); + }, + ...mutate, + }; +} + +export function useDeleteAclMutation(clusterName: ClusterName) { + const queryClient = useQueryClient(); + return useMutation( + (acl: KafkaAcl) => api.deleteAcl({ clusterName, kafkaAcl: acl }), + { + onSuccess: () => { + showSuccessAlert({ message: 'ACL deleted' }); + queryClient.invalidateQueries(['clusters', clusterName, 'acls']); + }, + } + ); +} + +export function useDeleteAcl(clusterName: ClusterName) { + const mutate = useDeleteAclMutation(clusterName); + + return { + deleteResource: async (param: KafkaAcl) => { + return mutate.mutateAsync(param); + }, + ...mutate, + }; +} diff --git a/kafka-ui-react-app/src/lib/hooks/api/appConfig.ts b/kafka-ui-react-app/src/lib/hooks/api/appConfig.ts new file mode 100644 index 00000000000..f452ad06740 --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/api/appConfig.ts @@ -0,0 +1,69 @@ +import { appConfigApiClient as api } from 'lib/api'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { ApplicationConfigPropertiesKafkaClusters } from 'generated-sources'; +import { QUERY_REFETCH_OFF_OPTIONS } from 'lib/constants'; + +export function useAppInfo() { + return useQuery( + ['app', 'info'], + () => api.getApplicationInfo(), + QUERY_REFETCH_OFF_OPTIONS + ); +} + +export function useAppConfig() { + return useQuery(['app', 'config'], () => api.getCurrentConfig()); +} + +export function useUpdateAppConfig({ initialName }: { initialName?: string }) { + const client = useQueryClient(); + return useMutation( + async (cluster: ApplicationConfigPropertiesKafkaClusters) => { + const existingConfig = await api.getCurrentConfig(); + const existingClusters = existingConfig.properties?.kafka?.clusters || []; + + let clusters: ApplicationConfigPropertiesKafkaClusters[] = []; + + if (existingClusters.length > 0) { + if (!initialName) { + clusters = [...existingClusters, cluster]; + } else { + clusters = existingClusters.map((c) => + c.name === initialName ? cluster : c + ); + } + } else { + clusters = [cluster]; + } + + const config = { + ...existingConfig, + properties: { + ...existingConfig.properties, + kafka: { clusters }, + }, + }; + return api.restartWithConfig({ restartRequest: { config } }); + }, + { + onSuccess: () => client.invalidateQueries(['app', 'config']), + } + ); +} + +export function useAppConfigFilesUpload() { + return useMutation((payload: FormData) => + fetch('/api/config/relatedfiles', { + method: 'POST', + body: payload, + }).then((res) => res.json()) + ); +} + +export function useValidateAppConfig() { + return useMutation((config: ApplicationConfigPropertiesKafkaClusters) => + api.validateConfig({ + applicationConfig: { properties: { kafka: { clusters: [config] } } }, + }) + ); +} diff --git a/kafka-ui-react-app/src/lib/hooks/api/brokers.ts b/kafka-ui-react-app/src/lib/hooks/api/brokers.ts new file mode 100644 index 00000000000..073e2eb75ac --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/api/brokers.ts @@ -0,0 +1,75 @@ +import { brokersApiClient as api } from 'lib/api'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { ClusterName } from 'redux/interfaces'; +import { BrokerConfigItem } from 'generated-sources'; + +interface UpdateBrokerConfigProps { + name: string; + brokerConfigItem: BrokerConfigItem; +} + +export function useBrokers(clusterName: ClusterName) { + return useQuery( + ['clusters', clusterName, 'brokers'], + () => api.getBrokers({ clusterName }), + { refetchInterval: 5000 } + ); +} + +export function useBrokerMetrics(clusterName: ClusterName, brokerId: number) { + return useQuery( + ['clusters', clusterName, 'brokers', brokerId, 'metrics'], + () => + api.getBrokersMetrics({ + clusterName, + id: brokerId, + }) + ); +} + +export function useBrokerLogDirs(clusterName: ClusterName, brokerId: number) { + return useQuery( + ['clusters', clusterName, 'brokers', brokerId, 'logDirs'], + () => + api.getAllBrokersLogdirs({ + clusterName, + broker: [brokerId], + }) + ); +} + +export function useBrokerConfig(clusterName: ClusterName, brokerId: number) { + return useQuery( + ['clusters', clusterName, 'brokers', brokerId, 'settings'], + () => + api.getBrokerConfig({ + clusterName, + id: brokerId, + }) + ); +} + +export function useUpdateBrokerConfigByName( + clusterName: ClusterName, + brokerId: number +) { + const client = useQueryClient(); + return useMutation( + (payload: UpdateBrokerConfigProps) => + api.updateBrokerConfigByName({ + ...payload, + clusterName, + id: brokerId, + }), + { + onSuccess: () => + client.invalidateQueries([ + 'clusters', + clusterName, + 'brokers', + brokerId, + 'settings', + ]), + } + ); +} diff --git a/kafka-ui-react-app/src/lib/hooks/api/clusters.ts b/kafka-ui-react-app/src/lib/hooks/api/clusters.ts new file mode 100644 index 00000000000..443363d3fd2 --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/api/clusters.ts @@ -0,0 +1,14 @@ +import { clustersApiClient as api } from 'lib/api'; +import { useQuery } from '@tanstack/react-query'; +import { ClusterName } from 'redux/interfaces'; + +export function useClusters() { + return useQuery(['clusters'], () => api.getClusters(), { suspense: false }); +} +export function useClusterStats(clusterName: ClusterName) { + return useQuery( + ['clusterStats', clusterName], + () => api.getClusterStats({ clusterName }), + { refetchInterval: 5000 } + ); +} diff --git a/kafka-ui-react-app/src/lib/hooks/api/consumers.ts b/kafka-ui-react-app/src/lib/hooks/api/consumers.ts new file mode 100644 index 00000000000..c0089e1f00f --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/api/consumers.ts @@ -0,0 +1,92 @@ +import { consumerGroupsApiClient as api } from 'lib/api'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { ClusterName } from 'redux/interfaces'; +import { + ConsumerGroup, + ConsumerGroupOffsetsReset, + ConsumerGroupOrdering, + SortOrder, +} from 'generated-sources'; +import { showSuccessAlert } from 'lib/errorHandling'; + +export type ConsumerGroupID = ConsumerGroup['groupId']; + +type UseConsumerGroupsProps = { + clusterName: ClusterName; + orderBy?: ConsumerGroupOrdering; + sortOrder?: SortOrder; + page?: number; + perPage?: number; + search: string; +}; + +type UseConsumerGroupDetailsProps = { + clusterName: ClusterName; + consumerGroupID: ConsumerGroupID; +}; + +export function useConsumerGroups(props: UseConsumerGroupsProps) { + const { clusterName, ...rest } = props; + return useQuery( + ['clusters', clusterName, 'consumerGroups', rest], + () => api.getConsumerGroupsPage(props), + { suspense: false, keepPreviousData: true } + ); +} + +export function useConsumerGroupDetails(props: UseConsumerGroupDetailsProps) { + const { clusterName, consumerGroupID } = props; + return useQuery( + ['clusters', clusterName, 'consumerGroups', consumerGroupID], + () => api.getConsumerGroup({ clusterName, id: consumerGroupID }) + ); +} + +export const useDeleteConsumerGroupMutation = ({ + clusterName, + consumerGroupID, +}: UseConsumerGroupDetailsProps) => { + const queryClient = useQueryClient(); + return useMutation( + () => api.deleteConsumerGroup({ clusterName, id: consumerGroupID }), + { + onSuccess: () => { + showSuccessAlert({ + message: `Consumer ${consumerGroupID} group deleted`, + }); + queryClient.invalidateQueries([ + 'clusters', + clusterName, + 'consumerGroups', + ]); + }, + } + ); +}; + +export const useResetConsumerGroupOffsetsMutation = ({ + clusterName, + consumerGroupID, +}: UseConsumerGroupDetailsProps) => { + const queryClient = useQueryClient(); + return useMutation( + (props: ConsumerGroupOffsetsReset) => + api.resetConsumerGroupOffsets({ + clusterName, + id: consumerGroupID, + consumerGroupOffsetsReset: props, + }), + { + onSuccess: () => { + showSuccessAlert({ + message: `Consumer ${consumerGroupID} group offsets reset`, + }); + queryClient.invalidateQueries([ + 'clusters', + clusterName, + 'consumerGroups', + ]); + }, + } + ); +}; diff --git a/kafka-ui-react-app/src/lib/hooks/api/kafkaConnect.ts b/kafka-ui-react-app/src/lib/hooks/api/kafkaConnect.ts new file mode 100644 index 00000000000..1d01d491954 --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/api/kafkaConnect.ts @@ -0,0 +1,142 @@ +import { + Connect, + Connector, + ConnectorAction, + NewConnector, +} from 'generated-sources'; +import { kafkaConnectApiClient as api } from 'lib/api'; +import sortBy from 'lodash/sortBy'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { ClusterName } from 'redux/interfaces'; +import { showSuccessAlert } from 'lib/errorHandling'; + +interface UseConnectorProps { + clusterName: ClusterName; + connectName: Connect['name']; + connectorName: Connector['name']; +} +interface CreateConnectorProps { + connectName: Connect['name']; + newConnector: NewConnector; +} + +const connectsKey = (clusterName: ClusterName) => [ + 'clusters', + clusterName, + 'connects', +]; +const connectorsKey = (clusterName: ClusterName, search?: string) => { + const base = ['clusters', clusterName, 'connectors']; + if (search) { + return [...base, { search }]; + } + return base; +}; +const connectorKey = (props: UseConnectorProps) => [ + 'clusters', + props.clusterName, + 'connects', + props.connectName, + 'connectors', + props.connectorName, +]; +const connectorTasksKey = (props: UseConnectorProps) => [ + ...connectorKey(props), + 'tasks', +]; + +export function useConnects(clusterName: ClusterName) { + return useQuery(connectsKey(clusterName), () => + api.getConnects({ clusterName }) + ); +} +export function useConnectors(clusterName: ClusterName, search?: string) { + return useQuery( + connectorsKey(clusterName, search), + () => api.getAllConnectors({ clusterName, search }), + { + select: (data) => sortBy(data, 'name'), + } + ); +} +export function useConnector(props: UseConnectorProps) { + return useQuery(connectorKey(props), () => api.getConnector(props)); +} +export function useConnectorTasks(props: UseConnectorProps) { + return useQuery( + connectorTasksKey(props), + () => api.getConnectorTasks(props), + { + select: (data) => sortBy(data, 'status.id'), + } + ); +} +export function useUpdateConnectorState(props: UseConnectorProps) { + const client = useQueryClient(); + return useMutation( + (action: ConnectorAction) => api.updateConnectorState({ ...props, action }), + { + onSuccess: () => + client.invalidateQueries(['clusters', props.clusterName, 'connectors']), + } + ); +} +export function useRestartConnectorTask(props: UseConnectorProps) { + const client = useQueryClient(); + return useMutation( + (taskId: number) => api.restartConnectorTask({ ...props, taskId }), + { + onSuccess: () => client.invalidateQueries(connectorTasksKey(props)), + } + ); +} +export function useConnectorConfig(props: UseConnectorProps) { + return useQuery([...connectorKey(props), 'config'], () => + api.getConnectorConfig(props) + ); +} +export function useUpdateConnectorConfig(props: UseConnectorProps) { + const client = useQueryClient(); + return useMutation( + (requestBody: Connector['config']) => + api.setConnectorConfig({ ...props, requestBody }), + { + onSuccess: () => { + showSuccessAlert({ + message: `Config successfully updated.`, + }); + client.invalidateQueries(connectorKey(props)); + }, + } + ); +} +function useCreateConnectorMutation(clusterName: ClusterName) { + const client = useQueryClient(); + return useMutation( + (props: CreateConnectorProps) => + api.createConnector({ ...props, clusterName }), + { + onSuccess: () => client.invalidateQueries(connectorsKey(clusterName)), + } + ); +} + +// this will change later when we validate the request before +export function useCreateConnector(clusterName: ClusterName) { + const mutate = useCreateConnectorMutation(clusterName); + + return { + createResource: async (param: CreateConnectorProps) => { + return mutate.mutateAsync(param); + }, + ...mutate, + }; +} + +export function useDeleteConnector(props: UseConnectorProps) { + const client = useQueryClient(); + + return useMutation(() => api.deleteConnector(props), { + onSuccess: () => client.invalidateQueries(connectorsKey(props.clusterName)), + }); +} diff --git a/kafka-ui-react-app/src/lib/hooks/api/ksqlDb.tsx b/kafka-ui-react-app/src/lib/hooks/api/ksqlDb.tsx new file mode 100644 index 00000000000..366141e82a4 --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/api/ksqlDb.tsx @@ -0,0 +1,186 @@ +import { ksqlDbApiClient as api } from 'lib/api'; +import { useMutation, useQueries } from '@tanstack/react-query'; +import { ClusterName } from 'redux/interfaces'; +import { BASE_PARAMS } from 'lib/constants'; +import React from 'react'; +import { fetchEventSource } from '@microsoft/fetch-event-source'; +import { + showAlert, + showServerError, + showSuccessAlert, +} from 'lib/errorHandling'; +import { + ExecuteKsqlRequest, + KsqlResponse, + KsqlTableResponse, +} from 'generated-sources'; +import { StopLoading } from 'components/Topics/Topic/Messages/Messages.styled'; +import toast from 'react-hot-toast'; + +export function useKsqlkDb(clusterName: ClusterName) { + return useQueries({ + queries: [ + { + queryKey: ['clusters', clusterName, 'ksqlDb', 'tables'], + queryFn: () => api.listTables({ clusterName }), + suspense: false, + }, + { + queryKey: ['clusters', clusterName, 'ksqlDb', 'streams'], + queryFn: () => api.listStreams({ clusterName }), + suspense: false, + }, + ], + }); +} + +export function useExecuteKsqlkDbQueryMutation() { + return useMutation((props: ExecuteKsqlRequest) => api.executeKsql(props)); +} + +const getFormattedErrorFromTableData = ( + responseValues: KsqlTableResponse['values'] +): { title: string; message: string } => { + // We expect someting like that + // [[ + // "@type", + // "error_code", + // "message", + // "statementText"?, + // "entities"? + // ]], + // or + // [["message"]] + + if (!responseValues || !responseValues.length) { + return { + title: 'Unknown error', + message: 'Recieved empty response', + }; + } + + let title = ''; + let message = ''; + if (responseValues[0].length < 2) { + const [messageText] = responseValues[0]; + title = messageText; + } else { + const [type, errorCode, messageText, statementText, entities] = + responseValues[0]; + title = `[Error #${errorCode}] ${type}`; + message = + (entities?.length ? `[${entities.join(', ')}] ` : '') + + (statementText ? `"${statementText}" ` : '') + + messageText; + } + + return { title, message }; +}; + +type UseKsqlkDbSSEProps = { + pipeId: string | false; + clusterName: ClusterName; +}; + +export const useKsqlkDbSSE = ({ clusterName, pipeId }: UseKsqlkDbSSEProps) => { + const [data, setData] = React.useState(); + const [isFetching, setIsFetching] = React.useState(false); + + const abortController = new AbortController(); + + React.useEffect(() => { + const fetchData = async () => { + const url = `${BASE_PARAMS.basePath}/api/clusters/${encodeURIComponent( + clusterName + )}/ksql/response`; + await fetchEventSource( + `${url}?${new URLSearchParams({ pipeId: pipeId || '' }).toString()}`, + { + method: 'GET', + signal: abortController.signal, + openWhenHidden: true, + async onopen(response) { + const { ok, status } = response; + if (ok) setData(undefined); // Reset + if (status >= 400 && status < 500 && status !== 429) { + showServerError(response); + } + }, + onmessage(event) { + const { table }: KsqlResponse = JSON.parse(event.data); + if (!table) { + return; + } + switch (table?.header) { + case 'Execution error': { + showAlert('error', { + ...getFormattedErrorFromTableData(table.values), + id: `${url}-executionError`, + }); + break; + } + case 'Schema': + setData(table); + break; + case 'Row': + setData((state) => ({ + header: state?.header, + columnNames: state?.columnNames, + values: [...(state?.values || []), ...(table?.values || [])], + })); + break; + case 'Query Result': + showSuccessAlert({ + id: `${url}-querySuccess`, + title: 'Query succeed', + message: '', + }); + break; + case 'Source Description': + case 'properties': + default: + setData(table); + break; + } + }, + onclose() { + setIsFetching(false); + }, + onerror(err) { + setIsFetching(false); + showServerError(err); + }, + } + ); + }; + + const abortFetchData = () => { + setIsFetching(false); + if (pipeId) abortController.abort(); + }; + if (pipeId) { + toast.promise( + fetchData(), + { + loading: ( + <> +
    Consuming query execution result...
    +   + Abort + + ), + success: 'Cancelled', + error: 'Something went wrong. Please try again.', + }, + { + id: 'messages', + success: { duration: 20 }, + } + ); + } + + return abortFetchData; + }, [pipeId]); + + return { data, isFetching }; +}; diff --git a/kafka-ui-react-app/src/lib/hooks/api/latestVersion.ts b/kafka-ui-react-app/src/lib/hooks/api/latestVersion.ts new file mode 100644 index 00000000000..0711ad34d91 --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/api/latestVersion.ts @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; +import { BASE_PARAMS, QUERY_REFETCH_OFF_OPTIONS } from 'lib/constants'; + +const fetchLatestVersionInfo = async () => { + const data = await fetch( + `${BASE_PARAMS.basePath}/api/info`, + BASE_PARAMS + ).then((res) => res.json()); + + return data; +}; + +export function useLatestVersion() { + return useQuery( + ['versionInfo'], + fetchLatestVersionInfo, + QUERY_REFETCH_OFF_OPTIONS + ); +} diff --git a/kafka-ui-react-app/src/lib/hooks/api/roles.ts b/kafka-ui-react-app/src/lib/hooks/api/roles.ts new file mode 100644 index 00000000000..a47eae4a6d6 --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/api/roles.ts @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; +import { authApiClient } from 'lib/api'; +import { QUERY_REFETCH_OFF_OPTIONS } from 'lib/constants'; + +export function useGetUserInfo() { + return useQuery( + ['userInfo'], + () => authApiClient.getUserAuthInfo(), + QUERY_REFETCH_OFF_OPTIONS + ); +} diff --git a/kafka-ui-react-app/src/lib/hooks/api/topicMessages.tsx b/kafka-ui-react-app/src/lib/hooks/api/topicMessages.tsx new file mode 100644 index 00000000000..886b2979c0f --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/api/topicMessages.tsx @@ -0,0 +1,198 @@ +import React from 'react'; +import { fetchEventSource } from '@microsoft/fetch-event-source'; +import { BASE_PARAMS, MESSAGES_PER_PAGE } from 'lib/constants'; +import { ClusterName } from 'redux/interfaces'; +import { + GetSerdesRequest, + SeekDirection, + SeekType, + TopicMessage, + TopicMessageConsuming, + TopicMessageEvent, + TopicMessageEventTypeEnum, +} from 'generated-sources'; +import { showServerError } from 'lib/errorHandling'; +import toast from 'react-hot-toast'; +import { useQuery } from '@tanstack/react-query'; +import { messagesApiClient } from 'lib/api'; +import { StopLoading } from 'components/Topics/Topic/Messages/Messages.styled'; + +interface UseTopicMessagesProps { + clusterName: ClusterName; + topicName: string; + searchParams: URLSearchParams; +} + +type ConsumingMode = + | 'live' + | 'oldest' + | 'newest' + | 'fromOffset' // from 900 -> 1000 + | 'toOffset' // from 900 -> 800 + | 'sinceTime' // from 10:15 -> 11:15 + | 'untilTime'; // from 10:15 -> 9:15 + +export const useTopicMessages = ({ + clusterName, + topicName, + searchParams, +}: UseTopicMessagesProps) => { + const [messages, setMessages] = React.useState([]); + const [phase, setPhase] = React.useState(); + const [meta, setMeta] = React.useState(); + const [isFetching, setIsFetching] = React.useState(false); + const abortController = new AbortController(); + + // get initial properties + const mode = searchParams.get('m') as ConsumingMode; + const limit = searchParams.get('perPage') || MESSAGES_PER_PAGE; + const seekTo = searchParams.get('seekTo') || '0-0'; + + React.useEffect(() => { + const fetchData = async () => { + setIsFetching(true); + const url = `${BASE_PARAMS.basePath}/api/clusters/${encodeURIComponent( + clusterName + )}/topics/${topicName}/messages`; + const requestParams = new URLSearchParams({ + limit, + seekTo: seekTo.replaceAll('-', '::').replaceAll('.', ','), + q: searchParams.get('q') || '', + keySerde: searchParams.get('keySerde') || '', + valueSerde: searchParams.get('valueSerde') || '', + }); + + switch (mode) { + case 'live': + requestParams.set('seekDirection', SeekDirection.TAILING); + requestParams.set('seekType', SeekType.LATEST); + break; + case 'oldest': + requestParams.set('seekType', SeekType.BEGINNING); + requestParams.set('seekDirection', SeekDirection.FORWARD); + break; + case 'newest': + requestParams.set('seekType', SeekType.LATEST); + requestParams.set('seekDirection', SeekDirection.BACKWARD); + break; + case 'fromOffset': + requestParams.set('seekType', SeekType.OFFSET); + requestParams.set('seekDirection', SeekDirection.FORWARD); + break; + case 'toOffset': + requestParams.set('seekType', SeekType.OFFSET); + requestParams.set('seekDirection', SeekDirection.BACKWARD); + break; + case 'sinceTime': + requestParams.set('seekType', SeekType.TIMESTAMP); + requestParams.set('seekDirection', SeekDirection.FORWARD); + break; + case 'untilTime': + requestParams.set('seekType', SeekType.TIMESTAMP); + requestParams.set('seekDirection', SeekDirection.BACKWARD); + break; + default: + break; + } + + await fetchEventSource(`${url}?${requestParams.toString()}`, { + method: 'GET', + signal: abortController.signal, + openWhenHidden: true, + async onopen(response) { + const { ok, status } = response; + if (ok && status === 200) { + // Reset list of messages. + setMessages([]); + } else if (status >= 400 && status < 500 && status !== 429) { + showServerError(response); + } + }, + onmessage(event) { + const parsedData: TopicMessageEvent = JSON.parse(event.data); + const { message, consuming } = parsedData; + + switch (parsedData.type) { + case TopicMessageEventTypeEnum.MESSAGE: + if (message) { + setMessages((prevMessages) => { + if (mode === 'live') { + return [message, ...prevMessages]; + } + return [...prevMessages, message]; + }); + } + break; + case TopicMessageEventTypeEnum.PHASE: + if (parsedData.phase?.name) setPhase(parsedData.phase.name); + break; + case TopicMessageEventTypeEnum.CONSUMING: + if (consuming) setMeta(consuming); + break; + default: + } + }, + onclose() { + setIsFetching(false); + }, + onerror(err) { + setIsFetching(false); + showServerError(err); + }, + }); + }; + const abortFetchData = () => { + setIsFetching(false); + abortController.abort(); + }; + + if (mode === 'live') { + toast.promise( + fetchData(), + { + loading: ( + <> +
    Consuming messages...
    +   + Abort + + ), + success: 'Cancelled', + error: 'Something went wrong. Please try again.', + }, + { + id: 'messages', + position: 'top-center', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - missing type for icon + success: { duration: 10, icon: false }, + } + ); + } else { + fetchData(); + } + + return abortFetchData; + }, [searchParams]); + + return { + phase, + messages, + meta, + isFetching, + }; +}; + +export function useSerdes(props: GetSerdesRequest) { + const { clusterName, topicName, use } = props; + + return useQuery( + ['clusters', clusterName, 'topics', topicName, 'serdes', use], + () => messagesApiClient.getSerdes(props), + { + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchInterval: false, + } + ); +} diff --git a/kafka-ui-react-app/src/lib/hooks/api/topics.ts b/kafka-ui-react-app/src/lib/hooks/api/topics.ts new file mode 100644 index 00000000000..00d08bc66b8 --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/api/topics.ts @@ -0,0 +1,331 @@ +import { + topicsApiClient as api, + messagesApiClient as messagesApi, + consumerGroupsApiClient, + messagesApiClient, +} from 'lib/api'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { + ClusterName, + TopicFormData, + TopicFormDataRaw, + TopicFormFormattedParams, +} from 'redux/interfaces'; +import { + CreateTopicMessage, + GetTopicDetailsRequest, + GetTopicsRequest, + Topic, + TopicConfig, + TopicCreation, + TopicUpdate, +} from 'generated-sources'; +import { showServerError, showSuccessAlert } from 'lib/errorHandling'; + +export const topicKeys = { + all: (clusterName: ClusterName) => + ['clusters', clusterName, 'topics'] as const, + list: ( + clusterName: ClusterName, + filters: Omit + ) => [...topicKeys.all(clusterName), filters] as const, + details: ({ clusterName, topicName }: GetTopicDetailsRequest) => + [...topicKeys.all(clusterName), topicName] as const, + config: (props: GetTopicDetailsRequest) => + [...topicKeys.details(props), 'config'] as const, + schema: (props: GetTopicDetailsRequest) => + [...topicKeys.details(props), 'schema'] as const, + consumerGroups: (props: GetTopicDetailsRequest) => + [...topicKeys.details(props), 'consumerGroups'] as const, + statistics: (props: GetTopicDetailsRequest) => + [...topicKeys.details(props), 'statistics'] as const, +}; + +export function useTopics(props: GetTopicsRequest) { + const { clusterName, ...filters } = props; + return useQuery( + topicKeys.list(clusterName, filters), + () => api.getTopics(props), + { keepPreviousData: true } + ); +} +export function useTopicDetails(props: GetTopicDetailsRequest) { + return useQuery(topicKeys.details(props), () => api.getTopicDetails(props)); +} +export function useTopicConfig(props: GetTopicDetailsRequest) { + return useQuery(topicKeys.config(props), () => api.getTopicConfigs(props)); +} +export function useTopicConsumerGroups(props: GetTopicDetailsRequest) { + return useQuery(topicKeys.consumerGroups(props), () => + consumerGroupsApiClient.getTopicConsumerGroups(props) + ); +} + +const topicReducer = ( + result: TopicFormFormattedParams, + customParam: TopicConfig +) => { + return { + ...result, + [customParam.name]: customParam.value, + }; +}; +const formatTopicCreation = (form: TopicFormData): TopicCreation => { + const { + name, + partitions, + replicationFactor, + cleanupPolicy, + retentionMs, + maxMessageBytes, + minInSyncReplicas, + customParams, + } = form; + + const configs = { + 'cleanup.policy': cleanupPolicy, + 'retention.ms': retentionMs.toString(), + 'max.message.bytes': maxMessageBytes.toString(), + 'min.insync.replicas': minInSyncReplicas.toString(), + ...Object.values(customParams || {}).reduce(topicReducer, {}), + }; + + const cleanConfigs = () => { + return Object.fromEntries( + Object.entries(configs).filter(([, val]) => val !== '') + ); + }; + + const topicsvalue = { + name, + partitions, + configs: cleanConfigs(), + }; + + return replicationFactor.toString() !== '' + ? { + ...topicsvalue, + replicationFactor, + } + : topicsvalue; +}; + +export function useCreateTopicMutation(clusterName: ClusterName) { + const client = useQueryClient(); + return useMutation( + (data: TopicFormData) => + api.createTopic({ + clusterName, + topicCreation: formatTopicCreation(data), + }), + { + onSuccess: () => { + client.invalidateQueries(topicKeys.all(clusterName)); + }, + } + ); +} + +// this will change later when we validate the request before +export function useCreateTopic(clusterName: ClusterName) { + const mutate = useCreateTopicMutation(clusterName); + + return { + createResource: async (param: TopicFormData) => { + return mutate.mutateAsync(param); + }, + ...mutate, + }; +} + +const formatTopicUpdate = (form: TopicFormDataRaw): TopicUpdate => { + const { + cleanupPolicy, + retentionBytes, + retentionMs, + maxMessageBytes, + minInSyncReplicas, + customParams, + } = form; + + return { + configs: { + ...Object.values(customParams || {}).reduce(topicReducer, {}), + 'cleanup.policy': cleanupPolicy, + 'retention.ms': retentionMs, + 'retention.bytes': retentionBytes, + 'max.message.bytes': maxMessageBytes, + 'min.insync.replicas': minInSyncReplicas, + }, + }; +}; + +export function useUpdateTopic(props: GetTopicDetailsRequest) { + const client = useQueryClient(); + return useMutation( + (data: TopicFormDataRaw) => { + return api.updateTopic({ + ...props, + topicUpdate: formatTopicUpdate(data), + }); + }, + { + onSuccess: () => { + showSuccessAlert({ + message: `Topic successfully updated.`, + }); + client.invalidateQueries(topicKeys.all(props.clusterName)); + }, + } + ); +} +export function useIncreaseTopicPartitionsCount(props: GetTopicDetailsRequest) { + const client = useQueryClient(); + return useMutation( + (totalPartitionsCount: number) => + api.increaseTopicPartitions({ + ...props, + partitionsIncrease: { totalPartitionsCount }, + }), + { + onSuccess: () => { + showSuccessAlert({ + message: `Number of partitions successfully increased`, + }); + client.invalidateQueries(topicKeys.all(props.clusterName)); + }, + } + ); +} +export function useUpdateTopicReplicationFactor(props: GetTopicDetailsRequest) { + const client = useQueryClient(); + return useMutation( + (totalReplicationFactor: number) => + api.changeReplicationFactor({ + ...props, + replicationFactorChange: { totalReplicationFactor }, + }), + { + onSuccess: () => { + showSuccessAlert({ + message: `Replication factor successfully updated`, + }); + client.invalidateQueries(topicKeys.all(props.clusterName)); + }, + } + ); +} +export function useDeleteTopic(clusterName: ClusterName) { + const client = useQueryClient(); + return useMutation( + (topicName: Topic['name']) => api.deleteTopic({ clusterName, topicName }), + { + onSuccess: (_, topicName) => { + showSuccessAlert({ + message: `Topic ${topicName} successfully deleted!`, + }); + client.invalidateQueries(topicKeys.all(clusterName)); + }, + } + ); +} + +export function useClearTopicMessages( + clusterName: ClusterName, + partitions?: number[] +) { + const client = useQueryClient(); + return useMutation( + async (topicName: Topic['name']) => { + await messagesApiClient.deleteTopicMessages({ + clusterName, + partitions, + topicName, + }); + return topicName; + }, + + { + onSuccess: (topicName) => { + showSuccessAlert({ + id: `message-${topicName}-${clusterName}-${partitions}`, + message: `${topicName} messages have been successfully cleared!`, + }); + client.invalidateQueries(topicKeys.all(clusterName)); + }, + } + ); +} + +export function useRecreateTopic(props: GetTopicDetailsRequest) { + const client = useQueryClient(); + return useMutation(() => api.recreateTopic(props), { + onSuccess: () => { + showSuccessAlert({ + message: `Topic ${props.topicName} successfully recreated!`, + }); + client.invalidateQueries(topicKeys.all(props.clusterName)); + }, + }); +} + +export function useSendMessage(props: GetTopicDetailsRequest) { + const client = useQueryClient(); + return useMutation( + (message: CreateTopicMessage) => + messagesApi.sendTopicMessages({ ...props, createTopicMessage: message }), + { + onSuccess: () => { + showSuccessAlert({ + message: `Message successfully sent`, + }); + client.invalidateQueries(topicKeys.all(props.clusterName)); + }, + onError: (e) => { + showServerError(e as Response); + }, + } + ); +} + +// Statistics +export function useTopicAnalysis( + props: GetTopicDetailsRequest, + enabled = true +) { + return useQuery( + topicKeys.statistics(props), + () => api.getTopicAnalysis(props), + { + enabled, + refetchInterval: 1000, + useErrorBoundary: true, + retry: false, + suspense: false, + onError: (error: Response) => { + if (error.status !== 404) { + showServerError(error as Response); + } + }, + } + ); +} +export function useAnalyzeTopic(props: GetTopicDetailsRequest) { + const client = useQueryClient(); + return useMutation(() => api.analyzeTopic(props), { + onSuccess: () => { + client.invalidateQueries(topicKeys.statistics(props)); + }, + }); +} +export function useCancelTopicAnalysis(props: GetTopicDetailsRequest) { + const client = useQueryClient(); + return useMutation(() => api.cancelTopicAnalysis(props), { + onSuccess: () => { + showSuccessAlert({ + message: `Topic analysis canceled`, + }); + client.invalidateQueries(topicKeys.statistics(props)); + }, + }); +} diff --git a/kafka-ui-react-app/src/lib/hooks/useActionTooltip.ts b/kafka-ui-react-app/src/lib/hooks/useActionTooltip.ts new file mode 100644 index 00000000000..3fcb7ec48bc --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/useActionTooltip.ts @@ -0,0 +1,36 @@ +import { useState } from 'react'; +import { + autoPlacement, + offset, + Placement, + useFloating, + useHover, + useInteractions, +} from '@floating-ui/react'; + +export function useActionTooltip(isDisabled?: boolean, placement?: Placement) { + const [open, setOpen] = useState(false); + + const setTooltipOpen = (state: boolean) => { + if (!isDisabled) return; + setOpen(state); + }; + + const { x, y, reference, floating, strategy, context } = useFloating({ + open, + onOpenChange: setTooltipOpen, + placement, + middleware: [offset(10), autoPlacement()], + }); + + useInteractions([useHover(context)]); + + return { + x, + y, + reference, + floating, + strategy, + open, + }; +} diff --git a/kafka-ui-react-app/src/lib/hooks/useBoolean.ts b/kafka-ui-react-app/src/lib/hooks/useBoolean.ts new file mode 100644 index 00000000000..e757ad3b156 --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/useBoolean.ts @@ -0,0 +1,21 @@ +import React, { useCallback, useState } from 'react'; + +interface ReturnType { + value: boolean; + setTrue: () => void; + setFalse: () => void; + toggle: () => void; + setValue: React.Dispatch>; +} + +function useBoolean(defaultValue?: boolean): ReturnType { + const [value, setValue] = useState(!!defaultValue); + + const setTrue = useCallback(() => setValue(true), []); + const setFalse = useCallback(() => setValue(false), []); + const toggle = useCallback(() => setValue((x) => !x), []); + + return { value, setValue, setTrue, setFalse, toggle }; +} + +export default useBoolean; diff --git a/kafka-ui-react-app/src/lib/hooks/useConfirm.ts b/kafka-ui-react-app/src/lib/hooks/useConfirm.ts new file mode 100644 index 00000000000..baac856c598 --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/useConfirm.ts @@ -0,0 +1,17 @@ +import { ConfirmContext } from 'components/contexts/ConfirmContext'; +import React, { useContext } from 'react'; + +export const useConfirm = (danger = false) => { + const context = useContext(ConfirmContext); + return ( + message: React.ReactNode, + callback: () => void | Promise + ) => { + context?.setDangerButton(danger); + context?.setContent(message); + context?.setConfirm(() => async () => { + await callback(); + context?.cancel(); + }); + }; +}; diff --git a/kafka-ui-react-app/src/lib/hooks/useCreatePermisson.ts b/kafka-ui-react-app/src/lib/hooks/useCreatePermisson.ts new file mode 100644 index 00000000000..79777aa8cbe --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/useCreatePermisson.ts @@ -0,0 +1,14 @@ +import { useContext } from 'react'; +import { ResourceType } from 'generated-sources'; +import { UserInfoRolesAccessContext } from 'components/contexts/UserInfoRolesAccessContext'; +import { ClusterNameRoute } from 'lib/paths'; +import { isPermittedToCreate } from 'lib/permissions'; + +import useAppParams from './useAppParams'; + +export function useCreatePermission(resource: ResourceType): boolean { + const { clusterName } = useAppParams(); + const { roles, rbacFlag } = useContext(UserInfoRolesAccessContext); + + return isPermittedToCreate({ roles, resource, clusterName, rbacFlag }); +} diff --git a/kafka-ui-react-app/src/lib/hooks/useDataSaver.ts b/kafka-ui-react-app/src/lib/hooks/useDataSaver.ts index f39bac32058..9bcc1036794 100644 --- a/kafka-ui-react-app/src/lib/hooks/useDataSaver.ts +++ b/kafka-ui-react-app/src/lib/hooks/useDataSaver.ts @@ -1,46 +1,36 @@ -import { isObject } from 'lodash'; -import { alertAdded, alertDissmissed } from 'redux/reducers/alerts/alertsSlice'; -import { useAppDispatch } from 'lib/hooks/redux'; - -const AUTO_DISMISS_TIME = 2000; +import { showAlert, showSuccessAlert } from 'lib/errorHandling'; const useDataSaver = ( subject: string, data: Record | string ) => { - const dispatch = useAppDispatch(); const copyToClipboard = () => { if (navigator.clipboard) { const str = typeof data === 'string' ? String(data) : JSON.stringify(data); navigator.clipboard.writeText(str); - dispatch( - alertAdded({ - id: subject, - type: 'success', - title: '', - message: 'Copied successfully!', - createdAt: Date.now(), - }) - ); - setTimeout(() => dispatch(alertDissmissed(subject)), AUTO_DISMISS_TIME); + showSuccessAlert({ + id: subject, + title: '', + message: 'Copied successfully!', + }); + } else { + showAlert('warning', { + id: subject, + title: 'Warning', + message: + 'Copying to clipboard is unavailable due to unsecured (non-HTTPS) connection', + }); } }; - const saveFile = () => { - const extension = isObject(data) ? 'json' : 'txt'; - const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent( - JSON.stringify(data) - )}`; - const downloadAnchorNode = document.createElement('a'); - downloadAnchorNode.setAttribute('href', dataStr); - downloadAnchorNode.setAttribute( - 'download', - `${subject}_${new Date().getTime()}.${extension}` - ); - document.body.appendChild(downloadAnchorNode); - downloadAnchorNode.click(); - downloadAnchorNode.remove(); + const blob = new Blob([data as BlobPart], { type: 'text/json' }); + const elem = window.document.createElement('a'); + elem.href = window.URL.createObjectURL(blob); + elem.download = subject; + document.body.appendChild(elem); + elem.click(); + document.body.removeChild(elem); }; return { copyToClipboard, saveFile }; diff --git a/kafka-ui-react-app/src/lib/hooks/useInterval.ts b/kafka-ui-react-app/src/lib/hooks/useInterval.ts deleted file mode 100644 index 5c314030c5c..00000000000 --- a/kafka-ui-react-app/src/lib/hooks/useInterval.ts +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; - -type Callback = () => void; - -const useInterval = (callback: Callback, delay: number) => { - const savedCallback = React.useRef(); - - React.useEffect(() => { - savedCallback.current = callback; - }, [callback]); - - // eslint-disable-next-line consistent-return - React.useEffect(() => { - const tick = () => { - if (savedCallback.current) savedCallback.current(); - }; - - if (delay !== null) { - const id = setInterval(tick, delay); - return () => clearInterval(id); - } - }, [delay]); -}; - -export default useInterval; diff --git a/kafka-ui-react-app/src/lib/hooks/useLocalStorage.ts b/kafka-ui-react-app/src/lib/hooks/useLocalStorage.ts new file mode 100644 index 00000000000..d8945620db3 --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/useLocalStorage.ts @@ -0,0 +1,20 @@ +import { LOCAL_STORAGE_KEY_PREFIX } from 'lib/constants'; +import { useState, useEffect } from 'react'; + +export const useLocalStorage = (featureKey: string, defaultValue: string) => { + const key = `${LOCAL_STORAGE_KEY_PREFIX}-${featureKey}`; + const [value, setValue] = useState(() => { + const saved = localStorage.getItem(key); + + if (saved !== null) { + return JSON.parse(saved); + } + return defaultValue; + }); + + useEffect(() => { + localStorage.setItem(key, JSON.stringify(value)); + }, [key, value]); + + return [value, setValue]; +}; diff --git a/kafka-ui-react-app/src/lib/hooks/useMessageFiltersStore.ts b/kafka-ui-react-app/src/lib/hooks/useMessageFiltersStore.ts new file mode 100644 index 00000000000..8397d41d273 --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/useMessageFiltersStore.ts @@ -0,0 +1,41 @@ +import { LOCAL_STORAGE_KEY_PREFIX } from 'lib/constants'; +import create from 'zustand'; +import { persist } from 'zustand/middleware'; + +interface AdvancedFilter { + name: string; + value: string; +} + +interface MessageFiltersState { + filters: AdvancedFilter[]; + activeFilter?: AdvancedFilter; + save: (filter: AdvancedFilter) => void; + apply: (filter: AdvancedFilter) => void; + remove: (name: string) => void; + update: (name: string, filter: AdvancedFilter) => void; +} + +export const useMessageFiltersStore = create()( + persist( + (set) => ({ + filters: [], + save: (filter) => + set((state) => ({ + filters: [...state.filters, filter], + })), + apply: (filter) => set(() => ({ activeFilter: filter })), + remove: (name) => + set((state) => ({ + filters: state.filters.filter((f) => f.name !== name), + })), + update: (name, filter) => + set((state) => ({ + filters: state.filters.map((f) => (f.name === name ? filter : f)), + })), + }), + { + name: `${LOCAL_STORAGE_KEY_PREFIX}-message-filters`, + } + ) +); diff --git a/kafka-ui-react-app/src/lib/hooks/useModal.ts b/kafka-ui-react-app/src/lib/hooks/useModal.ts deleted file mode 100644 index 88a417aab4c..00000000000 --- a/kafka-ui-react-app/src/lib/hooks/useModal.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useCallback, useState } from 'react'; - -interface UseModalReturn { - isOpen: boolean; - setOpen(): void; - setClose(): void; - toggle(): void; -} -const useModal = (initialModalState?: boolean): UseModalReturn => { - const [modalOpen, setModalOpen] = useState(!!initialModalState); - - const setOpen = useCallback(() => { - setModalOpen(true); - }, []); - - const setClose = useCallback(() => { - setModalOpen(false); - }, []); - - const toggle = useCallback(() => { - setModalOpen((prev) => !prev); - }, []); - - return { - isOpen: modalOpen, - setOpen, - setClose, - toggle, - }; -}; - -export default useModal; diff --git a/kafka-ui-react-app/src/lib/hooks/usePagination.ts b/kafka-ui-react-app/src/lib/hooks/usePagination.ts deleted file mode 100644 index 6ce01340535..00000000000 --- a/kafka-ui-react-app/src/lib/hooks/usePagination.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useLocation } from 'react-router-dom'; - -const usePagination = () => { - const { search, pathname } = useLocation(); - const params = new URLSearchParams(search); - - const page = params.get('page'); - const perPage = params.get('perPage'); - - return { - page: page ? Number(page) : undefined, - perPage: perPage ? Number(perPage) : undefined, - pathname, - }; -}; - -export default usePagination; diff --git a/kafka-ui-react-app/src/lib/hooks/usePermission.ts b/kafka-ui-react-app/src/lib/hooks/usePermission.ts new file mode 100644 index 00000000000..269d36caa6a --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/usePermission.ts @@ -0,0 +1,17 @@ +import { useContext } from 'react'; +import { Action, ResourceType } from 'generated-sources'; +import { UserInfoRolesAccessContext } from 'components/contexts/UserInfoRolesAccessContext'; +import { ClusterNameRoute } from 'lib/paths'; +import { isPermitted } from 'lib/permissions'; +import useAppParams from 'lib/hooks/useAppParams'; + +export function usePermission( + resource: ResourceType, + action: Action | Array, + value?: string +): boolean { + const { clusterName } = useAppParams(); + const { roles, rbacFlag } = useContext(UserInfoRolesAccessContext); + + return isPermitted({ roles, resource, action, clusterName, value, rbacFlag }); +} diff --git a/kafka-ui-react-app/src/lib/hooks/useSearch.ts b/kafka-ui-react-app/src/lib/hooks/useSearch.ts deleted file mode 100644 index a1d4be5ec52..00000000000 --- a/kafka-ui-react-app/src/lib/hooks/useSearch.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { useCallback, useEffect, useMemo } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; - -const SEARCH_QUERY_ARG = 'q'; - -// meant for use with component -// returns value of Q search param (?q='something') and callback to change it -const useSearch = (initValue = ''): [string, (value: string) => void] => { - const navigate = useNavigate(); - const { search } = useLocation(); - const queryParams = useMemo(() => new URLSearchParams(search), [search]); - const q = useMemo( - () => queryParams.get(SEARCH_QUERY_ARG)?.trim(), - [queryParams] - ); - const page = useMemo(() => queryParams.get('page')?.trim(), [queryParams]); - - // set intial value - useEffect(() => { - if (initValue.trim() !== '' && !q) { - queryParams.set(SEARCH_QUERY_ARG, initValue.trim()); - navigate({ search: queryParams.toString() }); - } - }, [navigate, initValue, q, queryParams]); - - const handleChange = useCallback( - (value: string) => { - const trimmedValue = value.trim(); - if (trimmedValue !== q) { - if (trimmedValue) { - queryParams.set(SEARCH_QUERY_ARG, trimmedValue); - } else { - queryParams.delete(SEARCH_QUERY_ARG); - } - // If we were on page 3 we can't determine if new search results have 3 pages - so we always reset page - if (page) { - queryParams.delete('page'); - } - navigate( - { - search: queryParams.toString(), - }, - { replace: true } - ); - } - }, - [q, page, navigate, queryParams] - ); - - return [q || initValue.trim() || '', handleChange]; -}; - -export default useSearch; diff --git a/kafka-ui-react-app/src/lib/hooks/useTableState.ts b/kafka-ui-react-app/src/lib/hooks/useTableState.ts deleted file mode 100644 index b5fa6ed79c1..00000000000 --- a/kafka-ui-react-app/src/lib/hooks/useTableState.ts +++ /dev/null @@ -1,78 +0,0 @@ -import React, { useCallback } from 'react'; -import { OrderableProps } from 'components/common/SmartTable/TableColumn'; - -export interface TableState { - data: T[]; - selectedIds: Set; - totalPages?: number; - idSelector: (row: T) => TId; - isRowSelectable: (row: T) => boolean; - selectedCount: number; - setRowsSelection: (rows: T[], selected: boolean) => void; - toggleSelection: (selected: boolean) => void; - orderable?: OrderableProps; -} - -export const useTableState = ( - data: T[], - options: { - totalPages: number; - isRowSelectable?: (row: T) => boolean; - idSelector: (row: T) => TId; - }, - orderable?: OrderableProps -): TableState => { - const [selectedIds, setSelectedIds] = React.useState(new Set()); - - const { idSelector, totalPages, isRowSelectable = () => true } = options; - - const selectedCount = selectedIds.size; - - const setRowsSelection = useCallback( - (rows: T[], selected: boolean) => { - rows.forEach((row) => { - const id = idSelector(row); - const newSet = new Set(selectedIds); - if (selected) { - newSet.add(id); - } else { - newSet.delete(id); - } - setSelectedIds(newSet); - }); - }, - [idSelector, selectedIds] - ); - - const toggleSelection = useCallback( - (selected: boolean) => { - const newSet = new Set(selected ? data.map((r) => idSelector(r)) : []); - setSelectedIds(newSet); - }, - [data, idSelector] - ); - - return React.useMemo>(() => { - return { - data, - totalPages, - selectedIds, - orderable, - selectedCount, - idSelector, - isRowSelectable, - setRowsSelection, - toggleSelection, - }; - }, [ - data, - orderable, - selectedIds, - totalPages, - selectedCount, - idSelector, - isRowSelectable, - setRowsSelection, - toggleSelection, - ]); -}; diff --git a/kafka-ui-react-app/src/lib/hooks/useUserInfo.ts b/kafka-ui-react-app/src/lib/hooks/useUserInfo.ts new file mode 100644 index 00000000000..6ae28f93ace --- /dev/null +++ b/kafka-ui-react-app/src/lib/hooks/useUserInfo.ts @@ -0,0 +1,6 @@ +import { useContext } from 'react'; +import { UserInfoRolesAccessContext } from 'components/contexts/UserInfoRolesAccessContext'; + +export function useUserInfo() { + return useContext(UserInfoRolesAccessContext); +} diff --git a/kafka-ui-react-app/src/lib/paths.ts b/kafka-ui-react-app/src/lib/paths.ts index a608d136aa6..9cee7ca285d 100644 --- a/kafka-ui-react-app/src/lib/paths.ts +++ b/kafka-ui-react-app/src/lib/paths.ts @@ -1,13 +1,8 @@ -import { - ClusterName, - ConnectName, - ConnectorName, - ConsumerGroupID, - SchemaName, - TopicName, -} from 'redux/interfaces'; +import { Broker, Connect, Connector } from 'generated-sources'; +import { ClusterName, SchemaName, TopicName } from 'redux/interfaces'; import { GIT_REPO_LINK } from './constants'; +import { ConsumerGroupID } from './hooks/api/consumers'; export const gitCommitPath = (commit: string) => `${GIT_REPO_LINK}/commit/${commit}`; @@ -19,10 +14,14 @@ export enum RouteParams { topicName = ':topicName', connectName = ':connectName', connectorName = ':connectorName', + brokerId = ':brokerId', } export const getNonExactPath = (path: string) => `${path}/*`; +export const errorPage = '/404'; +export const accessErrorPage = '/403'; + export const clusterPath = ( clusterName: ClusterName = RouteParams.clusterName ) => `/ui/clusters/${clusterName}`; @@ -31,10 +30,40 @@ export type ClusterNameRoute = { clusterName: ClusterName }; // Brokers export const clusterBrokerRelativePath = 'brokers'; +export const clusterBrokerMetricsRelativePath = 'metrics'; +export const clusterBrokerConfigsRelativePath = 'configs'; + export const clusterBrokersPath = ( clusterName: ClusterName = RouteParams.clusterName ) => `${clusterPath(clusterName)}/${clusterBrokerRelativePath}`; +export const clusterBrokerPath = ( + clusterName: ClusterName = RouteParams.clusterName, + brokerId: Broker['id'] | string = RouteParams.brokerId +) => `${clusterBrokersPath(clusterName)}/${brokerId}`; +export const clusterBrokerMetricsPath = ( + clusterName: ClusterName = RouteParams.clusterName, + brokerId: Broker['id'] | string = RouteParams.brokerId +) => + `${clusterBrokerPath( + clusterName, + brokerId + )}/${clusterBrokerMetricsRelativePath}`; + +export const clusterBrokerConfigsPath = ( + clusterName: ClusterName = RouteParams.clusterName, + brokerId: Broker['id'] | string = RouteParams.brokerId +) => + `${clusterBrokerPath( + clusterName, + brokerId + )}/${clusterBrokerConfigsRelativePath}`; + +export type ClusterBrokerParam = { + clusterName: ClusterName; + brokerId: string; +}; + // Consumer Groups export const clusterConsumerGroupsRelativePath = 'consumer-groups'; export const clusterConsumerGroupResetRelativePath = 'reset-offsets'; @@ -63,9 +92,9 @@ export type ClusterGroupParam = { export const clusterSchemasRelativePath = 'schemas'; export const clusterSchemaNewRelativePath = 'create-new'; export const clusterSchemaEditPageRelativePath = `edit`; -export const clusterSchemaSchemaDiffPageRelativePath = `diff`; +export const clusterSchemaSchemaComparePageRelativePath = `compare`; export const clusterSchemaEditRelativePath = `${RouteParams.subject}/${clusterSchemaEditPageRelativePath}`; -export const clusterSchemaSchemaDiffRelativePath = `${RouteParams.subject}/${clusterSchemaSchemaDiffPageRelativePath}`; +export const clusterSchemaSchemaDiffRelativePath = `${RouteParams.subject}/${clusterSchemaSchemaComparePageRelativePath}`; export const clusterSchemasPath = ( clusterName: ClusterName = RouteParams.clusterName ) => `${clusterPath(clusterName)}/schemas`; @@ -75,15 +104,23 @@ export const clusterSchemaNewPath = ( export const clusterSchemaPath = ( clusterName: ClusterName = RouteParams.clusterName, subject: SchemaName = RouteParams.subject -) => `${clusterSchemasPath(clusterName)}/${subject}`; +) => { + let subjectName = subject; + if (subject !== ':subject') subjectName = encodeURIComponent(subject); + return `${clusterSchemasPath(clusterName)}/${subjectName}`; +}; export const clusterSchemaEditPath = ( clusterName: ClusterName = RouteParams.clusterName, subject: SchemaName = RouteParams.subject -) => `${clusterSchemasPath(clusterName)}/${subject}/edit`; -export const clusterSchemaSchemaDiffPath = ( +) => { + let subjectName = subject; + if (subject !== ':subject') subjectName = encodeURIComponent(subject); + return `${clusterSchemasPath(clusterName)}/${subjectName}/edit`; +}; +export const clusterSchemaComparePath = ( clusterName: ClusterName = RouteParams.clusterName, subject: SchemaName = RouteParams.subject -) => `${clusterSchemaPath(clusterName, subject)}/diff`; +) => `${clusterSchemaPath(clusterName, subject)}/compare`; export type ClusterSubjectParam = { subject: string; @@ -91,8 +128,8 @@ export type ClusterSubjectParam = { }; // Topics -export const clusterTopicsRelativePath = 'topics'; -export const clusterTopicNewRelativePath = 'create-new'; +export const clusterTopicsRelativePath = 'all-topics'; +export const clusterTopicNewRelativePath = 'create-new-topic'; export const clusterTopicCopyRelativePath = 'copy'; export const clusterTopicsPath = ( clusterName: ClusterName = RouteParams.clusterName @@ -108,8 +145,8 @@ export const clusterTopicCopyPath = ( export const clusterTopicSettingsRelativePath = 'settings'; export const clusterTopicMessagesRelativePath = 'messages'; export const clusterTopicConsumerGroupsRelativePath = 'consumer-groups'; +export const clusterTopicStatisticsRelativePath = 'statistics'; export const clusterTopicEditRelativePath = 'edit'; -export const clusterTopicSendMessageRelativePath = 'message'; export const clusterTopicPath = ( clusterName: ClusterName = RouteParams.clusterName, topicName: TopicName = RouteParams.topicName @@ -143,14 +180,14 @@ export const clusterTopicConsumerGroupsPath = ( clusterName, topicName )}/${clusterTopicConsumerGroupsRelativePath}`; -export const clusterTopicSendMessagePath = ( +export const clusterTopicStatisticsPath = ( clusterName: ClusterName = RouteParams.clusterName, topicName: TopicName = RouteParams.topicName ) => `${clusterTopicPath( clusterName, topicName - )}/${clusterTopicSendMessageRelativePath}`; + )}/${clusterTopicStatisticsRelativePath}`; export type RouteParamsClusterTopic = { clusterName: ClusterName; @@ -163,8 +200,7 @@ export const clusterConnectorsRelativePath = 'connectors'; export const clusterConnectorNewRelativePath = 'create-new'; export const clusterConnectConnectorsRelativePath = `${RouteParams.connectName}/connectors`; export const clusterConnectConnectorRelativePath = `${clusterConnectConnectorsRelativePath}/${RouteParams.connectorName}`; -export const clusterConnectConnectorEditRelativePath = `${clusterConnectConnectorRelativePath}/edit`; -export const clusterConnectConnectorTasksRelativePath = 'tasks'; +const clusterConnectConnectorTasksRelativePath = 'tasks'; export const clusterConnectConnectorConfigRelativePath = 'config'; export const clusterConnectsPath = ( @@ -178,18 +214,18 @@ export const clusterConnectorNewPath = ( ) => `${clusterConnectorsPath(clusterName)}/create-new`; export const clusterConnectConnectorsPath = ( clusterName: ClusterName = RouteParams.clusterName, - connectName: ConnectName = RouteParams.connectName + connectName: Connect['name'] = RouteParams.connectName ) => `${clusterConnectsPath(clusterName)}/${connectName}/connectors`; export const clusterConnectConnectorPath = ( clusterName: ClusterName = RouteParams.clusterName, - connectName: ConnectName = RouteParams.connectName, - connectorName: ConnectorName = RouteParams.connectorName + connectName: Connect['name'] = RouteParams.connectName, + connectorName: Connector['name'] = RouteParams.connectorName ) => `${clusterConnectConnectorsPath(clusterName, connectName)}/${connectorName}`; export const clusterConnectConnectorEditPath = ( clusterName: ClusterName = RouteParams.clusterName, - connectName: ConnectName = RouteParams.connectName, - connectorName: ConnectorName = RouteParams.connectorName + connectName: Connect['name'] = RouteParams.connectName, + connectorName: Connector['name'] = RouteParams.connectorName ) => `${clusterConnectConnectorsPath( clusterName, @@ -197,8 +233,8 @@ export const clusterConnectConnectorEditPath = ( )}/${connectorName}/edit`; export const clusterConnectConnectorTasksPath = ( clusterName: ClusterName = RouteParams.clusterName, - connectName: ConnectName = RouteParams.connectName, - connectorName: ConnectorName = RouteParams.connectorName + connectName: Connect['name'] = RouteParams.connectName, + connectorName: Connector['name'] = RouteParams.connectorName ) => `${clusterConnectConnectorPath( clusterName, @@ -207,26 +243,52 @@ export const clusterConnectConnectorTasksPath = ( )}/${clusterConnectConnectorTasksRelativePath}`; export const clusterConnectConnectorConfigPath = ( clusterName: ClusterName = RouteParams.clusterName, - connectName: ConnectName = RouteParams.connectName, - connectorName: ConnectorName = RouteParams.connectorName + connectName: Connect['name'] = RouteParams.connectName, + connectorName: Connector['name'] = RouteParams.connectorName ) => `${clusterConnectConnectorPath( clusterName, connectName, connectorName )}/${clusterConnectConnectorConfigRelativePath}`; + export type RouterParamsClusterConnectConnector = { clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; + connectName: Connect['name']; + connectorName: Connector['name']; }; // KsqlDb export const clusterKsqlDbRelativePath = 'ksqldb'; export const clusterKsqlDbQueryRelativePath = 'query'; +export const clusterKsqlDbTablesRelativePath = 'tables'; +export const clusterKsqlDbStreamsRelativePath = 'streams'; + export const clusterKsqlDbPath = ( clusterName: ClusterName = RouteParams.clusterName ) => `${clusterPath(clusterName)}/${clusterKsqlDbRelativePath}`; export const clusterKsqlDbQueryPath = ( clusterName: ClusterName = RouteParams.clusterName ) => `${clusterKsqlDbPath(clusterName)}/${clusterKsqlDbQueryRelativePath}`; +export const clusterKsqlDbTablesPath = ( + clusterName: ClusterName = RouteParams.clusterName +) => `${clusterKsqlDbPath(clusterName)}/${clusterKsqlDbTablesRelativePath}`; +export const clusterKsqlDbStreamsPath = ( + clusterName: ClusterName = RouteParams.clusterName +) => `${clusterKsqlDbPath(clusterName)}/${clusterKsqlDbStreamsRelativePath}`; + +// Cluster Config +export const clusterConfigRelativePath = 'config'; +export const clusterConfigPath = ( + clusterName: ClusterName = RouteParams.clusterName +) => `${clusterPath(clusterName)}/${clusterConfigRelativePath}`; + +const clusterNewConfigRelativePath = 'create-new-cluster'; +export const clusterNewConfigPath = `/ui/clusters/${clusterNewConfigRelativePath}`; + +// ACL +export const clusterAclRelativePath = 'acl'; +export const clusterAclNewRelativePath = 'create-new-acl'; +export const clusterACLPath = ( + clusterName: ClusterName = RouteParams.clusterName +) => `${clusterPath(clusterName)}/${clusterAclRelativePath}`; diff --git a/kafka-ui-react-app/src/lib/permissions.ts b/kafka-ui-react-app/src/lib/permissions.ts new file mode 100644 index 00000000000..e72ffecf958 --- /dev/null +++ b/kafka-ui-react-app/src/lib/permissions.ts @@ -0,0 +1,151 @@ +import { Action, ResourceType, UserPermission } from 'generated-sources'; + +export type RolesType = UserPermission[]; + +export type RolesModifiedTypes = Map>; + +const ResourceExemptList: ResourceType[] = [ + ResourceType.KSQL, + ResourceType.CLUSTERCONFIG, + ResourceType.APPLICATIONCONFIG, + ResourceType.ACL, + ResourceType.AUDIT, +]; + +export function modifyRolesData( + data?: RolesType +): Map> { + const map = new Map>(); + + data?.forEach((item) => { + item.clusters.forEach((name) => { + const cluster = map.get(name); + if (cluster) { + const { resource } = item; + + const resourceItem = cluster.get(resource); + if (resourceItem) { + cluster.set(resource, resourceItem.concat(item)); + return; + } + cluster.set(resource, [item]); + return; + } + + map.set(name, new Map().set(item.resource, [item])); + }); + }); + return map; +} + +interface IsPermittedConfig { + roles?: RolesModifiedTypes; + resource: ResourceType; + action: Action | Array; + clusterName: string; + value?: string; + rbacFlag: boolean; +} + +const valueMatches = (regexp: string | undefined, val: string | undefined) => { + if (!val) return false; + if (!regexp) return true; + return new RegExp(regexp).test(val); +}; + +/** + * @description it the logic behind depending on the roles whether a certain action + * is permitted or not the philosophy is inspired from Headless UI libraries where + * you separate the logic from the renderer besides the Creation process which is handled by isPermittedToCreate + * + * Algorithm: we Mapped the cluster name and the resource name , because all the actions in them are + * constant and limited and hence faster lookup approach + * + * @example you can use this in the hook format where it used in , or if you want to calculate it dynamically + * you can call this dynamically in your component but the render is on you from that point on + * + * Don't use this anywhere , use the hook version in the component for declarative purposes + * + * Array action approach bear in mind they should be from the same resource with the same name restrictions, then the logic it + * will try to find every element from the given array inside the permissions data + * + * DON'T use the array approach until it is necessary to do so + * + * */ +export function isPermitted({ + roles, + resource, + action, + clusterName, + value, + rbacFlag, +}: { + roles?: RolesModifiedTypes; + resource: ResourceType; + action: Action | Array; + clusterName: string; + value?: string; + rbacFlag: boolean; +}) { + if (!rbacFlag) return true; + + // short circuit + if (!roles || roles.size === 0) return false; + + // short circuit + const clusterMap = roles.get(clusterName); + if (!clusterMap) return false; + + // short circuit + const resourcePermissions = clusterMap.get(resource); + if (!resourcePermissions) return false; + + const actions = Array.isArray(action) ? action : [action]; + + return actions.every((a) => { + return resourcePermissions.some((item) => { + if (!item.actions.includes(a)) return false; + if (ResourceExemptList.includes(resource)) return true; + return valueMatches(item.value, value); + }); + }); +} + +/** + * @description it the logic behind depending on create roles, since create has extra custom permission logic that is why + * it is seperated from the others + * + * Algorithm: we Mapped the cluster name and the resource name , because all the actions in them are + * constant and limited and hence faster lookup approach + * + * @example you can use this in the hook format where it used in , or if you want to calculate it dynamically + * you can call this dynamically in your component but the render is on you from that point on + * + * Don't use this anywhere , use the hook version in the component for declarative purposes + * + * */ +export function isPermittedToCreate({ + roles, + resource, + clusterName, + rbacFlag, +}: Omit) { + if (!rbacFlag) return true; + + // short circuit + if (!roles || roles.size === 0) return false; + + // short circuit + const clusterMap = roles.get(clusterName); + if (!clusterMap) return false; + + // short circuit + const resourceData = clusterMap.get(resource); + if (!resourceData) return false; + + const action = Action.CREATE; + + return resourceData.some((item) => { + return item.actions.includes(action); + }); +} diff --git a/kafka-ui-react-app/src/lib/propertyLookup.ts b/kafka-ui-react-app/src/lib/propertyLookup.ts deleted file mode 100644 index 6a563ca623d..00000000000 --- a/kafka-ui-react-app/src/lib/propertyLookup.ts +++ /dev/null @@ -1,9 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function propertyLookup( - path: string, - obj: T -) { - return path.split('.').reduce((prev, curr) => { - return prev ? prev[curr] : null; - }, obj); -} diff --git a/kafka-ui-react-app/src/lib/testHelpers.tsx b/kafka-ui-react-app/src/lib/testHelpers.tsx index 40f85a49203..06fcfcad495 100644 --- a/kafka-ui-react-app/src/lib/testHelpers.tsx +++ b/kafka-ui-react-app/src/lib/testHelpers.tsx @@ -1,43 +1,64 @@ -import React, { PropsWithChildren, ReactElement } from 'react'; +import React, { PropsWithChildren, ReactElement, useMemo } from 'react'; import { MemoryRouter, MemoryRouterProps, Route, Routes, } from 'react-router-dom'; +import fetchMock from 'fetch-mock'; import { Provider } from 'react-redux'; import { ThemeProvider } from 'styled-components'; -import theme from 'theme/theme'; -import { render, RenderOptions, screen } from '@testing-library/react'; +import { theme } from 'theme/theme'; +import { + render, + renderHook, + RenderOptions, + waitFor, +} from '@testing-library/react'; import { AnyAction, Store } from 'redux'; import { RootState } from 'redux/interfaces'; import { configureStore } from '@reduxjs/toolkit'; import rootReducer from 'redux/reducers'; -import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator'; +import { + QueryClient, + QueryClientProvider, + UseQueryResult, +} from '@tanstack/react-query'; +import { ConfirmContextProvider } from 'components/contexts/ConfirmContext'; +import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; +import { GlobalSettingsContext } from 'components/contexts/GlobalSettingsContext'; +import { UserInfoRolesAccessContext } from 'components/contexts/UserInfoRolesAccessContext'; + +import { RolesType, modifyRolesData } from './permissions'; interface CustomRenderOptions extends Omit { preloadedState?: Partial; store?: Store, AnyAction>; initialEntries?: MemoryRouterProps['initialEntries']; + userInfo?: { + roles?: RolesType; + rbacFlag: boolean; + }; + globalSettings?: { + hasDynamicConfig: boolean; + }; } -export function getByTextContent(textMatch: string | RegExp): HTMLElement { - return screen.getByText((content, node) => { - const hasText = (nod: Element) => nod.textContent === textMatch; - const nodeHasText = hasText(node as Element); - const childrenDontHaveText = Array.from(node?.children || []).every( - (child) => !hasText(child) - ); - return nodeHasText && childrenDontHaveText; - }); -} - -interface WithRouterProps { +interface WithRouteProps { children: React.ReactNode; path: string; } -export const WithRoute: React.FC = ({ children, path }) => { +export const expectQueryWorks = async ( + mock: fetchMock.FetchMockStatic, + result: { current: UseQueryResult } +) => { + await waitFor(() => expect(result.current.isFetched).toBeTruthy()); + expect(mock.calls()).toHaveLength(1); + expect(result.current.data).toBeDefined(); +}; + +export const WithRoute: React.FC = ({ children, path }) => { return ( @@ -45,6 +66,44 @@ export const WithRoute: React.FC = ({ children, path }) => { ); }; +export const TestQueryClientProvider: React.FC> = ({ + children, +}) => { + // use new QueryClient instance for each test run to avoid issues with cache + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + return ( + {children} + ); +}; + +/** + * @description it will create a UserInfo Provider that will actually + * disable the rbacFlag , to user if you can pass it as an argument + * */ +const TestUserInfoProvider: React.FC< + PropsWithChildren<{ data?: { roles?: RolesType; rbacFlag: boolean } }> +> = ({ children, data }) => { + const contextValue = useMemo(() => { + const roles = modifyRolesData(data?.roles); + + return { + username: 'test', + rbacFlag: !!(typeof data?.rbacFlag === 'undefined' + ? false + : data?.rbacFlag), + roles, + }; + }, [data]); + + return ( + + {children} + + ); +}; + const customRender = ( ui: ReactElement, { @@ -54,27 +113,43 @@ const customRender = ( preloadedState, }), initialEntries, + userInfo, + globalSettings, ...renderOptions }: CustomRenderOptions = {} ) => { // overrides @testing-library/react render. const AllTheProviders: React.FC> = ({ children, - }) => { - return ( - - - - {children} - - - - ); - }; + }) => ( + + + + + + + +
    + {children} + +
    +
    +
    +
    +
    +
    +
    +
    + ); return render(ui, { wrapper: AllTheProviders, ...renderOptions }); }; -export { customRender as render }; +const customRenderHook = (hook: () => UseQueryResult) => + renderHook(hook, { wrapper: TestQueryClientProvider }); + +export { customRender as render, customRenderHook as renderQueryHook }; export class EventSourceMock { url: string; @@ -95,12 +170,3 @@ export class EventSourceMock { this.close = jest.fn(); } } - -export const getTypeAndPayload = (store: typeof mockStoreCreator) => { - return store.getActions().map(({ type, payload }) => ({ type, payload })); -}; - -export const getAlertActions = (mockStore: typeof mockStoreCreator) => - getTypeAndPayload(mockStore).filter((currentAction: AnyAction) => - currentAction.type.startsWith('alerts') - ); diff --git a/kafka-ui-react-app/src/lib/yupExtended.ts b/kafka-ui-react-app/src/lib/yupExtended.ts index db185821095..ad0c9fb1923 100644 --- a/kafka-ui-react-app/src/lib/yupExtended.ts +++ b/kafka-ui-react-app/src/lib/yupExtended.ts @@ -1,15 +1,15 @@ import * as yup from 'yup'; -import { AnyObject, Maybe } from 'yup/lib/types'; import { TOPIC_NAME_VALIDATION_PATTERN } from './constants'; declare module 'yup' { interface StringSchema< - TType extends Maybe = string | undefined, - TContext extends AnyObject = AnyObject, - TOut extends TType = TType - > extends yup.BaseSchema { - isJsonObject(): StringSchema; + TType extends yup.Maybe = string | undefined, + TContext = yup.AnyObject, + TDefault = undefined, + TFlags extends yup.Flags = '' + > extends yup.Schema { + isJsonObject(message?: string): StringSchema; } } @@ -31,53 +31,58 @@ export const isValidJsonObject = (value?: string) => { return false; }; -const isJsonObject = () => { +const isJsonObject = (message?: string) => { return yup.string().test( 'isJsonObject', // eslint-disable-next-line no-template-curly-in-string - '${path} is not JSON object', + message || '${path} is not JSON object', isValidJsonObject ); }; +/** + * due to yup rerunning all the object validiation during any render, + * it makes sense to cache the async results + * */ +export function cacheTest( + asyncValidate: (val?: string, ctx?: yup.AnyObject) => Promise +) { + let valid = false; + let closureValue = ''; -yup.addMethod(yup.string, 'isJsonObject', isJsonObject); + return async (value?: string, ctx?: yup.AnyObject) => { + if (value !== closureValue) { + const response = await asyncValidate(value, ctx); + closureValue = value || ''; + valid = response; + return response; + } + return valid; + }; +} -export default yup; +yup.addMethod(yup.StringSchema, 'isJsonObject', isJsonObject); export const topicFormValidationSchema = yup.object().shape({ name: yup .string() - .required() + .max(249) + .required('Topic Name is required') .matches( TOPIC_NAME_VALIDATION_PATTERN, 'Only alphanumeric, _, -, and . allowed' ), partitions: yup .number() - .min(1) + .min(1, 'Number of Partitions must be greater than or equal to 1') + .max(2147483647) .required() - .typeError('Number of partitions is required and must be a number'), - replicationFactor: yup - .number() - .min(1) - .required() - .typeError('Replication factor is required and must be a number'), - minInsyncReplicas: yup - .number() - .min(1) - .required() - .typeError('Min in sync replicas is required and must be a number'), + .typeError('Number of Partitions is required and must be a number'), + replicationFactor: yup.string(), + minInSyncReplicas: yup.string(), cleanupPolicy: yup.string().required(), - retentionMs: yup - .number() - .min(-1, 'Must be greater than or equal to -1') - .typeError('Time to retain data is required and must be a number'), + retentionMs: yup.string(), retentionBytes: yup.number(), - maxMessageBytes: yup - .number() - .min(1) - .required() - .typeError('Maximum message size is required and must be a number'), + maxMessageBytes: yup.string(), customParams: yup.array().of( yup.object().shape({ name: yup.string().required('Custom parameter is required'), @@ -85,3 +90,5 @@ export const topicFormValidationSchema = yup.object().shape({ }) ), }); + +export default yup; diff --git a/kafka-ui-react-app/src/react-app-env.d.ts b/kafka-ui-react-app/src/react-app-env.d.ts index 30da8962982..1f42c255ef2 100644 --- a/kafka-ui-react-app/src/react-app-env.d.ts +++ b/kafka-ui-react-app/src/react-app-env.d.ts @@ -1 +1,3 @@ -// / +/// +/// +/// diff --git a/kafka-ui-react-app/src/redux/actions/__test__/fixtures.ts b/kafka-ui-react-app/src/redux/actions/__test__/fixtures.ts deleted file mode 100644 index c7aaff49682..00000000000 --- a/kafka-ui-react-app/src/redux/actions/__test__/fixtures.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - ClusterStats, - CompatibilityLevelCompatibilityEnum, - NewSchemaSubject, - SchemaSubject, - SchemaType, - SortOrder, -} from 'generated-sources'; - -export const clusterStats: ClusterStats = { - brokerCount: 1, - activeControllers: 1, - onlinePartitionCount: 6, - offlinePartitionCount: 0, - inSyncReplicasCount: 6, - outOfSyncReplicasCount: 0, - underReplicatedPartitionCount: 0, - diskUsage: [{ brokerId: 1, segmentSize: 6538, segmentCount: 6 }], -}; - -export const schemaPayload: NewSchemaSubject = { - subject: 'NewSchema', - schema: - '{"type":"record","name":"MyRecord1","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}', - schemaType: SchemaType.JSON, -}; - -export const schema: SchemaSubject = { - subject: 'NewSchema', - schema: - '{"type":"record","name":"MyRecord1","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}', - schemaType: SchemaType.JSON, - version: '1', - id: 1, - compatibilityLevel: CompatibilityLevelCompatibilityEnum.BACKWARD, -}; - -export const mockTopicsState = { - byName: {}, - allNames: [], - totalPages: 1, - messages: [], - search: '', - orderBy: null, - sortOrder: SortOrder.ASC, - consumerGroups: [], -}; diff --git a/kafka-ui-react-app/src/redux/interfaces/alerts.ts b/kafka-ui-react-app/src/redux/interfaces/alerts.ts deleted file mode 100644 index ab423c3d005..00000000000 --- a/kafka-ui-react-app/src/redux/interfaces/alerts.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ErrorResponse } from 'generated-sources'; - -export interface ServerResponse { - status: number; - statusText: string; - url?: string; - message?: ErrorResponse['message']; -} - -export type AlertType = 'error' | 'success' | 'warning' | 'info'; - -export interface Alert { - id: string; - type: AlertType; - title: string; - message: string; - response?: ServerResponse; - createdAt: number; -} - -export type Alerts = Alert[]; - -export type AlertsState = Record; diff --git a/kafka-ui-react-app/src/redux/interfaces/broker.ts b/kafka-ui-react-app/src/redux/interfaces/broker.ts deleted file mode 100644 index b9dfa7d30d8..00000000000 --- a/kafka-ui-react-app/src/redux/interfaces/broker.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ClusterStats, Broker } from 'generated-sources'; - -export type BrokerId = Broker['id']; - -export interface BrokersState extends ClusterStats { - items: Broker[]; -} diff --git a/kafka-ui-react-app/src/redux/interfaces/cluster.ts b/kafka-ui-react-app/src/redux/interfaces/cluster.ts index d51fbaa7e0c..e417f1b194f 100644 --- a/kafka-ui-react-app/src/redux/interfaces/cluster.ts +++ b/kafka-ui-react-app/src/redux/interfaces/cluster.ts @@ -1,5 +1,3 @@ import { Cluster } from 'generated-sources'; export type ClusterName = Cluster['name']; - -export type ClusterState = Cluster[]; diff --git a/kafka-ui-react-app/src/redux/interfaces/connect.ts b/kafka-ui-react-app/src/redux/interfaces/connect.ts deleted file mode 100644 index c7ea30f5d2c..00000000000 --- a/kafka-ui-react-app/src/redux/interfaces/connect.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Connect, Connector, FullConnectorInfo, Task } from 'generated-sources'; - -import { ClusterName } from './cluster'; - -export type ConnectName = Connect['name']; -export type ConnectorName = Connector['name']; -export type ConnectorConfig = Connector['config']; - -export interface ConnectState { - connects: Connect[]; - connectors: FullConnectorInfo[]; - currentConnector: { - connector: Connector | null; - tasks: Task[]; - config: ConnectorConfig | null; - }; - search: string; -} - -export interface ConnectorSearch { - clusterName: ClusterName; - search: string; -} diff --git a/kafka-ui-react-app/src/redux/interfaces/consumerGroup.ts b/kafka-ui-react-app/src/redux/interfaces/consumerGroup.ts index 8e05a9b6288..45412e264bb 100644 --- a/kafka-ui-react-app/src/redux/interfaces/consumerGroup.ts +++ b/kafka-ui-react-app/src/redux/interfaces/consumerGroup.ts @@ -5,10 +5,9 @@ import { import { ClusterName } from './cluster'; -export type ConsumerGroupID = ConsumerGroup['groupId']; export interface ConsumerGroupResetOffsetRequestParams { clusterName: ClusterName; - consumerGroupID: ConsumerGroupID; + consumerGroupID: ConsumerGroup['groupId']; requestBody: { topic: string; resetType: ConsumerGroupOffsetsResetType; diff --git a/kafka-ui-react-app/src/redux/interfaces/index.ts b/kafka-ui-react-app/src/redux/interfaces/index.ts index 41ec56e235a..0c8ddf234e3 100644 --- a/kafka-ui-react-app/src/redux/interfaces/index.ts +++ b/kafka-ui-react-app/src/redux/interfaces/index.ts @@ -3,13 +3,9 @@ import { store } from 'redux/store'; export * from './topic'; export * from './cluster'; -export * from './broker'; export * from './consumerGroup'; export * from './schema'; export * from './loader'; -export * from './alerts'; -export * from './connect'; export type RootState = ReturnType; -export type AppStore = ReturnType; export type AppDispatch = typeof store.dispatch; diff --git a/kafka-ui-react-app/src/redux/interfaces/ksqlDb.ts b/kafka-ui-react-app/src/redux/interfaces/ksqlDb.ts deleted file mode 100644 index 2290fb3ac88..00000000000 --- a/kafka-ui-react-app/src/redux/interfaces/ksqlDb.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { KsqlCommandV2Response } from 'generated-sources'; - -export interface KsqlTables { - data: { - headers: string[]; - rows: string[][]; - }; -} - -export interface KsqlState { - tables: Dictionary[]; - streams: Dictionary[]; - executionResult: KsqlCommandV2Response | null; -} diff --git a/kafka-ui-react-app/src/redux/interfaces/loader.ts b/kafka-ui-react-app/src/redux/interfaces/loader.ts index 650d121f860..67066399eae 100644 --- a/kafka-ui-react-app/src/redux/interfaces/loader.ts +++ b/kafka-ui-react-app/src/redux/interfaces/loader.ts @@ -1,8 +1,5 @@ import { AsyncRequestStatus } from 'lib/constants'; -export interface LoaderState { - [key: string]: 'notFetched' | 'fetching' | 'fetched' | 'errorFetching'; -} export interface LoaderSliceState { [key: string]: AsyncRequestStatus; } diff --git a/kafka-ui-react-app/src/redux/interfaces/schema.ts b/kafka-ui-react-app/src/redux/interfaces/schema.ts index fd23eacff0a..c03062e5615 100644 --- a/kafka-ui-react-app/src/redux/interfaces/schema.ts +++ b/kafka-ui-react-app/src/redux/interfaces/schema.ts @@ -1,18 +1,10 @@ import { CompatibilityLevelCompatibilityEnum, NewSchemaSubject, - SchemaSubject, } from 'generated-sources'; export type SchemaName = string; -export interface SchemasState { - byName: { [subject: string]: SchemaSubject }; - allNames: SchemaName[]; - currentSchemaVersions: SchemaSubject[]; - globalSchemaCompatibilityLevel?: CompatibilityLevelCompatibilityEnum; -} - export interface NewSchemaSubjectRaw extends NewSchemaSubject { subject: string; compatibilityLevel: CompatibilityLevelCompatibilityEnum; diff --git a/kafka-ui-react-app/src/redux/interfaces/topic.ts b/kafka-ui-react-app/src/redux/interfaces/topic.ts index def2c2184a8..bdc25ee0c60 100644 --- a/kafka-ui-react-app/src/redux/interfaces/topic.ts +++ b/kafka-ui-react-app/src/redux/interfaces/topic.ts @@ -1,71 +1,33 @@ import { Topic, - TopicDetails, TopicConfig, TopicCreation, - GetTopicMessagesRequest, - ConsumerGroup, - TopicColumnsToSort, TopicMessage, TopicMessageConsuming, - TopicMessageSchema, - SortOrder, } from 'generated-sources'; export type TopicName = Topic['name']; -export type CleanupPolicy = 'delete' | 'compact'; - -export interface TopicConfigByName { - byName: TopicConfigParams; -} - export interface TopicConfigParams { [paramName: string]: TopicConfig; } -export interface TopicConfigValue { - name: TopicConfig['name']; - value: TopicConfig['value']; -} - -export interface TopicMessageQueryParams { - q: GetTopicMessagesRequest['q']; - limit: GetTopicMessagesRequest['limit']; - seekType: GetTopicMessagesRequest['seekType']; - seekTo: GetTopicMessagesRequest['seekTo']; - seekDirection: GetTopicMessagesRequest['seekDirection']; +export interface TopicConfigByName { + byName: TopicConfigParams; } -export interface TopicFormCustomParams { +interface TopicFormCustomParams { byIndex: TopicConfigParams; allIndexes: TopicName[]; } -export interface TopicWithDetailedInfo extends Topic, TopicDetails { - id?: string; - config?: TopicConfig[]; - consumerGroups?: ConsumerGroup[]; - messageSchema?: TopicMessageSchema; -} - -export interface TopicsState { - byName: { [topicName: string]: TopicWithDetailedInfo }; - allNames: TopicName[]; - totalPages: number; - search: string; - orderBy: TopicColumnsToSort | null; - sortOrder: SortOrder; - consumerGroups: ConsumerGroup[]; -} - export type TopicFormFormattedParams = TopicCreation['configs']; -export interface TopicFormDataRaw { +interface TopicFormDataModified { name: string; partitions: number; replicationFactor: number; - minInsyncReplicas: number; + minInSyncReplicas: number; cleanupPolicy: string; retentionMs: number; retentionBytes: number; @@ -73,14 +35,15 @@ export interface TopicFormDataRaw { customParams: TopicFormCustomParams; } +export type TopicFormDataRaw = Partial; + export interface TopicFormData { name: string; partitions: number; replicationFactor: number; - minInsyncReplicas: number; + minInSyncReplicas: number; cleanupPolicy: string; retentionMs: number; - retentionBytes: number; maxMessageBytes: number; customParams: { name: string; @@ -92,5 +55,6 @@ export interface TopicMessagesState { messages: TopicMessage[]; phase?: string; meta: TopicMessageConsuming; + messageEventType?: string; isFetching: boolean; } diff --git a/kafka-ui-react-app/src/redux/reducers/alerts/alertsSlice.ts b/kafka-ui-react-app/src/redux/reducers/alerts/alertsSlice.ts deleted file mode 100644 index c68b24e002a..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/alerts/alertsSlice.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { - createAsyncThunk, - createEntityAdapter, - createSlice, - nanoid, - PayloadAction, -} from '@reduxjs/toolkit'; -import { UnknownAsyncThunkRejectedWithValueAction } from '@reduxjs/toolkit/dist/matchers'; -import { now } from 'lodash'; -import { Alert, RootState, ServerResponse } from 'redux/interfaces'; - -const alertsAdapter = createEntityAdapter({ - selectId: (alert) => alert.id, -}); - -const isServerResponse = (payload: unknown): payload is ServerResponse => { - if ((payload as ServerResponse).status) { - return true; - } - return false; -}; - -const transformResponseToAlert = (payload: ServerResponse) => { - const { status, statusText, message, url } = payload; - const alert: Alert = { - id: url || nanoid(), - type: 'error', - title: `${status} ${statusText}`, - message: message || '', - response: payload, - createdAt: now(), - }; - - return alert; -}; - -const alertsSlice = createSlice({ - name: 'alerts', - initialState: alertsAdapter.getInitialState(), - reducers: { - alertDissmissed: alertsAdapter.removeOne, - alertAdded(state, action: PayloadAction) { - alertsAdapter.upsertOne(state, action.payload); - }, - serverErrorAlertAdded: ( - state, - { payload }: PayloadAction - ) => { - alertsAdapter.upsertOne(state, transformResponseToAlert(payload)); - }, - }, - extraReducers: (builder) => { - builder.addMatcher( - (action): action is UnknownAsyncThunkRejectedWithValueAction => - action.type.endsWith('/rejected'), - (state, { meta, payload }) => { - const { rejectedWithValue } = meta; - if (rejectedWithValue && isServerResponse(payload)) { - alertsAdapter.upsertOne(state, transformResponseToAlert(payload)); - } - } - ); - }, -}); - -export const { selectAll } = alertsAdapter.getSelectors( - (state) => state.alerts -); - -export const { alertDissmissed, alertAdded, serverErrorAlertAdded } = - alertsSlice.actions; - -export const showSuccessAlert = createAsyncThunk< - number, - { id: string; message: string }, - { fulfilledMeta: null } ->( - 'alerts/showSuccessAlert', - async ({ id, message }, { dispatch, fulfillWithValue }) => { - const creationDate = Date.now(); - - dispatch( - alertAdded({ - id, - message, - title: '', - type: 'success', - createdAt: creationDate, - }) - ); - - setTimeout(() => { - dispatch(alertDissmissed(id)); - }, 3000); - - return fulfillWithValue(creationDate, null); - } -); - -export default alertsSlice.reducer; diff --git a/kafka-ui-react-app/src/redux/reducers/brokers/__test__/fixtures.ts b/kafka-ui-react-app/src/redux/reducers/brokers/__test__/fixtures.ts deleted file mode 100644 index b5b953e6696..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/brokers/__test__/fixtures.ts +++ /dev/null @@ -1,51 +0,0 @@ -export const brokersPayload = [ - { id: 1, host: 'b-1.test.kafka.amazonaws.com' }, - { id: 2, host: 'b-2.test.kafka.amazonaws.com' }, -]; - -export const clusterStatsPayload = { - brokerCount: 2, - activeControllers: 1, - onlinePartitionCount: 138, - offlinePartitionCount: 0, - inSyncReplicasCount: 239, - outOfSyncReplicasCount: 0, - underReplicatedPartitionCount: 0, - diskUsage: [ - { brokerId: 0, segmentSize: 334567, segmentCount: 245 }, - { brokerId: 1, segmentSize: 12345678, segmentCount: 121 }, - ], - version: '2.2.1', -}; - -export const initialBrokersReducerState = { - items: brokersPayload, - brokerCount: 2, - activeControllers: 1, - onlinePartitionCount: 138, - offlinePartitionCount: 0, - inSyncReplicasCount: 239, - outOfSyncReplicasCount: 0, - underReplicatedPartitionCount: 0, - diskUsage: [ - { brokerId: 0, segmentSize: 1111, segmentCount: 333 }, - { brokerId: 1, segmentSize: 2222, segmentCount: 444 }, - ], - version: '2.2.1', -}; - -export const updatedBrokersReducerState = { - items: brokersPayload, - brokerCount: 2, - activeControllers: 1, - onlinePartitionCount: 138, - offlinePartitionCount: 0, - inSyncReplicasCount: 239, - outOfSyncReplicasCount: 0, - underReplicatedPartitionCount: 0, - diskUsage: [ - { brokerId: 0, segmentSize: 334567, segmentCount: 245 }, - { brokerId: 1, segmentSize: 12345678, segmentCount: 121 }, - ], - version: '2.2.1', -}; diff --git a/kafka-ui-react-app/src/redux/reducers/brokers/__test__/reducer.spec.ts b/kafka-ui-react-app/src/redux/reducers/brokers/__test__/reducer.spec.ts deleted file mode 100644 index 55ef49445f6..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/brokers/__test__/reducer.spec.ts +++ /dev/null @@ -1,113 +0,0 @@ -import fetchMock from 'fetch-mock-jest'; -import reducer, { - initialState, - fetchBrokers, - fetchClusterStats, -} from 'redux/reducers/brokers/brokersSlice'; -import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator'; - -import { - brokersPayload, - clusterStatsPayload, - initialBrokersReducerState, - updatedBrokersReducerState, -} from './fixtures'; - -const store = mockStoreCreator; -const clusterName = 'test-sluster-name'; - -describe('Brokers slice', () => { - describe('reducer', () => { - it('returns the initial state', () => { - expect(reducer(undefined, { type: fetchBrokers.pending })).toEqual( - initialState - ); - }); - it('reacts on fetchBrokers.fullfiled and returns payload', () => { - expect( - reducer(initialState, { - type: fetchBrokers.fulfilled, - payload: brokersPayload, - }) - ).toEqual({ - ...initialState, - items: brokersPayload, - }); - }); - it('reacts on fetchClusterStats.fullfiled and returns payload', () => { - expect( - reducer(initialBrokersReducerState, { - type: fetchClusterStats.fulfilled, - payload: clusterStatsPayload, - }) - ).toEqual(updatedBrokersReducerState); - }); - }); - - describe('thunks', () => { - afterEach(() => { - fetchMock.restore(); - store.clearActions(); - }); - - describe('fetchBrokers', () => { - it('creates fetchBrokers.fulfilled when broker are fetched', async () => { - fetchMock.getOnce( - `/api/clusters/${clusterName}/brokers`, - brokersPayload - ); - await store.dispatch(fetchBrokers(clusterName)); - expect( - store.getActions().map(({ type, payload }) => ({ type, payload })) - ).toEqual([ - { type: fetchBrokers.pending.type }, - { - type: fetchBrokers.fulfilled.type, - payload: brokersPayload, - }, - ]); - }); - - it('creates fetchBrokers.rejected when fetched clusters', async () => { - fetchMock.getOnce(`/api/clusters/${clusterName}/brokers`, 422); - await store.dispatch(fetchBrokers(clusterName)); - expect( - store.getActions().map(({ type, payload }) => ({ type, payload })) - ).toEqual([ - { type: fetchBrokers.pending.type }, - { type: fetchBrokers.rejected.type }, - ]); - }); - }); - - describe('fetchClusterStats', () => { - it('creates fetchClusterStats.fulfilled when broker are fetched', async () => { - fetchMock.getOnce( - `/api/clusters/${clusterName}/stats`, - clusterStatsPayload - ); - await store.dispatch(fetchClusterStats(clusterName)); - expect( - store.getActions().map(({ type, payload }) => ({ type, payload })) - ).toEqual([ - { type: fetchClusterStats.pending.type }, - { - type: fetchClusterStats.fulfilled.type, - payload: clusterStatsPayload, - }, - ]); - }); - - it('creates fetchClusterStats.rejected when fetched clusters', async () => { - fetchMock.getOnce(`/api/clusters/${clusterName}/stats`, 422); - await store.dispatch(fetchClusterStats(clusterName)); - expect( - store.getActions().map(({ type, payload }) => ({ type, payload })) - ).toEqual([ - { type: fetchClusterStats.pending.type }, - { type: fetchClusterStats.rejected.type }, - ]); - }); - }); - }); -}); diff --git a/kafka-ui-react-app/src/redux/reducers/brokers/__test__/selectors.spec.ts b/kafka-ui-react-app/src/redux/reducers/brokers/__test__/selectors.spec.ts deleted file mode 100644 index 119d984cf6a..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/brokers/__test__/selectors.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { store } from 'redux/store'; -import * as selectors from 'redux/reducers/brokers/selectors'; -import { - fetchBrokers, - fetchClusterStats, -} from 'redux/reducers/brokers/brokersSlice'; - -import { brokersPayload, updatedBrokersReducerState } from './fixtures'; - -const { dispatch, getState } = store; - -describe('Brokers selectors', () => { - describe('Initial State', () => { - it('returns broker count', () => { - expect(selectors.getBrokerCount(getState())).toEqual(0); - }); - it('returns active controllers', () => { - expect(selectors.getActiveControllers(getState())).toEqual(0); - }); - it('returns online partition count', () => { - expect(selectors.getOnlinePartitionCount(getState())).toEqual(0); - }); - it('returns offline partition count', () => { - expect(selectors.getOfflinePartitionCount(getState())).toEqual(0); - }); - it('returns in sync replicas count', () => { - expect(selectors.getInSyncReplicasCount(getState())).toEqual(0); - }); - it('returns out of sync replicas count', () => { - expect(selectors.getOutOfSyncReplicasCount(getState())).toEqual(0); - }); - it('returns under replicated partition count', () => { - expect(selectors.getUnderReplicatedPartitionCount(getState())).toEqual(0); - }); - it('returns disk usage', () => { - expect(selectors.getDiskUsage(getState())).toEqual([]); - }); - it('returns version', () => { - expect(selectors.getVersion(getState())).toBeUndefined(); - }); - }); - - describe('state', () => { - beforeAll(() => { - dispatch({ type: fetchBrokers.fulfilled.type, payload: brokersPayload }); - dispatch({ - type: fetchClusterStats.fulfilled.type, - payload: updatedBrokersReducerState, - }); - }); - - it('returns broker count', () => { - expect(selectors.getBrokerCount(getState())).toEqual(2); - }); - it('returns active controllers', () => { - expect(selectors.getActiveControllers(getState())).toEqual(1); - }); - it('returns online partition count', () => { - expect(selectors.getOnlinePartitionCount(getState())).toEqual(138); - }); - it('returns offline partition count', () => { - expect(selectors.getOfflinePartitionCount(getState())).toEqual(0); - }); - it('returns in sync replicas count', () => { - expect(selectors.getInSyncReplicasCount(getState())).toEqual(239); - }); - it('returns out of sync replicas count', () => { - expect(selectors.getOutOfSyncReplicasCount(getState())).toEqual(0); - }); - it('returns under replicated partition count', () => { - expect(selectors.getUnderReplicatedPartitionCount(getState())).toEqual(0); - }); - it('returns disk usage', () => { - expect(selectors.getDiskUsage(getState())).toEqual([ - { brokerId: 0, segmentSize: 334567, segmentCount: 245 }, - { brokerId: 1, segmentSize: 12345678, segmentCount: 121 }, - ]); - }); - it('returns version', () => { - expect(selectors.getVersion(getState())).toEqual('2.2.1'); - }); - }); -}); diff --git a/kafka-ui-react-app/src/redux/reducers/brokers/brokersSlice.ts b/kafka-ui-react-app/src/redux/reducers/brokers/brokersSlice.ts deleted file mode 100644 index d7c762e4a62..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/brokers/brokersSlice.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { BrokersApi, ClustersApi, Configuration } from 'generated-sources'; -import { BrokersState, ClusterName, RootState } from 'redux/interfaces'; -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; -import { BASE_PARAMS } from 'lib/constants'; - -const apiClientConf = new Configuration(BASE_PARAMS); -export const brokersApiClient = new BrokersApi(apiClientConf); -export const clustersApiClient = new ClustersApi(apiClientConf); - -export const fetchBrokers = createAsyncThunk( - 'brokers/fetchBrokers', - (clusterName: ClusterName) => brokersApiClient.getBrokers({ clusterName }) -); - -export const fetchClusterStats = createAsyncThunk( - 'brokers/fetchClusterStats', - (clusterName: ClusterName) => - clustersApiClient.getClusterStats({ clusterName }) -); - -export const initialState: BrokersState = { - items: [], - brokerCount: 0, - activeControllers: 0, - onlinePartitionCount: 0, - offlinePartitionCount: 0, - inSyncReplicasCount: 0, - outOfSyncReplicasCount: 0, - underReplicatedPartitionCount: 0, - diskUsage: [], -}; - -export const brokersSlice = createSlice({ - name: 'brokers', - initialState, - reducers: {}, - extraReducers: (builder) => { - builder.addCase(fetchBrokers.pending, () => initialState); - builder.addCase(fetchBrokers.fulfilled, (state, { payload }) => ({ - ...state, - items: payload, - })); - builder.addCase(fetchClusterStats.fulfilled, (state, { payload }) => ({ - ...state, - ...payload, - })); - }, -}); - -export const selectStats = (state: RootState) => state.brokers; - -export default brokersSlice.reducer; diff --git a/kafka-ui-react-app/src/redux/reducers/brokers/selectors.ts b/kafka-ui-react-app/src/redux/reducers/brokers/selectors.ts deleted file mode 100644 index 31654a70d83..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/brokers/selectors.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { RootState, BrokersState } from 'redux/interfaces'; - -const brokersState = ({ brokers }: RootState): BrokersState => brokers; - -export const getBrokerCount = createSelector( - brokersState, - ({ brokerCount }) => brokerCount -); -export const getActiveControllers = createSelector( - brokersState, - ({ activeControllers }) => activeControllers -); -export const getOnlinePartitionCount = createSelector( - brokersState, - ({ onlinePartitionCount }) => onlinePartitionCount -); -export const getOfflinePartitionCount = createSelector( - brokersState, - ({ offlinePartitionCount }) => offlinePartitionCount -); -export const getInSyncReplicasCount = createSelector( - brokersState, - ({ inSyncReplicasCount }) => inSyncReplicasCount -); -export const getOutOfSyncReplicasCount = createSelector( - brokersState, - ({ outOfSyncReplicasCount }) => outOfSyncReplicasCount -); -export const getUnderReplicatedPartitionCount = createSelector( - brokersState, - ({ underReplicatedPartitionCount }) => underReplicatedPartitionCount -); - -export const getDiskUsage = createSelector( - brokersState, - ({ diskUsage }) => diskUsage -); - -export const getVersion = createSelector( - brokersState, - ({ version }) => version -); diff --git a/kafka-ui-react-app/src/redux/reducers/clusters/__test__/fixtures.ts b/kafka-ui-react-app/src/redux/reducers/clusters/__test__/fixtures.ts deleted file mode 100644 index c1d048658fc..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/clusters/__test__/fixtures.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Cluster, ServerStatus } from 'generated-sources'; - -export const onlineClusterPayload: Cluster = { - name: 'secondLocal', - defaultCluster: true, - status: ServerStatus.ONLINE, - brokerCount: 1, - onlinePartitionCount: 6, - topicCount: 3, - bytesInPerSec: 1.55, - bytesOutPerSec: 9.314, - features: [], -}; -export const offlineClusterPayload: Cluster = { - name: 'local', - defaultCluster: false, - status: ServerStatus.OFFLINE, - brokerCount: 1, - onlinePartitionCount: 2, - topicCount: 2, - bytesInPerSec: 3.42, - bytesOutPerSec: 4.14, - features: [], -}; - -export const clustersPayload: Cluster[] = [ - onlineClusterPayload, - offlineClusterPayload, -]; diff --git a/kafka-ui-react-app/src/redux/reducers/clusters/__test__/reducer.spec.ts b/kafka-ui-react-app/src/redux/reducers/clusters/__test__/reducer.spec.ts deleted file mode 100644 index d10be934481..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/clusters/__test__/reducer.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import fetchMock from 'fetch-mock-jest'; -import reducer, { fetchClusters } from 'redux/reducers/clusters/clustersSlice'; -import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator'; - -import { clustersPayload } from './fixtures'; - -const store = mockStoreCreator; - -describe('Clusters Slice', () => { - describe('Reducer', () => { - it('returns the initial state', () => { - expect(reducer(undefined, { type: fetchClusters.pending })).toEqual([]); - }); - - it('reacts on fetchClusters.fulfilled and returns payload', () => { - expect( - reducer([], { - type: fetchClusters.fulfilled, - payload: clustersPayload, - }) - ).toEqual(clustersPayload); - }); - }); - - describe('thunks', () => { - afterEach(() => { - fetchMock.restore(); - store.clearActions(); - }); - - describe('fetchClusters', () => { - it('creates fetchClusters.fulfilled when fetched clusters', async () => { - fetchMock.getOnce('/api/clusters', clustersPayload); - await store.dispatch(fetchClusters()); - expect( - store.getActions().map(({ type, payload }) => ({ type, payload })) - ).toEqual([ - { type: fetchClusters.pending.type }, - { type: fetchClusters.fulfilled.type, payload: clustersPayload }, - ]); - }); - - it('creates fetchClusters.rejected when fetched clusters', async () => { - fetchMock.getOnce('/api/clusters', 422); - await store.dispatch(fetchClusters()); - expect( - store.getActions().map(({ type, payload }) => ({ type, payload })) - ).toEqual([ - { type: fetchClusters.pending.type }, - { type: fetchClusters.rejected.type }, - ]); - }); - }); - }); -}); diff --git a/kafka-ui-react-app/src/redux/reducers/clusters/__test__/selectors.spec.ts b/kafka-ui-react-app/src/redux/reducers/clusters/__test__/selectors.spec.ts deleted file mode 100644 index c9b51067e5a..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/clusters/__test__/selectors.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { store } from 'redux/store'; -import { - fetchClusters, - getAreClustersFulfilled, - getClusterList, - getOnlineClusters, - getOfflineClusters, -} from 'redux/reducers/clusters/clustersSlice'; - -import { - clustersPayload, - offlineClusterPayload, - onlineClusterPayload, -} from './fixtures'; - -describe('Clusters selectors', () => { - describe('Initial State', () => { - it('returns fetch status', () => { - expect(getAreClustersFulfilled(store.getState())).toBeFalsy(); - }); - - it('returns cluster list', () => { - expect(getClusterList(store.getState())).toEqual([]); - }); - - it('returns online cluster list', () => { - expect(getOnlineClusters(store.getState())).toEqual([]); - }); - - it('returns offline cluster list', () => { - expect(getOfflineClusters(store.getState())).toEqual([]); - }); - }); - - describe('state', () => { - beforeAll(() => { - store.dispatch(fetchClusters.fulfilled(clustersPayload, '1234')); - }); - - it('returns fetch status', () => { - expect(getAreClustersFulfilled(store.getState())).toBeTruthy(); - }); - - it('returns cluster list', () => { - expect(getClusterList(store.getState())).toEqual(clustersPayload); - }); - - it('returns online cluster list', () => { - expect(getOnlineClusters(store.getState())).toEqual([ - onlineClusterPayload, - ]); - }); - - it('returns offline cluster list', () => { - expect(getOfflineClusters(store.getState())).toEqual([ - offlineClusterPayload, - ]); - }); - }); -}); diff --git a/kafka-ui-react-app/src/redux/reducers/clusters/clustersSlice.ts b/kafka-ui-react-app/src/redux/reducers/clusters/clustersSlice.ts deleted file mode 100644 index 0a3e589bab6..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/clusters/clustersSlice.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { - createAsyncThunk, - createSlice, - createSelector, -} from '@reduxjs/toolkit'; -import { - ClustersApi, - Configuration, - Cluster, - ServerStatus, - ClusterFeaturesEnum, -} from 'generated-sources'; -import { BASE_PARAMS, AsyncRequestStatus } from 'lib/constants'; -import { RootState } from 'redux/interfaces'; -import { createFetchingSelector } from 'redux/reducers/loader/selectors'; - -const apiClientConf = new Configuration(BASE_PARAMS); -export const clustersApiClient = new ClustersApi(apiClientConf); - -export const fetchClusters = createAsyncThunk( - 'clusters/fetchClusters', - async () => { - const clusters: Cluster[] = await clustersApiClient.getClusters(); - return clusters; - } -); - -export const initialState: Cluster[] = []; -export const clustersSlice = createSlice({ - name: 'clusters', - initialState, - reducers: {}, - extraReducers: (builder) => { - builder.addCase(fetchClusters.fulfilled, (_, { payload }) => payload); - }, -}); - -const clustersState = ({ clusters }: RootState): Cluster[] => clusters; -const getClusterListFetchingStatus = createFetchingSelector( - 'clusters/fetchClusters' -); -export const getAreClustersFulfilled = createSelector( - getClusterListFetchingStatus, - (status) => status === AsyncRequestStatus.fulfilled -); -export const getClusterList = createSelector( - clustersState, - (clusters) => clusters -); -export const getOnlineClusters = createSelector(getClusterList, (clusters) => - clusters.filter(({ status }) => status === ServerStatus.ONLINE) -); -export const getOfflineClusters = createSelector(getClusterList, (clusters) => - clusters.filter(({ status }) => status === ServerStatus.OFFLINE) -); -export const getClustersReadonlyStatus = (clusterName: string) => - createSelector( - getClusterList, - (clusters): boolean => - clusters.find(({ name }) => name === clusterName)?.readOnly || false - ); -export const getClustersFeatures = (clusterName: string) => - createSelector( - getClusterList, - (clusters): ClusterFeaturesEnum[] => - clusters.find(({ name }) => name === clusterName)?.features || [] - ); - -export default clustersSlice.reducer; diff --git a/kafka-ui-react-app/src/redux/reducers/connect/__test__/fixtures.ts b/kafka-ui-react-app/src/redux/reducers/connect/__test__/fixtures.ts deleted file mode 100644 index 5fc7cad55df..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/connect/__test__/fixtures.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { - Connect, - Connector, - ConnectorState, - ConnectorTaskStatus, - ConnectorType, - FullConnectorInfo, - Task, -} from 'generated-sources'; - -export const connects: Connect[] = [ - { name: 'first', address: 'localhost:8083' }, - { name: 'second', address: 'localhost:8084' }, -]; - -export const connectorsServerPayload = [ - { - connect: 'first', - name: 'hdfs-source-connector', - connector_class: 'FileStreamSource', - type: ConnectorType.SOURCE, - topics: ['a', 'b', 'c'], - status: { - state: ConnectorTaskStatus.RUNNING, - workerId: 1, - }, - tasks_count: 2, - failed_tasks_count: 0, - }, - { - connect: 'second', - name: 'hdfs2-source-connector', - connector_class: 'FileStreamSource', - type: ConnectorType.SINK, - topics: ['test-topic'], - status: { - state: ConnectorTaskStatus.FAILED, - workerId: 1, - }, - tasks_count: 3, - failed_tasks_count: 1, - }, -]; - -export const connectors: FullConnectorInfo[] = [ - { - connect: 'first', - name: 'hdfs-source-connector', - connectorClass: 'FileStreamSource', - type: ConnectorType.SOURCE, - topics: ['a', 'b', 'c'], - status: { - state: ConnectorState.RUNNING, - }, - tasksCount: 2, - failedTasksCount: 0, - }, - { - connect: 'second', - name: 'hdfs2-source-connector', - connectorClass: 'FileStreamSource', - type: ConnectorType.SINK, - topics: ['test-topic'], - status: { - state: ConnectorState.FAILED, - }, - tasksCount: 3, - failedTasksCount: 1, - }, -]; - -export const failedConnectors: FullConnectorInfo[] = [ - { - connect: 'first', - name: 'hdfs-source-connector', - connectorClass: 'FileStreamSource', - type: ConnectorType.SOURCE, - topics: ['a', 'b', 'c'], - status: { - state: ConnectorState.FAILED, - }, - tasksCount: 2, - failedTasksCount: 0, - }, - { - connect: 'second', - name: 'hdfs2-source-connector', - connectorClass: 'FileStreamSource', - type: ConnectorType.SINK, - topics: ['a', 'b', 'c'], - status: { - state: ConnectorState.FAILED, - }, - tasksCount: 3, - failedTasksCount: 1, - }, -]; - -export const connectorServerPayload = { - connect: 'first', - name: 'hdfs-source-connector', - type: ConnectorType.SOURCE, - status: { - state: ConnectorTaskStatus.RUNNING, - worker_id: 'kafka-connect0:8083', - }, - config: { - 'connector.class': 'FileStreamSource', - 'tasks.max': '10', - topic: 'test-topic', - file: '/some/file', - }, - tasks: [{ connector: 'first', task: 1 }], -}; - -export const connector: Connector = { - connect: 'first', - name: 'hdfs-source-connector', - type: ConnectorType.SOURCE, - status: { - state: ConnectorState.RUNNING, - workerId: 'kafka-connect0:8083', - }, - config: { - 'connector.class': 'FileStreamSource', - 'tasks.max': '10', - topic: 'test-topic', - file: '/some/file', - }, - tasks: [{ connector: 'first', task: 1 }], -}; - -export const tasksServerPayload = [ - { - id: { connector: 'first', task: 1 }, - status: { - id: 1, - state: ConnectorTaskStatus.RUNNING, - worker_id: 'kafka-connect0:8083', - }, - config: { - 'batch.size': '2000', - file: '/some/file', - 'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask', - topic: 'test-topic', - }, - }, - { - id: { connector: 'first', task: 2 }, - status: { - id: 2, - state: ConnectorTaskStatus.FAILED, - trace: 'Failure 1', - worker_id: 'kafka-connect0:8083', - }, - config: { - 'batch.size': '1000', - file: '/some/file2', - 'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask', - topic: 'test-topic', - }, - }, - { - id: { connector: 'first', task: 3 }, - status: { - id: 3, - state: ConnectorTaskStatus.RUNNING, - worker_id: 'kafka-connect0:8083', - }, - config: { - 'batch.size': '3000', - file: '/some/file3', - 'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask', - topic: 'test-topic', - }, - }, -]; - -export const tasks: Task[] = [ - { - id: { connector: 'first', task: 1 }, - status: { - id: 1, - state: ConnectorTaskStatus.RUNNING, - workerId: 'kafka-connect0:8083', - }, - config: { - 'batch.size': '2000', - file: '/some/file', - 'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask', - topic: 'test-topic', - }, - }, - { - id: { connector: 'first', task: 2 }, - status: { - id: 2, - state: ConnectorTaskStatus.FAILED, - trace: 'Failure 1', - workerId: 'kafka-connect0:8083', - }, - config: { - 'batch.size': '1000', - file: '/some/file2', - 'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask', - topic: 'test-topic', - }, - }, - { - id: { connector: 'first', task: 3 }, - status: { - id: 3, - state: ConnectorTaskStatus.RUNNING, - workerId: 'kafka-connect0:8083', - }, - config: { - 'batch.size': '3000', - file: '/some/file3', - 'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask', - topic: 'test-topic', - }, - }, -]; diff --git a/kafka-ui-react-app/src/redux/reducers/connect/__test__/reducer.spec.ts b/kafka-ui-react-app/src/redux/reducers/connect/__test__/reducer.spec.ts deleted file mode 100644 index 3630753b716..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/connect/__test__/reducer.spec.ts +++ /dev/null @@ -1,765 +0,0 @@ -import { - ConnectorState, - ConnectorTaskStatus, - ConnectorAction, -} from 'generated-sources'; -import reducer, { - initialState, - fetchConnects, - fetchConnectors, - fetchConnector, - createConnector, - deleteConnector, - setConnectorStatusState, - fetchConnectorTasks, - fetchConnectorConfig, - updateConnectorConfig, - restartConnector, - pauseConnector, - resumeConnector, - restartConnectorTask, -} from 'redux/reducers/connect/connectSlice'; -import fetchMock from 'fetch-mock-jest'; -import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator'; -import { getTypeAndPayload, getAlertActions } from 'lib/testHelpers'; - -import { - connects, - connectors, - connector, - tasks, - connectorsServerPayload, - connectorServerPayload, - tasksServerPayload, -} from './fixtures'; - -const runningConnectorState = { - ...initialState, - currentConnector: { - ...initialState.currentConnector, - connector: { - ...connector, - status: { - ...connector.status, - state: ConnectorState.RUNNING, - }, - }, - tasks: tasks.map((task) => ({ - ...task, - status: { - ...task.status, - state: ConnectorTaskStatus.RUNNING, - }, - })), - }, -}; - -const pausedConnectorState = { - ...initialState, - currentConnector: { - ...initialState.currentConnector, - connector: { - ...connector, - status: { - ...connector.status, - state: ConnectorState.PAUSED, - }, - }, - tasks: tasks.map((task) => ({ - ...task, - status: { - ...task.status, - state: ConnectorTaskStatus.PAUSED, - }, - })), - }, -}; - -describe('Connect slice', () => { - describe('Reducer', () => { - it('reacts on fetchConnects/fulfilled', () => { - expect( - reducer(initialState, { - type: fetchConnects.fulfilled, - payload: { connects }, - }) - ).toEqual({ - ...initialState, - connects, - }); - }); - - it('reacts on fetchConnectors/fulfilled', () => { - expect( - reducer(initialState, { - type: fetchConnectors.fulfilled, - payload: { connectors }, - }) - ).toEqual({ - ...initialState, - connectors, - }); - }); - - it('reacts on fetchConnector/fulfilled', () => { - expect( - reducer(initialState, { - type: fetchConnector.fulfilled, - payload: { connector }, - }) - ).toEqual({ - ...initialState, - currentConnector: { - ...initialState.currentConnector, - connector, - }, - }); - }); - - it('reacts on createConnector/fulfilled', () => { - expect( - reducer(initialState, { - type: createConnector.fulfilled, - payload: { connector }, - }) - ).toEqual({ - ...initialState, - currentConnector: { - ...initialState.currentConnector, - connector, - }, - }); - }); - - it('reacts on deleteConnector/fulfilled', () => { - expect( - reducer( - { ...initialState, connectors }, - { - type: deleteConnector.fulfilled, - payload: { connectorName: connectors[0].name }, - } - ) - ).toEqual({ - ...initialState, - connectors: connectors.slice(1), - }); - }); - - it('reacts on setConnectorStatusState/fulfilled', () => { - expect( - reducer(runningConnectorState, { - type: setConnectorStatusState, - payload: { - taskState: ConnectorTaskStatus.PAUSED, - connectorState: ConnectorState.PAUSED, - }, - }) - ).toEqual(pausedConnectorState); - }); - - it('reacts on fetchConnectorTasks/fulfilled', () => { - expect( - reducer(initialState, { - type: fetchConnectorTasks.fulfilled, - payload: { tasks }, - }) - ).toEqual({ - ...initialState, - currentConnector: { - ...initialState.currentConnector, - tasks, - }, - }); - }); - - it('reacts on fetchConnectorConfig/fulfilled', () => { - expect( - reducer(initialState, { - type: fetchConnectorConfig.fulfilled, - payload: { config: connector.config }, - }) - ).toEqual({ - ...initialState, - currentConnector: { - ...initialState.currentConnector, - config: connector.config, - }, - }); - }); - - it('reacts on updateConnectorConfig/fulfilled', () => { - expect( - reducer( - { - ...initialState, - currentConnector: { - ...initialState.currentConnector, - config: { - ...connector.config, - fieldToRemove: 'Fake', - }, - }, - }, - { - type: updateConnectorConfig.fulfilled, - payload: { connector }, - } - ) - ).toEqual({ - ...initialState, - currentConnector: { - ...initialState.currentConnector, - connector, - config: connector.config, - }, - }); - }); - }); - - describe('Thunks', () => { - const store = mockStoreCreator; - const clusterName = 'local'; - const connectName = 'first'; - const connectorName = 'hdfs-source-connector'; - const taskId = 10; - - describe('Thunks', () => { - afterEach(() => { - fetchMock.restore(); - store.clearActions(); - }); - describe('fetchConnects', () => { - it('creates fetchConnects/fulfilled when fetching connects', async () => { - fetchMock.getOnce(`/api/clusters/${clusterName}/connects`, connects); - await store.dispatch(fetchConnects(clusterName)); - - expect(getTypeAndPayload(store)).toEqual([ - { type: fetchConnects.pending.type }, - { - type: fetchConnects.fulfilled.type, - payload: { connects }, - }, - ]); - }); - it('creates fetchConnects/rejected', async () => { - fetchMock.getOnce(`/api/clusters/${clusterName}/connects`, 404); - await store.dispatch(fetchConnects(clusterName)); - expect(getTypeAndPayload(store)).toEqual([ - { type: fetchConnects.pending.type }, - { - type: fetchConnects.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/connects`, - message: undefined, - }, - }, - ]); - }); - }); - describe('fetchConnectors', () => { - it('creates fetchConnectors/fulfilled when fetching connectors', async () => { - fetchMock.getOnce( - `/api/clusters/${clusterName}/connectors`, - connectorsServerPayload, - { query: { search: '' } } - ); - await store.dispatch(fetchConnectors({ clusterName })); - expect(getTypeAndPayload(store)).toEqual([ - { type: fetchConnectors.pending.type }, - { - type: fetchConnectors.fulfilled.type, - payload: { connectors }, - }, - ]); - }); - it('creates fetchConnectors/rejected', async () => { - fetchMock.getOnce(`/api/clusters/${clusterName}/connectors`, 404, { - query: { search: '' }, - }); - await store.dispatch(fetchConnectors({ clusterName })); - expect(getTypeAndPayload(store)).toEqual([ - { type: fetchConnectors.pending.type }, - { - type: fetchConnectors.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/connectors?search=`, - message: undefined, - }, - }, - ]); - }); - }); - describe('fetchConnector', () => { - it('creates fetchConnector/fulfilled when fetching connector', async () => { - fetchMock.getOnce( - `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`, - connectorServerPayload - ); - await store.dispatch( - fetchConnector({ clusterName, connectName, connectorName }) - ); - - expect(getTypeAndPayload(store)).toEqual([ - { type: fetchConnector.pending.type }, - { - type: fetchConnector.fulfilled.type, - payload: { connector }, - }, - ]); - }); - it('creates fetchConnector/rejected', async () => { - fetchMock.getOnce( - `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`, - 404 - ); - await store.dispatch( - fetchConnector({ clusterName, connectName, connectorName }) - ); - expect(getTypeAndPayload(store)).toEqual([ - { type: fetchConnector.pending.type }, - { - type: fetchConnector.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`, - message: undefined, - }, - }, - ]); - }); - }); - describe('createConnector', () => { - it('creates createConnector/fulfilled when fetching connects', async () => { - fetchMock.postOnce( - { - url: `/api/clusters/${clusterName}/connects/${connectName}/connectors`, - body: { - name: connectorName, - config: connector.config, - }, - }, - connectorServerPayload - ); - await store.dispatch( - createConnector({ - clusterName, - connectName, - newConnector: { - name: connectorName, - config: connector.config, - }, - }) - ); - expect(getTypeAndPayload(store)).toEqual([ - { type: createConnector.pending.type }, - { - type: createConnector.fulfilled.type, - payload: { connector }, - }, - ]); - }); - it('creates createConnector/rejected', async () => { - fetchMock.postOnce( - { - url: `/api/clusters/${clusterName}/connects/${connectName}/connectors`, - body: { - name: connectorName, - config: connector.config, - }, - }, - 404 - ); - await store.dispatch( - createConnector({ - clusterName, - connectName, - newConnector: { - name: connectorName, - config: connector.config, - }, - }) - ); - expect(getTypeAndPayload(store)).toEqual([ - { type: createConnector.pending.type }, - { - type: createConnector.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/connects/${connectName}/connectors`, - message: undefined, - }, - }, - ]); - }); - }); - describe('deleteConnector', () => { - it('creates deleteConnector/fulfilled', async () => { - fetchMock.deleteOnce( - `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`, - {} - ); - fetchMock.getOnce( - `/api/clusters/${clusterName}/connectors?search=`, - connectorsServerPayload - ); - await store.dispatch( - deleteConnector({ clusterName, connectName, connectorName }) - ); - expect(getTypeAndPayload(store)).toEqual([ - { type: deleteConnector.pending.type }, - { type: fetchConnectors.pending.type }, - { - type: deleteConnector.fulfilled.type, - payload: { connectorName }, - }, - ]); - }); - it('creates deleteConnector/rejected', async () => { - fetchMock.deleteOnce( - `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`, - 404 - ); - try { - await store.dispatch( - deleteConnector({ clusterName, connectName, connectorName }) - ); - } catch { - expect(getTypeAndPayload(store)).toEqual([ - { type: deleteConnector.pending.type }, - { - type: deleteConnector.rejected.type, - payload: { - alert: { - subject: 'local-first-hdfs-source-connector', - title: 'Kafka Connect Connector Delete', - response: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`, - }, - }, - }, - }, - ]); - } - }); - }); - describe('fetchConnectorTasks', () => { - it('creates fetchConnectorTasks/fulfilled when fetching connects', async () => { - fetchMock.getOnce( - `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks`, - tasksServerPayload - ); - await store.dispatch( - fetchConnectorTasks({ clusterName, connectName, connectorName }) - ); - expect(getTypeAndPayload(store)).toEqual([ - { type: fetchConnectorTasks.pending.type }, - { - type: fetchConnectorTasks.fulfilled.type, - payload: { tasks }, - }, - ]); - }); - it('creates fetchConnectorTasks/rejected', async () => { - fetchMock.getOnce( - `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks`, - 404 - ); - await store.dispatch( - fetchConnectorTasks({ clusterName, connectName, connectorName }) - ); - expect(getTypeAndPayload(store)).toEqual([ - { type: fetchConnectorTasks.pending.type }, - { - type: fetchConnectorTasks.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks`, - message: undefined, - }, - }, - ]); - }); - }); - describe('restartConnector', () => { - it('creates restartConnector/fulfilled', async () => { - fetchMock.postOnce( - `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.RESTART}`, - { message: 'success' } - ); - fetchMock.getOnce( - `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks`, - tasksServerPayload - ); - await store.dispatch( - restartConnector({ clusterName, connectName, connectorName }) - ); - expect(getTypeAndPayload(store)).toEqual([ - { type: restartConnector.pending.type }, - { type: fetchConnectorTasks.pending.type }, - { type: restartConnector.fulfilled.type }, - ]); - }); - it('creates restartConnector/rejected', async () => { - fetchMock.postOnce( - `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.RESTART}`, - 404 - ); - await store.dispatch( - restartConnector({ clusterName, connectName, connectorName }) - ); - expect(getTypeAndPayload(store)).toEqual([ - { type: restartConnector.pending.type }, - { - type: restartConnector.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.RESTART}`, - message: undefined, - }, - }, - ]); - }); - }); - describe('pauseConnector', () => { - it('creates pauseConnector/fulfilled when fetching connects', async () => { - fetchMock.postOnce( - `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.PAUSE}`, - { message: 'success' } - ); - await store.dispatch( - pauseConnector({ clusterName, connectName, connectorName }) - ); - expect(getTypeAndPayload(store)).toEqual([ - { type: pauseConnector.pending.type }, - { - type: setConnectorStatusState.type, - payload: { - connectorState: ConnectorState.PAUSED, - taskState: ConnectorTaskStatus.PAUSED, - }, - }, - { type: pauseConnector.fulfilled.type }, - ]); - }); - it('creates pauseConnector/rejected', async () => { - fetchMock.postOnce( - `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.PAUSE}`, - 404 - ); - await store.dispatch( - pauseConnector({ clusterName, connectName, connectorName }) - ); - expect(getTypeAndPayload(store)).toEqual([ - { type: pauseConnector.pending.type }, - { - type: pauseConnector.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.PAUSE}`, - }, - }, - ]); - }); - }); - describe('resumeConnector', () => { - it('creates resumeConnector/fulfilled when fetching connects', async () => { - fetchMock.postOnce( - `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.RESUME}`, - { message: 'success' } - ); - await store.dispatch( - resumeConnector({ clusterName, connectName, connectorName }) - ); - expect(getTypeAndPayload(store)).toEqual([ - { type: resumeConnector.pending.type }, - { - type: setConnectorStatusState.type, - payload: { - connectorState: ConnectorState.RUNNING, - taskState: ConnectorTaskStatus.RUNNING, - }, - }, - { type: resumeConnector.fulfilled.type }, - ]); - }); - it('creates resumeConnector/rejected', async () => { - fetchMock.postOnce( - `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.RESUME}`, - 404 - ); - await store.dispatch( - resumeConnector({ clusterName, connectName, connectorName }) - ); - expect(getTypeAndPayload(store)).toEqual([ - { type: resumeConnector.pending.type }, - { - type: resumeConnector.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.RESUME}`, - }, - }, - ]); - }); - }); - describe('restartConnectorTask', () => { - it('creates restartConnectorTask/fulfilled when fetching connects', async () => { - fetchMock.postOnce( - `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks/${taskId}/action/restart`, - { message: 'success' } - ); - fetchMock.getOnce( - `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks`, - tasksServerPayload - ); - await store.dispatch( - restartConnectorTask({ - clusterName, - connectName, - connectorName, - taskId, - }) - ); - expect(getTypeAndPayload(store)).toEqual([ - { type: restartConnectorTask.pending.type }, - { type: fetchConnectorTasks.pending.type }, - { - type: fetchConnectorTasks.fulfilled.type, - payload: { tasks }, - }, - ...getAlertActions(store), - { type: restartConnectorTask.fulfilled.type }, - ]); - }); - it('creates restartConnectorTask/rejected', async () => { - fetchMock.postOnce( - `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks/${taskId}/action/restart`, - 404 - ); - await store.dispatch( - restartConnectorTask({ - clusterName, - connectName, - connectorName, - taskId, - }) - ); - expect(getTypeAndPayload(store)).toEqual([ - { type: restartConnectorTask.pending.type }, - { - type: restartConnectorTask.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks/${taskId}/action/restart`, - }, - }, - ]); - }); - }); - describe('fetchConnectorConfig', () => { - it('creates fetchConnectorConfig/fulfilled when fetching connects', async () => { - fetchMock.getOnce( - `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/config`, - connector.config - ); - await store.dispatch( - fetchConnectorConfig({ clusterName, connectName, connectorName }) - ); - expect(getTypeAndPayload(store)).toEqual([ - { type: fetchConnectorConfig.pending.type }, - { - type: fetchConnectorConfig.fulfilled.type, - payload: { config: connector.config }, - }, - ]); - }); - it('creates fetchConnectorConfig/rejected', async () => { - fetchMock.getOnce( - `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/config`, - 404 - ); - await store.dispatch( - fetchConnectorConfig({ clusterName, connectName, connectorName }) - ); - expect(getTypeAndPayload(store)).toEqual([ - { type: fetchConnectorConfig.pending.type }, - { - type: fetchConnectorConfig.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/config`, - message: undefined, - }, - }, - ]); - }); - }); - describe('updateConnectorConfig', () => { - it('creates updateConnectorConfig/fulfilled when fetching connects', async () => { - fetchMock.putOnce( - `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/config`, - connectorServerPayload - ); - - await store.dispatch( - updateConnectorConfig({ - clusterName, - connectName, - connectorName, - connectorConfig: connector.config, - }) - ); - expect(getTypeAndPayload(store)).toEqual([ - { type: updateConnectorConfig.pending.type, payload: undefined }, - ...getAlertActions(store), - { - type: updateConnectorConfig.fulfilled.type, - payload: { connector }, - }, - ]); - }); - it('creates updateConnectorConfig/rejected', async () => { - fetchMock.putOnce( - `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/config`, - 404 - ); - await store.dispatch( - updateConnectorConfig({ - clusterName, - connectName, - connectorName, - connectorConfig: connector.config, - }) - ); - expect(getTypeAndPayload(store)).toEqual([ - { type: updateConnectorConfig.pending.type }, - { - type: updateConnectorConfig.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/config`, - message: undefined, - }, - }, - ]); - }); - }); - }); - }); -}); diff --git a/kafka-ui-react-app/src/redux/reducers/connect/__test__/selectors.spec.ts b/kafka-ui-react-app/src/redux/reducers/connect/__test__/selectors.spec.ts deleted file mode 100644 index a1f71626856..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/connect/__test__/selectors.spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { - fetchConnector, - fetchConnectorConfig, - fetchConnectors, - fetchConnectorTasks, - fetchConnects, -} from 'redux/reducers/connect/connectSlice'; -import { store } from 'redux/store'; -import * as selectors from 'redux/reducers/connect/selectors'; - -import { connects, connectors, connector, tasks } from './fixtures'; - -describe('Connect selectors', () => { - describe('Initial State', () => { - it('returns initial values', () => { - expect(selectors.getAreConnectsFetching(store.getState())).toEqual(false); - expect(selectors.getConnects(store.getState())).toEqual([]); - expect(selectors.getAreConnectorsFetching(store.getState())).toEqual( - false - ); - expect(selectors.getConnectors(store.getState())).toEqual([]); - expect(selectors.getFailedConnectors(store.getState())).toEqual([]); - expect(selectors.getFailedTasks(store.getState())).toEqual(0); - expect(selectors.getIsConnectorFetching(store.getState())).toEqual(false); - expect(selectors.getConnector(store.getState())).toEqual(null); - expect(selectors.getConnectorStatus(store.getState())).toEqual(undefined); - expect(selectors.getIsConnectorDeleting(store.getState())).toEqual(false); - expect(selectors.getIsConnectorRestarting(store.getState())).toEqual( - false - ); - expect(selectors.getIsConnectorPausing(store.getState())).toEqual(false); - expect(selectors.getIsConnectorResuming(store.getState())).toEqual(false); - expect(selectors.getIsConnectorActionRunning(store.getState())).toEqual( - false - ); - expect(selectors.getAreConnectorTasksFetching(store.getState())).toEqual( - false - ); - expect(selectors.getConnectorTasks(store.getState())).toEqual([]); - expect(selectors.getConnectorRunningTasksCount(store.getState())).toEqual( - 0 - ); - expect(selectors.getConnectorFailedTasksCount(store.getState())).toEqual( - 0 - ); - expect(selectors.getIsConnectorConfigFetching(store.getState())).toEqual( - false - ); - expect(selectors.getConnectorConfig(store.getState())).toEqual(null); - }); - }); - - describe('state', () => { - it('returns connects', () => { - store.dispatch({ - type: fetchConnects.fulfilled.type, - payload: { connects }, - }); - expect(selectors.getConnects(store.getState())).toEqual(connects); - }); - - it('returns connectors', () => { - store.dispatch({ - type: fetchConnectors.fulfilled.type, - payload: { connectors }, - }); - expect(selectors.getConnectors(store.getState())).toEqual(connectors); - }); - - it('returns failed connectors', () => { - store.dispatch({ - type: fetchConnectors.fulfilled.type, - payload: { connectors }, - }); - expect(selectors.getFailedConnectors(store.getState()).length).toEqual(1); - }); - - it('returns failed tasks', () => { - store.dispatch({ - type: fetchConnectors.fulfilled.type, - payload: { connectors }, - }); - expect(selectors.getFailedTasks(store.getState())).toEqual(1); - }); - - it('returns sorted topics', () => { - store.dispatch({ - type: fetchConnectors.fulfilled.type, - payload: { connectors }, - }); - const sortedTopics = selectors.getSortedTopics(store.getState()); - if (sortedTopics[0] && sortedTopics[0].length > 1) { - expect(sortedTopics[0]).toEqual(['a', 'b', 'c']); - } - }); - - it('returns connector', () => { - store.dispatch({ - type: fetchConnector.fulfilled.type, - payload: { connector }, - }); - expect(selectors.getConnector(store.getState())).toEqual(connector); - expect(selectors.getConnectorStatus(store.getState())).toEqual( - connector.status.state - ); - }); - - it('returns connector tasks', () => { - store.dispatch({ - type: fetchConnectorTasks.fulfilled.type, - payload: { tasks }, - }); - expect(selectors.getConnectorTasks(store.getState())).toEqual(tasks); - expect(selectors.getConnectorRunningTasksCount(store.getState())).toEqual( - 2 - ); - expect(selectors.getConnectorFailedTasksCount(store.getState())).toEqual( - 1 - ); - }); - - it('returns connector config', () => { - store.dispatch({ - type: fetchConnectorConfig.fulfilled.type, - payload: { config: connector.config }, - }); - expect(selectors.getConnectorConfig(store.getState())).toEqual( - connector.config - ); - }); - }); -}); diff --git a/kafka-ui-react-app/src/redux/reducers/connect/connectSlice.ts b/kafka-ui-react-app/src/redux/reducers/connect/connectSlice.ts deleted file mode 100644 index 9402b3c2e8c..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/connect/connectSlice.ts +++ /dev/null @@ -1,483 +0,0 @@ -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; -import { - Configuration, - Connect, - Connector, - ConnectorAction, - ConnectorState, - ConnectorTaskStatus, - FullConnectorInfo, - KafkaConnectApi, - NewConnector, - Task, - TaskId, -} from 'generated-sources'; -import { BASE_PARAMS } from 'lib/constants'; -import { getResponse } from 'lib/errorHandling'; -import { - ClusterName, - ConnectName, - ConnectorConfig, - ConnectorName, - ConnectorSearch, - ConnectState, -} from 'redux/interfaces'; -import { showSuccessAlert } from 'redux/reducers/alerts/alertsSlice'; - -const apiClientConf = new Configuration(BASE_PARAMS); -export const kafkaConnectApiClient = new KafkaConnectApi(apiClientConf); - -export const fetchConnects = createAsyncThunk< - { connects: Connect[] }, - ClusterName ->('connect/fetchConnects', async (clusterName, { rejectWithValue }) => { - try { - const connects = await kafkaConnectApiClient.getConnects({ clusterName }); - - return { connects }; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } -}); - -export const fetchConnectors = createAsyncThunk< - { connectors: FullConnectorInfo[] }, - { clusterName: ClusterName; search?: string } ->( - 'connect/fetchConnectors', - async ({ clusterName, search = '' }, { rejectWithValue }) => { - try { - const connectors = await kafkaConnectApiClient.getAllConnectors({ - clusterName, - search, - }); - - return { connectors }; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } - } -); - -export const fetchConnector = createAsyncThunk< - { connector: Connector }, - { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - } ->( - 'connect/fetchConnector', - async ({ clusterName, connectName, connectorName }, { rejectWithValue }) => { - try { - const connector = await kafkaConnectApiClient.getConnector({ - clusterName, - connectName, - connectorName, - }); - - return { connector }; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } - } -); - -export const createConnector = createAsyncThunk< - { connector: Connector }, - { - clusterName: ClusterName; - connectName: ConnectName; - newConnector: NewConnector; - } ->( - 'connect/createConnector', - async ({ clusterName, connectName, newConnector }, { rejectWithValue }) => { - try { - const connector = await kafkaConnectApiClient.createConnector({ - clusterName, - connectName, - newConnector, - }); - - return { connector }; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } - } -); - -export const deleteConnector = createAsyncThunk< - { connectorName: string }, - { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - } ->( - 'connect/deleteConnector', - async ( - { clusterName, connectName, connectorName }, - { rejectWithValue, dispatch } - ) => { - try { - await kafkaConnectApiClient.deleteConnector({ - clusterName, - connectName, - connectorName, - }); - - dispatch(fetchConnectors({ clusterName, search: '' })); - - return { connectorName }; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } - } -); - -export const fetchConnectorTasks = createAsyncThunk< - { tasks: Task[] }, - { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - } ->( - 'connect/fetchConnectorTasks', - async ({ clusterName, connectName, connectorName }, { rejectWithValue }) => { - try { - const tasks = await kafkaConnectApiClient.getConnectorTasks({ - clusterName, - connectName, - connectorName, - }); - - return { tasks }; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } - } -); - -export const restartConnector = createAsyncThunk< - undefined, - { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - } ->( - 'connect/restartConnector', - async ( - { clusterName, connectName, connectorName }, - { rejectWithValue, dispatch } - ) => { - try { - await kafkaConnectApiClient.updateConnectorState({ - clusterName, - connectName, - connectorName, - action: ConnectorAction.RESTART, - }); - - dispatch( - fetchConnectorTasks({ - clusterName, - connectName, - connectorName, - }) - ); - - return undefined; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } - } -); - -export const restartTasks = createAsyncThunk< - undefined, - { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - action: ConnectorAction; - } ->( - 'connect/restartTasks', - async ( - { clusterName, connectName, connectorName, action }, - { rejectWithValue, dispatch } - ) => { - try { - await kafkaConnectApiClient.updateConnectorState({ - clusterName, - connectName, - connectorName, - action, - }); - - dispatch( - fetchConnectorTasks({ - clusterName, - connectName, - connectorName, - }) - ); - - return undefined; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } - } -); - -export const restartConnectorTask = createAsyncThunk< - undefined, - { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - taskId: TaskId['task']; - } ->( - 'connect/restartConnectorTask', - async ( - { clusterName, connectName, connectorName, taskId }, - { rejectWithValue, dispatch } - ) => { - try { - await kafkaConnectApiClient.restartConnectorTask({ - clusterName, - connectName, - connectorName, - taskId: Number(taskId), - }); - - await dispatch( - fetchConnectorTasks({ - clusterName, - connectName, - connectorName, - }) - ); - - dispatch( - showSuccessAlert({ - id: `connect-${connectName}-${clusterName}`, - message: 'Tasks successfully restarted.', - }) - ); - - return undefined; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } - } -); - -export const fetchConnectorConfig = createAsyncThunk< - { config: { [key: string]: unknown } }, - { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - } ->( - 'connect/fetchConnectorConfig', - async ({ clusterName, connectName, connectorName }, { rejectWithValue }) => { - try { - const config = await kafkaConnectApiClient.getConnectorConfig({ - clusterName, - connectName, - connectorName, - }); - - return { config }; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } - } -); - -export const updateConnectorConfig = createAsyncThunk< - { connector: Connector }, - { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - connectorConfig: ConnectorConfig; - } ->( - 'connect/updateConnectorConfig', - async ( - { clusterName, connectName, connectorName, connectorConfig }, - { rejectWithValue, dispatch } - ) => { - try { - const connector = await kafkaConnectApiClient.setConnectorConfig({ - clusterName, - connectName, - connectorName, - requestBody: connectorConfig, - }); - - dispatch( - showSuccessAlert({ - id: `connector-${connectorName}-${clusterName}`, - message: 'Connector config updated.', - }) - ); - - return { connector }; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } - } -); - -export const initialState: ConnectState = { - connects: [], - connectors: [], - currentConnector: { - connector: null, - tasks: [], - config: null, - }, - search: '', -}; - -const connectSlice = createSlice({ - name: 'connect', - initialState, - reducers: { - setConnectorStatusState: (state, { payload }) => { - const { connector, tasks } = state.currentConnector; - - if (connector) { - connector.status.state = payload.connectorState; - } - - state.currentConnector.tasks = tasks.map((task) => ({ - ...task, - status: { - ...task.status, - state: payload.taskState, - }, - })); - }, - }, - extraReducers: (builder) => { - builder.addCase(fetchConnects.fulfilled, (state, { payload }) => { - state.connects = payload.connects; - }); - builder.addCase(fetchConnectors.fulfilled, (state, { payload }) => { - state.connectors = payload.connectors; - }); - builder.addCase(fetchConnector.fulfilled, (state, { payload }) => { - state.currentConnector.connector = payload.connector; - }); - builder.addCase(createConnector.fulfilled, (state, { payload }) => { - state.currentConnector.connector = payload.connector; - }); - builder.addCase(deleteConnector.fulfilled, (state, { payload }) => { - state.connectors = state.connectors.filter( - ({ name }) => name !== payload.connectorName - ); - }); - builder.addCase(fetchConnectorTasks.fulfilled, (state, { payload }) => { - state.currentConnector.tasks = payload.tasks; - }); - builder.addCase(fetchConnectorConfig.fulfilled, (state, { payload }) => { - state.currentConnector.config = payload.config; - }); - builder.addCase(updateConnectorConfig.fulfilled, (state, { payload }) => { - state.currentConnector.connector = payload.connector; - state.currentConnector.config = payload.connector.config; - }); - }, -}); - -export const { setConnectorStatusState } = connectSlice.actions; - -export const pauseCurrentConnector = () => - setConnectorStatusState({ - connectorState: ConnectorState.PAUSED, - taskState: ConnectorTaskStatus.PAUSED, - }); - -export const resumeCurrentConnector = () => - setConnectorStatusState({ - connectorState: ConnectorState.RUNNING, - taskState: ConnectorTaskStatus.RUNNING, - }); - -export const pauseConnector = createAsyncThunk< - undefined, - { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - } ->( - 'connect/pauseConnector', - async ( - { clusterName, connectName, connectorName }, - { rejectWithValue, dispatch } - ) => { - try { - await kafkaConnectApiClient.updateConnectorState({ - clusterName, - connectName, - connectorName, - action: ConnectorAction.PAUSE, - }); - - dispatch(pauseCurrentConnector()); - - return undefined; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } - } -); - -export const resumeConnector = createAsyncThunk< - undefined, - { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; - } ->( - 'connect/resumeConnector', - async ( - { clusterName, connectName, connectorName }, - { rejectWithValue, dispatch } - ) => { - try { - await kafkaConnectApiClient.updateConnectorState({ - clusterName, - connectName, - connectorName, - action: ConnectorAction.RESUME, - }); - - dispatch(resumeCurrentConnector()); - - return undefined; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } - } -); - -export const setConnectorSearch = (connectorSearch: ConnectorSearch) => { - return fetchConnectors({ - clusterName: connectorSearch.clusterName, - search: connectorSearch.search, - }); -}; - -export default connectSlice.reducer; diff --git a/kafka-ui-react-app/src/redux/reducers/connect/selectors.ts b/kafka-ui-react-app/src/redux/reducers/connect/selectors.ts deleted file mode 100644 index 32f654ec79d..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/connect/selectors.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { ConnectState, RootState } from 'redux/interfaces'; -import { createFetchingSelector } from 'redux/reducers/loader/selectors'; -import { - ConnectorTaskStatus, - ConnectorState, - FullConnectorInfo, -} from 'generated-sources'; -import { sortBy } from 'lodash'; -import { AsyncRequestStatus } from 'lib/constants'; - -import { - deleteConnector, - fetchConnector, - fetchConnectorConfig, - fetchConnectors, - fetchConnectorTasks, - fetchConnects, - pauseConnector, - restartConnector, - resumeConnector, -} from './connectSlice'; - -const connectState = ({ connect }: RootState): ConnectState => connect; - -const getConnectsFetchingStatus = createFetchingSelector( - fetchConnects.typePrefix -); -export const getAreConnectsFetching = createSelector( - getConnectsFetchingStatus, - (status) => status === AsyncRequestStatus.pending -); - -export const getConnects = createSelector( - connectState, - ({ connects }) => connects -); - -const getConnectorsFetchingStatus = createFetchingSelector( - fetchConnectors.typePrefix -); -export const getAreConnectorsFetching = createSelector( - getConnectorsFetchingStatus, - (status) => status === AsyncRequestStatus.pending -); - -export const getConnectors = createSelector( - connectState, - ({ connectors }) => connectors -); - -export const getFailedConnectors = createSelector( - connectState, - ({ connectors }) => { - return connectors.filter( - (connector: FullConnectorInfo) => - connector.status.state === ConnectorState.FAILED - ); - } -); - -export const getFailedTasks = createSelector(connectState, ({ connectors }) => { - return connectors - .map((connector: FullConnectorInfo) => connector.failedTasksCount || 0) - .reduce((acc: number, value: number) => acc + value, 0); -}); - -export const getSortedTopics = createSelector(connectState, ({ connectors }) => - connectors.map(({ topics }) => sortBy(topics || [])) -); - -const getConnectorFetchingStatus = createFetchingSelector( - fetchConnector.typePrefix -); -export const getIsConnectorFetching = createSelector( - getConnectorFetchingStatus, - (status) => status === AsyncRequestStatus.pending -); - -const getCurrentConnector = createSelector( - connectState, - ({ currentConnector }) => currentConnector -); - -export const getConnector = createSelector( - getCurrentConnector, - ({ connector }) => connector -); - -export const getConnectorStatus = createSelector( - getConnector, - (connector) => connector?.status?.state -); - -const getConnectorDeletingStatus = createFetchingSelector( - deleteConnector.typePrefix -); -export const getIsConnectorDeleting = createSelector( - getConnectorDeletingStatus, - (status) => status === AsyncRequestStatus.pending -); - -const getConnectorRestartingStatus = createFetchingSelector( - restartConnector.typePrefix -); -export const getIsConnectorRestarting = createSelector( - getConnectorRestartingStatus, - (status) => status === AsyncRequestStatus.pending -); - -const getConnectorPausingStatus = createFetchingSelector( - pauseConnector.typePrefix -); -export const getIsConnectorPausing = createSelector( - getConnectorPausingStatus, - (status) => status === AsyncRequestStatus.pending -); - -const getConnectorResumingStatus = createFetchingSelector( - resumeConnector.typePrefix -); -export const getIsConnectorResuming = createSelector( - getConnectorResumingStatus, - (status) => status === AsyncRequestStatus.pending -); - -export const getIsConnectorActionRunning = createSelector( - getIsConnectorRestarting, - getIsConnectorPausing, - getIsConnectorResuming, - (restarting, pausing, resuming) => restarting || pausing || resuming -); - -const getConnectorTasksFetchingStatus = createFetchingSelector( - fetchConnectorTasks.typePrefix -); -export const getAreConnectorTasksFetching = createSelector( - getConnectorTasksFetchingStatus, - (status) => status === AsyncRequestStatus.pending -); - -export const getConnectorTasks = createSelector( - getCurrentConnector, - ({ tasks }) => tasks -); - -export const getConnectorRunningTasksCount = createSelector( - getConnectorTasks, - (tasks) => - tasks.filter((task) => task.status?.state === ConnectorTaskStatus.RUNNING) - .length -); - -export const getConnectorFailedTasksCount = createSelector( - getConnectorTasks, - (tasks) => - tasks.filter((task) => task.status?.state === ConnectorTaskStatus.FAILED) - .length -); - -const getConnectorConfigFetchingStatus = createFetchingSelector( - fetchConnectorConfig.typePrefix -); -export const getIsConnectorConfigFetching = createSelector( - getConnectorConfigFetchingStatus, - (status) => status === AsyncRequestStatus.pending -); - -export const getConnectorConfig = createSelector( - getCurrentConnector, - ({ config }) => config -); - -export const getConnectorSearch = createSelector( - connectState, - (state) => state.search -); diff --git a/kafka-ui-react-app/src/redux/reducers/consumerGroups/__test__/consumerGroupSlice.spec.ts b/kafka-ui-react-app/src/redux/reducers/consumerGroups/__test__/consumerGroupSlice.spec.ts deleted file mode 100644 index 2bf20606adc..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/consumerGroups/__test__/consumerGroupSlice.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { store } from 'redux/store'; -import { - sortBy, - getConsumerGroupsOrderBy, - getConsumerGroupsSortOrder, - getAreConsumerGroupsPagedFulfilled, - fetchConsumerGroupsPaged, - selectAll, -} from 'redux/reducers/consumerGroups/consumerGroupsSlice'; -import { ConsumerGroupOrdering, SortOrder } from 'generated-sources'; -import { consumerGroups } from 'redux/reducers/consumerGroups/__test__/fixtures'; - -describe('Consumer Groups Slice', () => { - describe('Actions', () => { - it('should test the sortBy actions', () => { - expect(store.getState().consumerGroups.sortOrder).toBe(SortOrder.ASC); - - store.dispatch(sortBy(ConsumerGroupOrdering.STATE)); - expect(getConsumerGroupsOrderBy(store.getState())).toBe( - ConsumerGroupOrdering.STATE - ); - expect(getConsumerGroupsSortOrder(store.getState())).toBe(SortOrder.DESC); - store.dispatch(sortBy(ConsumerGroupOrdering.STATE)); - expect(getConsumerGroupsSortOrder(store.getState())).toBe(SortOrder.ASC); - }); - }); - - describe('Thunk Actions', () => { - it('should check the fetchConsumerPaged ', () => { - store.dispatch({ - type: fetchConsumerGroupsPaged.fulfilled.type, - payload: { - consumerGroups, - }, - }); - - expect(getAreConsumerGroupsPagedFulfilled(store.getState())).toBeTruthy(); - expect(selectAll(store.getState())).toEqual(consumerGroups); - - store.dispatch({ - type: fetchConsumerGroupsPaged.fulfilled.type, - payload: { - consumerGroups: null, - }, - }); - expect(selectAll(store.getState())).toEqual([]); - }); - }); -}); diff --git a/kafka-ui-react-app/src/redux/reducers/consumerGroups/__test__/fixtures.ts b/kafka-ui-react-app/src/redux/reducers/consumerGroups/__test__/fixtures.ts deleted file mode 100644 index 21f18177021..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/consumerGroups/__test__/fixtures.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { ConsumerGroupState } from 'generated-sources'; - -export const consumerGroups = [ - { - groupId: 'groupId1', - members: 0, - topics: 1, - simple: false, - partitionAssignor: '', - coordinator: { - id: 1, - host: 'host', - }, - }, - { - groupId: 'groupId2', - members: 0, - topics: 1, - simple: false, - partitionAssignor: '', - coordinator: { - id: 1, - host: 'host', - }, - }, -]; - -export const noConsumerGroupsResponse = { - pageCount: 1, - consumerGroups: [], -}; - -export const consumerGroupsPage = { - totalPages: 1, - consumerGroups, -}; - -export const consumerGroupPayload = { - groupId: 'amazon.msk.canary.group.broker-1', - members: 0, - topics: 2, - simple: false, - partitionAssignor: '', - state: ConsumerGroupState.EMPTY, - coordinator: { - id: 2, - host: 'b-2.kad-msk.st2jzq.c6.kafka.eu-west-1.amazonaws.com', - }, - messagesBehind: 0, - partitions: [ - { - topic: '__amazon_msk_canary', - partition: 1, - currentOffset: 0, - endOffset: 0, - messagesBehind: 0, - consumerId: undefined, - host: undefined, - }, - { - topic: '__amazon_msk_canary', - partition: 0, - currentOffset: 56932, - endOffset: 56932, - messagesBehind: 0, - consumerId: undefined, - host: undefined, - }, - { - topic: 'other_topic', - partition: 3, - currentOffset: 56932, - endOffset: 56932, - messagesBehind: 0, - consumerId: undefined, - host: undefined, - }, - { - topic: 'other_topic', - partition: 4, - currentOffset: 56932, - endOffset: 56932, - messagesBehind: 0, - consumerId: undefined, - host: undefined, - }, - ], -}; diff --git a/kafka-ui-react-app/src/redux/reducers/consumerGroups/consumerGroupsSlice.ts b/kafka-ui-react-app/src/redux/reducers/consumerGroups/consumerGroupsSlice.ts deleted file mode 100644 index d34d4594de9..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/consumerGroups/consumerGroupsSlice.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { - createAsyncThunk, - createEntityAdapter, - createSlice, - createSelector, - PayloadAction, -} from '@reduxjs/toolkit'; -import { - Configuration, - ConsumerGroupDetails, - ConsumerGroupOrdering, - ConsumerGroupsApi, - ConsumerGroupsPageResponse, - SortOrder, -} from 'generated-sources'; -import { BASE_PARAMS, AsyncRequestStatus } from 'lib/constants'; -import { getResponse } from 'lib/errorHandling'; -import { - ClusterName, - ConsumerGroupID, - ConsumerGroupResetOffsetRequestParams, - RootState, -} from 'redux/interfaces'; -import { createFetchingSelector } from 'redux/reducers/loader/selectors'; -import { EntityState } from '@reduxjs/toolkit/src/entities/models'; - -const apiClientConf = new Configuration(BASE_PARAMS); -export const api = new ConsumerGroupsApi(apiClientConf); - -export const fetchConsumerGroupsPaged = createAsyncThunk< - ConsumerGroupsPageResponse, - { - clusterName: ClusterName; - orderBy?: ConsumerGroupOrdering; - sortOrder?: SortOrder; - page?: number; - perPage?: number; - search: string; - } ->( - 'consumerGroups/fetchConsumerGroupsPaged', - async ( - { clusterName, orderBy, sortOrder, page, perPage, search }, - { rejectWithValue } - ) => { - try { - const response = await api.getConsumerGroupsPageRaw({ - clusterName, - orderBy, - sortOrder, - page, - perPage, - search, - }); - return await response.value(); - } catch (error) { - return rejectWithValue(await getResponse(error as Response)); - } - } -); - -export const fetchConsumerGroupDetails = createAsyncThunk< - ConsumerGroupDetails, - { clusterName: ClusterName; consumerGroupID: ConsumerGroupID } ->( - 'consumerGroups/fetchConsumerGroupDetails', - async ({ clusterName, consumerGroupID }, { rejectWithValue }) => { - try { - return await api.getConsumerGroup({ - clusterName, - id: consumerGroupID, - }); - } catch (error) { - return rejectWithValue(await getResponse(error as Response)); - } - } -); - -export const deleteConsumerGroup = createAsyncThunk< - ConsumerGroupID, - { clusterName: ClusterName; consumerGroupID: ConsumerGroupID } ->( - 'consumerGroups/deleteConsumerGroup', - async ({ clusterName, consumerGroupID }, { rejectWithValue }) => { - try { - await api.deleteConsumerGroup({ - clusterName, - id: consumerGroupID, - }); - - return consumerGroupID; - } catch (error) { - return rejectWithValue(await getResponse(error as Response)); - } - } -); - -export const resetConsumerGroupOffsets = createAsyncThunk< - ConsumerGroupID, - ConsumerGroupResetOffsetRequestParams ->( - 'consumerGroups/resetConsumerGroupOffsets', - async ( - { clusterName, consumerGroupID, requestBody }, - { rejectWithValue } - ) => { - try { - await api.resetConsumerGroupOffsets({ - clusterName, - id: consumerGroupID, - consumerGroupOffsetsReset: { - topic: requestBody.topic, - resetType: requestBody.resetType, - partitions: requestBody.partitions, - partitionsOffsets: requestBody.partitionsOffsets?.map((offset) => ({ - ...offset, - offset: +offset.offset, - })), - resetToTimestamp: requestBody.resetToTimestamp?.getTime(), - }, - }); - return consumerGroupID; - } catch (error) { - return rejectWithValue(await getResponse(error as Response)); - } - } -); -const SCHEMAS_PAGE_COUNT = 1; - -const consumerGroupsAdapter = createEntityAdapter({ - selectId: (consumerGroup) => consumerGroup.groupId, -}); - -interface ConsumerGroupState extends EntityState { - orderBy: ConsumerGroupOrdering | null; - sortOrder: SortOrder; - totalPages: number; -} - -const initialState: ConsumerGroupState = { - orderBy: ConsumerGroupOrdering.NAME, - sortOrder: SortOrder.ASC, - totalPages: SCHEMAS_PAGE_COUNT, - ...consumerGroupsAdapter.getInitialState(), -}; - -export const consumerGroupsSlice = createSlice({ - name: 'consumerGroups', - initialState, - reducers: { - sortBy: (state, action: PayloadAction) => { - state.orderBy = action.payload; - state.sortOrder = - state.orderBy === action.payload && state.sortOrder === SortOrder.ASC - ? SortOrder.DESC - : SortOrder.ASC; - }, - }, - extraReducers: (builder) => { - builder.addCase( - fetchConsumerGroupsPaged.fulfilled, - (state, { payload }) => { - state.totalPages = payload.pageCount || SCHEMAS_PAGE_COUNT; - consumerGroupsAdapter.setAll(state, payload.consumerGroups || []); - } - ); - builder.addCase(fetchConsumerGroupDetails.fulfilled, (state, { payload }) => - consumerGroupsAdapter.upsertOne(state, payload) - ); - builder.addCase(deleteConsumerGroup.fulfilled, (state, { payload }) => - consumerGroupsAdapter.removeOne(state, payload) - ); - }, -}); - -export const { sortBy } = consumerGroupsSlice.actions; - -const consumerGroupsState = ({ - consumerGroups, -}: RootState): ConsumerGroupState => consumerGroups; - -export const { selectAll, selectById } = - consumerGroupsAdapter.getSelectors(consumerGroupsState); - -export const getAreConsumerGroupsPagedFulfilled = createSelector( - createFetchingSelector('consumerGroups/fetchConsumerGroupsPaged'), - (status) => status === AsyncRequestStatus.fulfilled -); - -export const getIsConsumerGroupDeleted = createSelector( - createFetchingSelector('consumerGroups/deleteConsumerGroup'), - (status) => status === AsyncRequestStatus.fulfilled -); - -export const getAreConsumerGroupDetailsFulfilled = createSelector( - createFetchingSelector('consumerGroups/fetchConsumerGroupDetails'), - (status) => status === AsyncRequestStatus.fulfilled -); - -export const getIsOffsetReseted = createSelector( - createFetchingSelector('consumerGroups/resetConsumerGroupOffsets'), - (status) => status === AsyncRequestStatus.fulfilled -); - -export const getConsumerGroupsOrderBy = createSelector( - consumerGroupsState, - (state) => state.orderBy -); - -export const getConsumerGroupsSortOrder = createSelector( - consumerGroupsState, - (state) => state.sortOrder -); - -export const getConsumerGroupsTotalPages = createSelector( - consumerGroupsState, - (state) => state.totalPages -); - -export default consumerGroupsSlice.reducer; diff --git a/kafka-ui-react-app/src/redux/reducers/index.ts b/kafka-ui-react-app/src/redux/reducers/index.ts index f345d9d858c..aa5cb69cf08 100644 --- a/kafka-ui-react-app/src/redux/reducers/index.ts +++ b/kafka-ui-react-app/src/redux/reducers/index.ts @@ -1,24 +1,10 @@ import { combineReducers } from '@reduxjs/toolkit'; -import clusters from 'redux/reducers/clusters/clustersSlice'; import loader from 'redux/reducers/loader/loaderSlice'; -import brokers from 'redux/reducers/brokers/brokersSlice'; -import alerts from 'redux/reducers/alerts/alertsSlice'; import schemas from 'redux/reducers/schemas/schemasSlice'; -import connect from 'redux/reducers/connect/connectSlice'; import topicMessages from 'redux/reducers/topicMessages/topicMessagesSlice'; -import topics from 'redux/reducers/topics/topicsSlice'; -import consumerGroups from 'redux/reducers/consumerGroups/consumerGroupsSlice'; -import ksqlDb from 'redux/reducers/ksqlDb/ksqlDbSlice'; export default combineReducers({ loader, - alerts, - topics, topicMessages, - clusters, - brokers, - consumerGroups, schemas, - connect, - ksqlDb, }); diff --git a/kafka-ui-react-app/src/redux/reducers/ksqlDb/__test__/fixtures.ts b/kafka-ui-react-app/src/redux/reducers/ksqlDb/__test__/fixtures.ts deleted file mode 100644 index 73ef6644ade..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/ksqlDb/__test__/fixtures.ts +++ /dev/null @@ -1,64 +0,0 @@ -export const fetchKsqlDbTablesPayload: { - tables: Dictionary[]; - streams: Dictionary[]; -} = { - tables: [ - { - type: 'TABLE', - name: 'USERS', - topic: 'users', - keyFormat: 'KAFKA', - valueFormat: 'AVRO', - isWindowed: 'false', - }, - { - type: 'TABLE', - name: 'USERS2', - topic: 'users', - keyFormat: 'KAFKA', - valueFormat: 'AVRO', - isWindowed: 'false', - }, - ], - streams: [ - { - type: 'STREAM', - name: 'KSQL_PROCESSING_LOG', - topic: 'default_ksql_processing_log', - keyFormat: 'KAFKA', - valueFormat: 'JSON', - isWindowed: 'false', - }, - { - type: 'STREAM', - name: 'PAGEVIEWS', - topic: 'pageviews', - keyFormat: 'KAFKA', - valueFormat: 'AVRO', - isWindowed: 'false', - }, - ], -}; - -export const ksqlCommandResponse = { - header: 'Test header', - columnNames: [ - 'type', - 'name', - 'topic', - 'keyFormat', - 'valueFormat', - 'isWindowed', - ], - rows: [ - [ - 'STREAM', - 'KSQL_PROCESSING_LOG', - 'default_ksql_processing_log', - 'KAFKA', - 'JSON', - 'false', - ], - ['STREAM', 'PAGEVIEWS', 'pageviews', 'KAFKA', 'AVRO', 'false'], - ], -}; diff --git a/kafka-ui-react-app/src/redux/reducers/ksqlDb/__test__/selectors.spec.ts b/kafka-ui-react-app/src/redux/reducers/ksqlDb/__test__/selectors.spec.ts deleted file mode 100644 index 44213996d3d..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/ksqlDb/__test__/selectors.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { store } from 'redux/store'; -import * as selectors from 'redux/reducers/ksqlDb/selectors'; -import { fetchKsqlDbTables } from 'redux/reducers/ksqlDb/ksqlDbSlice'; - -import { fetchKsqlDbTablesPayload } from './fixtures'; - -describe('TopicMessages selectors', () => { - describe('Initial state', () => { - beforeAll(() => { - store.dispatch({ - type: fetchKsqlDbTables.pending.type, - payload: fetchKsqlDbTablesPayload, - }); - }); - - it('Returns empty state', () => { - expect(selectors.getKsqlDbTables(store.getState())).toEqual({ - rows: [], - fetched: false, - fetching: true, - tablesCount: 0, - streamsCount: 0, - }); - }); - }); - - describe('State', () => { - beforeAll(() => { - store.dispatch({ - type: fetchKsqlDbTables.fulfilled.type, - payload: fetchKsqlDbTablesPayload, - }); - }); - - it('Returns tables and streams', () => { - expect(selectors.getKsqlDbTables(store.getState())).toEqual({ - rows: [ - ...fetchKsqlDbTablesPayload.streams, - ...fetchKsqlDbTablesPayload.tables, - ], - fetched: true, - fetching: false, - tablesCount: 2, - streamsCount: 2, - }); - }); - }); -}); diff --git a/kafka-ui-react-app/src/redux/reducers/ksqlDb/ksqlDbSlice.ts b/kafka-ui-react-app/src/redux/reducers/ksqlDb/ksqlDbSlice.ts deleted file mode 100644 index 33f65a81759..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/ksqlDb/ksqlDbSlice.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { KsqlState } from 'redux/interfaces/ksqlDb'; -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; -import { BASE_PARAMS } from 'lib/constants'; -import { - Configuration, - ExecuteKsqlRequest, - KsqlApi, - Table as KsqlTable, -} from 'generated-sources'; -import { ClusterName } from 'redux/interfaces'; - -const apiClientConf = new Configuration(BASE_PARAMS); -export const ksqlDbApiClient = new KsqlApi(apiClientConf); - -export const transformKsqlResponse = ( - rawTable: Required -): Dictionary[] => - rawTable.rows.map((row) => - row.reduce( - (res, acc, index) => ({ - ...res, - [rawTable.headers[index]]: acc, - }), - {} as Dictionary - ) - ); - -const getTables = (clusterName: ClusterName) => - ksqlDbApiClient.executeKsqlCommand({ - clusterName, - ksqlCommand: { ksql: 'SHOW TABLES;' }, - }); - -const getStreams = (clusterName: ClusterName) => - ksqlDbApiClient.executeKsqlCommand({ - clusterName, - ksqlCommand: { ksql: 'SHOW STREAMS;' }, - }); - -export const fetchKsqlDbTables = createAsyncThunk( - 'ksqlDb/fetchKsqlDbTables', - async (clusterName: ClusterName) => { - const [tables, streams] = await Promise.all([ - getTables(clusterName), - getStreams(clusterName), - ]); - - return { - tables: tables.data ? transformKsqlResponse(tables.data) : [], - streams: streams.data ? transformKsqlResponse(streams.data) : [], - }; - } -); - -export const executeKsql = createAsyncThunk( - 'ksqlDb/executeKsql', - (params: ExecuteKsqlRequest) => ksqlDbApiClient.executeKsql(params) -); - -export const initialState: KsqlState = { - streams: [], - tables: [], - executionResult: null, -}; - -export const ksqlDbSlice = createSlice({ - name: 'ksqlDb', - initialState, - reducers: { - resetExecutionResult: (state) => ({ - ...state, - executionResult: null, - }), - }, - extraReducers: (builder) => { - builder.addCase(fetchKsqlDbTables.fulfilled, (state, action) => ({ - ...state, - ...action.payload, - })); - builder.addCase(executeKsql.fulfilled, (state, action) => ({ - ...state, - executionResult: action.payload, - })); - }, -}); - -export const { resetExecutionResult } = ksqlDbSlice.actions; - -export default ksqlDbSlice.reducer; diff --git a/kafka-ui-react-app/src/redux/reducers/ksqlDb/selectors.ts b/kafka-ui-react-app/src/redux/reducers/ksqlDb/selectors.ts deleted file mode 100644 index 6f8bbcd61b4..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/ksqlDb/selectors.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { RootState } from 'redux/interfaces'; -import { createFetchingSelector } from 'redux/reducers/loader/selectors'; -import { KsqlState } from 'redux/interfaces/ksqlDb'; -import { AsyncRequestStatus } from 'lib/constants'; - -const ksqlDbState = ({ ksqlDb }: RootState): KsqlState => ksqlDb; - -const getKsqlDbFetchTablesAndStreamsFetchingStatus = createFetchingSelector( - 'ksqlDb/fetchKsqlDbTables' -); - -const getKsqlExecutionStatus = createFetchingSelector('ksqlDb/executeKsql'); - -export const getKsqlDbTables = createSelector( - [ksqlDbState, getKsqlDbFetchTablesAndStreamsFetchingStatus], - (state, status) => ({ - rows: [...state.streams, ...state.tables], - fetched: status === AsyncRequestStatus.fulfilled, - fetching: status === AsyncRequestStatus.pending, - tablesCount: state.tables.length, - streamsCount: state.streams.length, - }) -); - -export const getKsqlExecution = createSelector( - [ksqlDbState, getKsqlExecutionStatus], - (state, status) => ({ - executionResult: state.executionResult, - fetched: status === AsyncRequestStatus.fulfilled, - fetching: status === AsyncRequestStatus.pending, - }) -); diff --git a/kafka-ui-react-app/src/redux/reducers/loader/loaderSlice.ts b/kafka-ui-react-app/src/redux/reducers/loader/loaderSlice.ts index 47950d4559b..294b359116c 100644 --- a/kafka-ui-react-app/src/redux/reducers/loader/loaderSlice.ts +++ b/kafka-ui-react-app/src/redux/reducers/loader/loaderSlice.ts @@ -4,16 +4,12 @@ import { UnknownAsyncThunkPendingAction, UnknownAsyncThunkRejectedAction, } from '@reduxjs/toolkit/dist/matchers'; -import { ClustersApi, Configuration } from 'generated-sources'; -import { BASE_PARAMS, AsyncRequestStatus } from 'lib/constants'; +import { AsyncRequestStatus } from 'lib/constants'; import { LoaderSliceState } from 'redux/interfaces'; -const apiClientConf = new Configuration(BASE_PARAMS); -export const clustersApiClient = new ClustersApi(apiClientConf); +const initialState: LoaderSliceState = {}; -export const initialState: LoaderSliceState = {}; - -export const loaderSlice = createSlice({ +const loaderSlice = createSlice({ name: 'loader', initialState, reducers: { diff --git a/kafka-ui-react-app/src/redux/reducers/schemas/__test__/fixtures.ts b/kafka-ui-react-app/src/redux/reducers/schemas/__test__/fixtures.ts index 02c8d2278c9..5cab12584b7 100644 --- a/kafka-ui-react-app/src/redux/reducers/schemas/__test__/fixtures.ts +++ b/kafka-ui-react-app/src/redux/reducers/schemas/__test__/fixtures.ts @@ -29,6 +29,14 @@ export const schemaVersion2: SchemaSubject = { compatibilityLevel: 'FORWARD_TRANSITIVE', schemaType: SchemaType.JSON, }; +export const schemaVersionWithNonAsciiChars: SchemaSubject = { + subject: 'test/test', + version: '1', + id: 29, + schema: '13', + compatibilityLevel: 'FORWARD_TRANSITIVE', + schemaType: SchemaType.JSON, +}; export { schemaVersion1 as schemaVersion }; @@ -45,17 +53,3 @@ export const schemasFulfilledState = { entities: {}, }, }; - -export const versionFulfilledState = { - totalPages: 1, - ids: [], - entities: {}, - versions: { - latest: schemaVersion2, - ids: [schemaVersion1.id, schemaVersion2.id], - entities: { - [schemaVersion2.id]: schemaVersion2, - [schemaVersion1.id]: schemaVersion1, - }, - }, -}; diff --git a/kafka-ui-react-app/src/redux/reducers/schemas/schemasSlice.ts b/kafka-ui-react-app/src/redux/reducers/schemas/schemasSlice.ts index 899cdc64a66..018775ced0a 100644 --- a/kafka-ui-react-app/src/redux/reducers/schemas/schemasSlice.ts +++ b/kafka-ui-react-app/src/redux/reducers/schemas/schemasSlice.ts @@ -5,21 +5,17 @@ import { createSlice, } from '@reduxjs/toolkit'; import { - Configuration, - SchemasApi, SchemaSubject, SchemaSubjectsResponse, GetSchemasRequest, GetLatestSchemaRequest, } from 'generated-sources'; -import { BASE_PARAMS, AsyncRequestStatus } from 'lib/constants'; -import { getResponse } from 'lib/errorHandling'; +import { schemasApiClient } from 'lib/api'; +import { AsyncRequestStatus } from 'lib/constants'; +import { getResponse, showServerError } from 'lib/errorHandling'; import { ClusterName, RootState } from 'redux/interfaces'; import { createFetchingSelector } from 'redux/reducers/loader/selectors'; -const apiClientConf = new Configuration(BASE_PARAMS); -export const schemasApiClient = new SchemasApi(apiClientConf); - export const SCHEMA_LATEST_FETCH_ACTION = 'schemas/latest/fetch'; export const fetchLatestSchema = createAsyncThunk< SchemaSubject, @@ -28,6 +24,7 @@ export const fetchLatestSchema = createAsyncThunk< try { return await schemasApiClient.getLatestSchema(schemaParams); } catch (error) { + showServerError(error as Response); return rejectWithValue(await getResponse(error as Response)); } }); @@ -47,6 +44,7 @@ export const fetchSchemas = createAsyncThunk< search: search || undefined, }); } catch (error) { + showServerError(error as Response); return rejectWithValue(await getResponse(error as Response)); } } @@ -65,6 +63,7 @@ export const fetchSchemaVersions = createAsyncThunk< subject, }); } catch (error) { + showServerError(error as Response); return rejectWithValue(await getResponse(error as Response)); } } @@ -110,15 +109,13 @@ const schemasSlice = createSlice({ }, }); -export const { selectAll: selectAllSchemas, selectById: selectSchemaById } = +export const { selectAll: selectAllSchemas } = schemasAdapter.getSelectors((state) => state.schemas); -export const { - selectAll: selectAllSchemaVersions, - selectById: selectVersionSchemaByID, -} = schemaVersionsAdapter.getSelectors( - (state) => state.schemas.versions -); +export const { selectAll: selectAllSchemaVersions } = + schemaVersionsAdapter.getSelectors( + (state) => state.schemas.versions + ); const getSchemaVersions = (state: RootState) => state.schemas.versions; export const getSchemaLatest = createSelector( @@ -137,6 +134,11 @@ export const getAreSchemaLatestFulfilled = createSelector( createFetchingSelector(SCHEMA_LATEST_FETCH_ACTION), (status) => status === AsyncRequestStatus.fulfilled ); +export const getAreSchemaLatestRejected = createSelector( + createFetchingSelector(SCHEMA_LATEST_FETCH_ACTION), + (status) => status === AsyncRequestStatus.rejected +); + export const getAreSchemaVersionsFulfilled = createSelector( createFetchingSelector(SCHEMAS_VERSIONS_FETCH_ACTION), (status) => status === AsyncRequestStatus.fulfilled diff --git a/kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/reducer.spec.ts b/kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/reducer.spec.ts index c2cfa73f273..b9fa4cd4ec1 100644 --- a/kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/reducer.spec.ts +++ b/kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/reducer.spec.ts @@ -1,6 +1,5 @@ import reducer, { addTopicMessage, - clearTopicMessages, resetTopicMessages, updateTopicMessagesMeta, updateTopicMessagesPhase, @@ -12,9 +11,6 @@ import { topicMessagesMetaPayload, } from './fixtures'; -const clusterName = 'local'; -const topicName = 'localTopic'; - describe('TopicMessages reducer', () => { it('Adds new message', () => { const state = reducer( @@ -67,24 +63,6 @@ describe('TopicMessages reducer', () => { expect(newState.messages.length).toEqual(0); }); - it('clear messages', () => { - const state = reducer( - undefined, - addTopicMessage({ message: topicMessagePayload }) - ); - expect(state.messages.length).toEqual(1); - - expect( - reducer(state, { - type: clearTopicMessages.fulfilled, - payload: { clusterName, topicName }, - }) - ).toEqual({ - ...state, - messages: [], - }); - }); - it('Updates Topic Messages Phase', () => { const phase = 'Polling'; diff --git a/kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/selectors.spec.ts b/kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/selectors.spec.ts index 3e84e730c88..d324a2d5e97 100644 --- a/kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/selectors.spec.ts +++ b/kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/selectors.spec.ts @@ -9,6 +9,10 @@ import { import { topicMessagePayload, topicMessagesMetaPayload } from './fixtures'; +const newTopicMessagePayload = { + ...topicMessagePayload, + timestamp: topicMessagePayload.timestamp.toString(), +}; describe('TopicMessages selectors', () => { describe('Initial state', () => { it('returns empty message array', () => { @@ -28,14 +32,18 @@ describe('TopicMessages selectors', () => { describe('state', () => { beforeAll(() => { - store.dispatch(addTopicMessage({ message: topicMessagePayload })); + store.dispatch( + addTopicMessage({ + message: newTopicMessagePayload, + }) + ); store.dispatch(updateTopicMessagesPhase('consuming')); store.dispatch(updateTopicMessagesMeta(topicMessagesMetaPayload)); }); it('returns messages', () => { expect(selectors.getTopicMessges(store.getState())).toEqual([ - topicMessagePayload, + newTopicMessagePayload, ]); }); diff --git a/kafka-ui-react-app/src/redux/reducers/topicMessages/selectors.ts b/kafka-ui-react-app/src/redux/reducers/topicMessages/selectors.ts index 03adca8e424..b2636cdf2ad 100644 --- a/kafka-ui-react-app/src/redux/reducers/topicMessages/selectors.ts +++ b/kafka-ui-react-app/src/redux/reducers/topicMessages/selectors.ts @@ -23,3 +23,8 @@ export const getIsTopicMessagesFetching = createSelector( topicMessagesState, ({ isFetching }) => isFetching ); + +export const getIsTopicMessagesType = createSelector( + topicMessagesState, + ({ messageEventType }) => messageEventType +); diff --git a/kafka-ui-react-app/src/redux/reducers/topicMessages/topicMessagesSlice.ts b/kafka-ui-react-app/src/redux/reducers/topicMessages/topicMessagesSlice.ts index 8760c954943..530a3781140 100644 --- a/kafka-ui-react-app/src/redux/reducers/topicMessages/topicMessagesSlice.ts +++ b/kafka-ui-react-app/src/redux/reducers/topicMessages/topicMessagesSlice.ts @@ -1,43 +1,6 @@ -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; -import { TopicMessagesState, ClusterName, TopicName } from 'redux/interfaces'; -import { TopicMessage, Configuration, MessagesApi } from 'generated-sources'; -import { BASE_PARAMS } from 'lib/constants'; -import { getResponse } from 'lib/errorHandling'; -import { showSuccessAlert } from 'redux/reducers/alerts/alertsSlice'; -import { fetchTopicDetails } from 'redux/reducers/topics/topicsSlice'; - -const apiClientConf = new Configuration(BASE_PARAMS); -export const messagesApiClient = new MessagesApi(apiClientConf); - -export const clearTopicMessages = createAsyncThunk< - undefined, - { clusterName: ClusterName; topicName: TopicName; partitions?: number[] } ->( - 'topicMessages/clearTopicMessages', - async ( - { clusterName, topicName, partitions }, - { rejectWithValue, dispatch } - ) => { - try { - await messagesApiClient.deleteTopicMessages({ - clusterName, - topicName, - partitions, - }); - dispatch(fetchTopicDetails({ clusterName, topicName })); - dispatch( - showSuccessAlert({ - id: `message-${topicName}-${clusterName}-${partitions}`, - message: 'Messages successfully cleared!', - }) - ); - - return undefined; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } - } -); +import { createSlice } from '@reduxjs/toolkit'; +import { TopicMessagesState } from 'redux/interfaces'; +import { TopicMessage } from 'generated-sources'; export const initialState: TopicMessagesState = { messages: [], @@ -47,10 +10,11 @@ export const initialState: TopicMessagesState = { messagesConsumed: 0, isCancelled: false, }, + messageEventType: '', isFetching: false, }; -export const topicMessagesSlice = createSlice({ +const topicMessagesSlice = createSlice({ name: 'topicMessages', initialState, reducers: { @@ -74,11 +38,10 @@ export const topicMessagesSlice = createSlice({ setTopicMessagesFetchingStatus: (state, action) => { state.isFetching = action.payload; }, - }, - extraReducers: (builder) => { - builder.addCase(clearTopicMessages.fulfilled, (state) => { - state.messages = []; - }); + + setMessageEventType: (state, action) => { + state.messageEventType = action.payload; + }, }, }); @@ -88,6 +51,7 @@ export const { updateTopicMessagesPhase, updateTopicMessagesMeta, setTopicMessagesFetchingStatus, + setMessageEventType, } = topicMessagesSlice.actions; export default topicMessagesSlice.reducer; diff --git a/kafka-ui-react-app/src/redux/reducers/topics/__test__/fixtures.ts b/kafka-ui-react-app/src/redux/reducers/topics/__test__/fixtures.ts deleted file mode 100644 index d7296d4b396..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/topics/__test__/fixtures.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { SortOrder, Topic, ConsumerGroup } from 'generated-sources'; -import { TopicsState, TopicWithDetailedInfo } from 'redux/interfaces'; - -export const internalTopicPayload = { - name: '__internal.topic', - internal: true, - partitionCount: 1, - replicationFactor: 1, - replicas: 1, - inSyncReplicas: 1, - segmentSize: 0, - segmentCount: 1, - underReplicatedPartitions: 0, - partitions: [ - { - partition: 0, - leader: 1, - replicas: [{ broker: 1, leader: false, inSync: true }], - offsetMax: 0, - offsetMin: 0, - }, - ], -}; - -export const externalTopicPayload = { - name: 'external.topic', - internal: false, - partitionCount: 1, - replicationFactor: 1, - replicas: 1, - inSyncReplicas: 1, - segmentSize: 1263, - segmentCount: 1, - underReplicatedPartitions: 0, - partitions: [ - { - partition: 0, - leader: 1, - replicas: [{ broker: 1, leader: false, inSync: true }], - offsetMax: 0, - offsetMin: 0, - }, - ], -}; - -export const topicsPayload: Topic[] = [ - internalTopicPayload, - externalTopicPayload, -]; - -export const getTopicStateFixtures = ( - topics: TopicWithDetailedInfo[], - consumerGroups?: ConsumerGroup[] -): TopicsState => { - const byName = topics.reduce((acc: { [i in string]: Topic }, curr) => { - const obj = { ...acc }; - obj[curr.name] = curr; - return obj; - }, {} as { [i in string]: Topic }); - - const allNames = Object.keys(byName); - - return { - byName, - allNames, - totalPages: 1, - search: '', - orderBy: null, - sortOrder: SortOrder.ASC, - consumerGroups: - consumerGroups && consumerGroups.length ? consumerGroups : [], - }; -}; diff --git a/kafka-ui-react-app/src/redux/reducers/topics/__test__/reducer.spec.ts b/kafka-ui-react-app/src/redux/reducers/topics/__test__/reducer.spec.ts deleted file mode 100644 index c4a0657970a..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/topics/__test__/reducer.spec.ts +++ /dev/null @@ -1,934 +0,0 @@ -import { - MessageSchemaSourceEnum, - SortOrder, - TopicColumnsToSort, - ConfigSource, -} from 'generated-sources'; -import reducer, { - clearTopicsMessages, - setTopicsSearch, - setTopicsOrderBy, - fetchTopicConsumerGroups, - fetchTopicMessageSchema, - recreateTopic, - createTopic, - deleteTopic, - fetchTopicsList, - fetchTopicDetails, - fetchTopicConfig, - updateTopic, - updateTopicPartitionsCount, - updateTopicReplicationFactor, - deleteTopics, -} from 'redux/reducers/topics/topicsSlice'; -import { - createTopicPayload, - createTopicResponsePayload, -} from 'components/Topics/New/__test__/fixtures'; -import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures'; -import fetchMock from 'fetch-mock-jest'; -import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator'; -import { getTypeAndPayload } from 'lib/testHelpers'; -import { - alertAdded, - showSuccessAlert, -} from 'redux/reducers/alerts/alertsSlice'; - -const topic = { - name: 'topic', - id: 'id', -}; - -const messageSchema = { - key: { - name: 'key', - source: MessageSchemaSourceEnum.SCHEMA_REGISTRY, - schema: `{ -"$schema": "http://json-schema.org/draft-07/schema#", -"$id": "http://example.com/myURI.schema.json", -"title": "TestRecord", -"type": "object", -"additionalProperties": false, -"properties": { - "f1": { - "type": "integer" - }, - "f2": { - "type": "string" - }, - "schema": { - "type": "string" - } -} -} -`, - }, - value: { - name: 'value', - source: MessageSchemaSourceEnum.SCHEMA_REGISTRY, - schema: `{ -"$schema": "http://json-schema.org/draft-07/schema#", -"$id": "http://example.com/myURI1.schema.json", -"title": "TestRecord", -"type": "object", -"additionalProperties": false, -"properties": { - "f1": { - "type": "integer" - }, - "f2": { - "type": "string" - }, - "schema": { - "type": "string" - } -} -} -`, - }, -}; - -const config = [ - { - name: 'compression.type', - value: 'producer', - defaultValue: 'producer', - source: ConfigSource.DYNAMIC_TOPIC_CONFIG, - isSensitive: false, - isReadOnly: false, - synonyms: [ - { - name: 'compression.type', - value: 'producer', - source: ConfigSource.DYNAMIC_TOPIC_CONFIG, - }, - { - name: 'compression.type', - value: 'producer', - source: ConfigSource.DEFAULT_CONFIG, - }, - ], - }, -]; -const details = { - name: 'local', - internal: false, - partitionCount: 1, - replicationFactor: 1, - replicas: 1, - inSyncReplicas: 1, - segmentSize: 0, - segmentCount: 0, - cleanUpPolicy: 'DELETE', - partitions: [ - { - partition: 0, - leader: 1, - replicas: [{ broker: 1, leader: false, inSync: true }], - offsetMax: 0, - offsetMin: 0, - }, - ], - bytesInPerSec: 0.1, - bytesOutPerSec: 0.1, -}; - -let state = { - byName: { - [topic.name]: topic, - }, - allNames: [topic.name], - messages: [], - totalPages: 1, - search: '', - orderBy: null, - sortOrder: SortOrder.ASC, - consumerGroups: [], -}; -const clusterName = 'local'; - -describe('topics Slice', () => { - describe('topics reducer', () => { - describe('fetch topic details', () => { - it('fetchTopicDetails/fulfilled', () => { - expect( - reducer(state, { - type: fetchTopicDetails.fulfilled, - payload: { - clusterName, - topicName: topic.name, - topicDetails: details, - }, - }) - ).toEqual({ - ...state, - byName: { - [topic.name]: { - ...topic, - ...details, - }, - }, - allNames: [topic.name], - }); - }); - }); - describe('fetch topics', () => { - it('fetchTopicsList/fulfilled', () => { - expect( - reducer(state, { - type: fetchTopicsList.fulfilled, - payload: { clusterName, topicName: topic.name }, - }) - ).toEqual({ - ...state, - byName: { topic }, - allNames: [topic.name], - }); - }); - }); - describe('fetch topic config', () => { - it('fetchTopicConfig/fulfilled', () => { - expect( - reducer(state, { - type: fetchTopicConfig.fulfilled, - payload: { - clusterName, - topicName: topic.name, - topicConfig: config, - }, - }) - ).toEqual({ - ...state, - byName: { - [topic.name]: { - ...topic, - config: config.map((conf) => ({ ...conf })), - }, - }, - allNames: [topic.name], - }); - }); - }); - describe('update topic', () => { - it('updateTopic/fulfilled', () => { - const updatedTopic = { - name: 'topic', - id: 'id', - partitions: 1, - }; - expect( - reducer(state, { - type: updateTopic.fulfilled, - payload: { - clusterName, - topicName: topic.name, - topic: updatedTopic, - }, - }) - ).toEqual({ - ...state, - byName: { - [topic.name]: { - ...updatedTopic, - }, - }, - }); - }); - }); - describe('delete topic', () => { - it('deleteTopic/fulfilled', () => { - expect( - reducer(state, { - type: deleteTopic.fulfilled, - payload: { clusterName, topicName: topic.name }, - }) - ).toEqual({ - ...state, - byName: {}, - allNames: [], - }); - }); - - it('clearTopicsMessages/fulfilled', () => { - expect( - reducer(state, { - type: clearTopicsMessages.fulfilled, - payload: { clusterName, topicName: topic.name }, - }) - ).toEqual({ - ...state, - messages: [], - }); - }); - - it('recreateTopic/fulfilled', () => { - expect( - reducer(state, { - type: recreateTopic.fulfilled, - payload: { topic, topicName: topic.name }, - }) - ).toEqual({ - ...state, - byName: { - [topic.name]: topic, - }, - }); - }); - }); - - describe('create topics', () => { - it('createTopic/fulfilled', () => { - expect( - reducer(state, { - type: createTopic.fulfilled, - payload: { clusterName, data: createTopicPayload }, - }) - ).toEqual({ - ...state, - }); - }); - }); - - describe('search topics', () => { - it('setTopicsSearch', () => { - expect( - reducer(state, { - type: setTopicsSearch, - payload: 'test', - }) - ).toEqual({ - ...state, - search: 'test', - }); - }); - }); - - describe('order topics', () => { - it('setTopicsOrderBy', () => { - expect( - reducer(state, { - type: setTopicsOrderBy, - payload: TopicColumnsToSort.NAME, - }) - ).toEqual({ - ...state, - orderBy: TopicColumnsToSort.NAME, - }); - }); - }); - - describe('topic consumer groups', () => { - it('fetchTopicConsumerGroups/fulfilled', () => { - expect( - reducer(state, { - type: fetchTopicConsumerGroups.fulfilled, - payload: { - clusterName, - topicName: topic.name, - consumerGroups: consumerGroupPayload, - }, - }) - ).toEqual({ - ...state, - byName: { - [topic.name]: { - ...topic, - ...consumerGroupPayload, - }, - }, - }); - }); - }); - - describe('message sending', () => { - it('fetchTopicMessageSchema/fulfilled', () => { - state = { - byName: { - [topic.name]: topic, - }, - allNames: [topic.name], - messages: [], - totalPages: 1, - search: '', - orderBy: null, - sortOrder: SortOrder.ASC, - consumerGroups: [], - }; - expect( - reducer(state, { - type: fetchTopicMessageSchema.fulfilled, - payload: { topicName: topic.name, schema: messageSchema }, - }).byName - ).toEqual({ - [topic.name]: { ...topic, messageSchema }, - }); - }); - }); - }); - describe('Thunks', () => { - const store = mockStoreCreator; - const topicName = topic.name; - const RealDate = Date.now; - - beforeAll(() => { - global.Date.now = jest.fn(() => - new Date('2019-04-07T10:20:30Z').getTime() - ); - }); - afterAll(() => { - global.Date.now = RealDate; - }); - afterEach(() => { - fetchMock.restore(); - store.clearActions(); - }); - describe('fetchTopicsList', () => { - const topicResponse = { - pageCount: 1, - topics: [createTopicResponsePayload], - }; - it('fetchTopicsList/fulfilled', async () => { - fetchMock.getOnce(`/api/clusters/${clusterName}/topics`, topicResponse); - await store.dispatch(fetchTopicsList({ clusterName })); - - expect(getTypeAndPayload(store)).toEqual([ - { type: fetchTopicsList.pending.type }, - { - type: fetchTopicsList.fulfilled.type, - payload: { ...topicResponse }, - }, - ]); - }); - it('fetchTopicsList/rejected', async () => { - fetchMock.getOnce(`/api/clusters/${clusterName}/topics`, 404); - await store.dispatch(fetchTopicsList({ clusterName })); - - expect(getTypeAndPayload(store)).toEqual([ - { type: fetchTopicsList.pending.type }, - { - type: fetchTopicsList.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/topics`, - message: undefined, - }, - }, - ]); - }); - }); - describe('fetchTopicDetails', () => { - it('fetchTopicDetails/fulfilled', async () => { - fetchMock.getOnce( - `/api/clusters/${clusterName}/topics/${topicName}`, - details - ); - await store.dispatch(fetchTopicDetails({ clusterName, topicName })); - - expect(getTypeAndPayload(store)).toEqual([ - { type: fetchTopicDetails.pending.type }, - { - type: fetchTopicDetails.fulfilled.type, - payload: { topicDetails: { ...details }, topicName }, - }, - ]); - }); - it('fetchTopicDetails/rejected', async () => { - fetchMock.getOnce( - `/api/clusters/${clusterName}/topics/${topicName}`, - 404 - ); - await store.dispatch(fetchTopicDetails({ clusterName, topicName })); - - expect(getTypeAndPayload(store)).toEqual([ - { type: fetchTopicDetails.pending.type }, - { - type: fetchTopicDetails.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/topics/${topicName}`, - message: undefined, - }, - }, - ]); - }); - }); - describe('fetchTopicConfig', () => { - it('fetchTopicConfig/fulfilled', async () => { - fetchMock.getOnce( - `/api/clusters/${clusterName}/topics/${topicName}/config`, - config - ); - await store.dispatch(fetchTopicConfig({ clusterName, topicName })); - - expect(getTypeAndPayload(store)).toEqual([ - { type: fetchTopicConfig.pending.type }, - { - type: fetchTopicConfig.fulfilled.type, - payload: { - topicConfig: config, - topicName, - }, - }, - ]); - }); - it('fetchTopicConfig/rejected', async () => { - fetchMock.getOnce( - `/api/clusters/${clusterName}/topics/${topicName}/config`, - 404 - ); - await store.dispatch(fetchTopicConfig({ clusterName, topicName })); - - expect(getTypeAndPayload(store)).toEqual([ - { type: fetchTopicConfig.pending.type }, - { - type: fetchTopicConfig.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/topics/${topicName}/config`, - message: undefined, - }, - }, - ]); - }); - }); - describe('deleteTopic', () => { - it('deleteTopic/fulfilled', async () => { - fetchMock.deleteOnce( - `/api/clusters/${clusterName}/topics/${topicName}`, - topicName - ); - await store.dispatch(deleteTopic({ clusterName, topicName })); - - expect(getTypeAndPayload(store)).toEqual([ - { type: deleteTopic.pending.type }, - { type: showSuccessAlert.pending.type }, - { - type: alertAdded.type, - payload: { - id: 'message-topic-local', - title: '', - type: 'success', - createdAt: global.Date.now(), - message: 'Topic successfully deleted!', - }, - }, - { type: showSuccessAlert.fulfilled.type }, - { - type: deleteTopic.fulfilled.type, - payload: { topicName }, - }, - ]); - }); - it('deleteTopic/rejected', async () => { - fetchMock.deleteOnce( - `/api/clusters/${clusterName}/topics/${topicName}`, - 404 - ); - await store.dispatch(deleteTopic({ clusterName, topicName })); - - expect(getTypeAndPayload(store)).toEqual([ - { type: deleteTopic.pending.type }, - { - type: deleteTopic.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/topics/${topicName}`, - message: undefined, - }, - }, - ]); - }); - }); - describe('deleteTopics', () => { - it('deleteTopics/fulfilled', async () => { - fetchMock.delete(`/api/clusters/${clusterName}/topics/${topicName}`, [ - topicName, - 'topic2', - ]); - await store.dispatch( - deleteTopics({ clusterName, topicNames: [topicName, 'topic2'] }) - ); - - expect(getTypeAndPayload(store)).toEqual([ - { type: deleteTopics.pending.type }, - { type: deleteTopic.pending.type }, - { type: deleteTopic.pending.type }, - { type: deleteTopics.fulfilled.type }, - ]); - }); - }); - describe('recreateTopic', () => { - const recreateResponse = { - cleanUpPolicy: 'DELETE', - inSyncReplicas: 1, - internal: false, - name: topicName, - partitionCount: 1, - partitions: undefined, - replicas: 1, - replicationFactor: 1, - segmentCount: 0, - segmentSize: 0, - underReplicatedPartitions: undefined, - }; - it('recreateTopic/fulfilled', async () => { - fetchMock.postOnce( - `/api/clusters/${clusterName}/topics/${topicName}`, - recreateResponse - ); - await store.dispatch(recreateTopic({ clusterName, topicName })); - - expect(getTypeAndPayload(store)).toEqual([ - { type: recreateTopic.pending.type }, - { - type: recreateTopic.fulfilled.type, - payload: { [topicName]: { ...recreateResponse } }, - }, - ]); - }); - it('recreateTopic/rejected', async () => { - fetchMock.postOnce( - `/api/clusters/${clusterName}/topics/${topicName}`, - 404 - ); - await store.dispatch(recreateTopic({ clusterName, topicName })); - - expect(getTypeAndPayload(store)).toEqual([ - { type: recreateTopic.pending.type }, - { - type: recreateTopic.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/topics/${topicName}`, - message: undefined, - }, - }, - ]); - }); - }); - describe('fetchTopicConsumerGroups', () => { - const consumerGroups = [ - { - groupId: 'groupId1', - members: 0, - topics: 1, - simple: false, - partitionAssignor: '', - coordinator: { - id: 1, - port: undefined, - host: 'host', - }, - messagesBehind: undefined, - state: undefined, - }, - { - groupId: 'groupId2', - members: 0, - topics: 1, - simple: false, - partitionAssignor: '', - coordinator: { - id: 1, - port: undefined, - host: 'host', - }, - messagesBehind: undefined, - state: undefined, - }, - ]; - it('fetchTopicConsumerGroups/fulfilled', async () => { - fetchMock.getOnce( - `/api/clusters/${clusterName}/topics/${topicName}/consumer-groups`, - consumerGroups - ); - await store.dispatch( - fetchTopicConsumerGroups({ clusterName, topicName }) - ); - - expect(getTypeAndPayload(store)).toEqual([ - { type: fetchTopicConsumerGroups.pending.type }, - { - type: fetchTopicConsumerGroups.fulfilled.type, - payload: { consumerGroups, topicName }, - }, - ]); - }); - it('fetchTopicConsumerGroups/rejected', async () => { - fetchMock.getOnce( - `/api/clusters/${clusterName}/topics/${topicName}/consumer-groups`, - 404 - ); - await store.dispatch( - fetchTopicConsumerGroups({ clusterName, topicName }) - ); - - expect(getTypeAndPayload(store)).toEqual([ - { type: fetchTopicConsumerGroups.pending.type }, - { - type: fetchTopicConsumerGroups.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/topics/${topicName}/consumer-groups`, - message: undefined, - }, - }, - ]); - }); - }); - describe('updateTopicPartitionsCount', () => { - it('updateTopicPartitionsCount/fulfilled', async () => { - fetchMock.patchOnce( - `/api/clusters/${clusterName}/topics/${topicName}/partitions`, - { message: 'success' } - ); - await store.dispatch( - updateTopicPartitionsCount({ - clusterName, - topicName, - partitions: 1, - }) - ); - expect(getTypeAndPayload(store)).toEqual([ - { type: updateTopicPartitionsCount.pending.type }, - { type: showSuccessAlert.pending.type }, - { - type: alertAdded.type, - payload: { - id: 'message-topic-local-1', - title: '', - type: 'success', - createdAt: global.Date.now(), - message: 'Number of partitions successfully increased!', - }, - }, - { type: fetchTopicDetails.pending.type }, - { type: showSuccessAlert.fulfilled.type }, - { - type: updateTopicPartitionsCount.fulfilled.type, - }, - ]); - }); - it('updateTopicPartitionsCount/rejected', async () => { - fetchMock.patchOnce( - `/api/clusters/${clusterName}/topics/${topicName}/partitions`, - 404 - ); - await store.dispatch( - updateTopicPartitionsCount({ - clusterName, - topicName, - partitions: 1, - }) - ); - - expect(getTypeAndPayload(store)).toEqual([ - { type: updateTopicPartitionsCount.pending.type }, - { - type: updateTopicPartitionsCount.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/topics/${topicName}/partitions`, - message: undefined, - }, - }, - ]); - }); - }); - describe('updateTopicReplicationFactor', () => { - it('updateTopicReplicationFactor/fulfilled', async () => { - fetchMock.patchOnce( - `/api/clusters/${clusterName}/topics/${topicName}/replications`, - { message: 'success' } - ); - await store.dispatch( - updateTopicReplicationFactor({ - clusterName, - topicName, - replicationFactor: 1, - }) - ); - - expect(getTypeAndPayload(store)).toEqual([ - { type: updateTopicReplicationFactor.pending.type }, - { - type: updateTopicReplicationFactor.fulfilled.type, - }, - ]); - }); - it('updateTopicReplicationFactor/rejected', async () => { - fetchMock.patchOnce( - `/api/clusters/${clusterName}/topics/${topicName}/replications`, - 404 - ); - await store.dispatch( - updateTopicReplicationFactor({ - clusterName, - topicName, - replicationFactor: 1, - }) - ); - - expect(getTypeAndPayload(store)).toEqual([ - { type: updateTopicReplicationFactor.pending.type }, - { - type: updateTopicReplicationFactor.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/topics/${topicName}/replications`, - message: undefined, - }, - }, - ]); - }); - }); - describe('createTopic', () => { - const newTopic = { - name: 'newTopic', - partitions: 0, - replicationFactor: 0, - minInsyncReplicas: 0, - cleanupPolicy: 'DELETE', - retentionMs: 1, - retentionBytes: 1, - maxMessageBytes: 1, - customParams: [ - { - name: '', - value: '', - }, - ], - }; - it('createTopic/fulfilled', async () => { - fetchMock.postOnce(`/api/clusters/${clusterName}/topics`, { - message: 'success', - }); - await store.dispatch( - createTopic({ - clusterName, - data: newTopic, - }) - ); - - expect(getTypeAndPayload(store)).toEqual([ - { type: createTopic.pending.type }, - { - type: createTopic.fulfilled.type, - }, - ]); - }); - it('createTopic/rejected', async () => { - fetchMock.postOnce(`/api/clusters/${clusterName}/topics`, 404); - await store.dispatch( - createTopic({ - clusterName, - data: newTopic, - }) - ); - - expect(getTypeAndPayload(store)).toEqual([ - { type: createTopic.pending.type }, - { - type: createTopic.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/topics`, - message: undefined, - }, - }, - ]); - }); - }); - describe('updateTopic', () => { - const updateTopicResponse = { - name: topicName, - partitions: 0, - replicationFactor: 0, - minInsyncReplicas: 0, - cleanupPolicy: 'DELETE', - retentionMs: 0, - retentionBytes: 0, - maxMessageBytes: 0, - customParams: { - byIndex: {}, - allIndexes: [], - }, - }; - it('updateTopic/fulfilled', async () => { - fetchMock.patchOnce( - `/api/clusters/${clusterName}/topics/${topicName}`, - createTopicResponsePayload - ); - await store.dispatch( - updateTopic({ - clusterName, - topicName, - form: updateTopicResponse, - }) - ); - - expect(getTypeAndPayload(store)).toEqual([ - { type: updateTopic.pending.type }, - { - type: updateTopic.fulfilled.type, - payload: { [topicName]: { ...createTopicResponsePayload } }, - }, - ]); - }); - it('updateTopic/rejected', async () => { - fetchMock.patchOnce( - `/api/clusters/${clusterName}/topics/${topicName}`, - 404 - ); - await store.dispatch( - updateTopic({ - clusterName, - topicName, - form: updateTopicResponse, - }) - ); - - expect(getTypeAndPayload(store)).toEqual([ - { type: updateTopic.pending.type }, - { - type: updateTopic.rejected.type, - payload: { - status: 404, - statusText: 'Not Found', - url: `/api/clusters/${clusterName}/topics/${topicName}`, - message: undefined, - }, - }, - ]); - }); - }); - describe('clearTopicsMessages', () => { - it('clearTopicsMessages/fulfilled', async () => { - fetchMock.deleteOnce( - `/api/clusters/${clusterName}/topics/${topicName}/messages`, - [topicName, 'topic2'] - ); - await store.dispatch( - clearTopicsMessages({ - clusterName, - topicNames: [topicName, 'topic2'], - }) - ); - - expect(getTypeAndPayload(store)).toEqual([ - { type: clearTopicsMessages.pending.type }, - { type: clearTopicsMessages.fulfilled.type }, - ]); - }); - }); - }); -}); diff --git a/kafka-ui-react-app/src/redux/reducers/topics/__test__/selectors.spec.ts b/kafka-ui-react-app/src/redux/reducers/topics/__test__/selectors.spec.ts deleted file mode 100644 index 1fc223b48f8..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/topics/__test__/selectors.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { store } from 'redux/store'; -import * as selectors from 'redux/reducers/topics/selectors'; - -describe('Topics selectors', () => { - describe('Initial State', () => { - it('returns initial values', () => { - expect(selectors.getTopicListTotalPages(store.getState())).toEqual(1); - expect(selectors.getIsTopicDeleted(store.getState())).toBeFalsy(); - expect(selectors.getAreTopicsFetching(store.getState())).toEqual(false); - expect(selectors.getAreTopicsFetched(store.getState())).toEqual(false); - expect(selectors.getIsTopicDetailsFetching(store.getState())).toEqual( - false - ); - expect(selectors.getIsTopicDetailsFetched(store.getState())).toEqual( - false - ); - expect(selectors.getTopicConfigFetched(store.getState())).toEqual(false); - expect(selectors.getTopicCreated(store.getState())).toEqual(false); - expect(selectors.getTopicUpdated(store.getState())).toEqual(false); - expect(selectors.getTopicMessageSchemaFetched(store.getState())).toEqual( - false - ); - expect( - selectors.getTopicPartitionsCountIncreased(store.getState()) - ).toEqual(false); - expect( - selectors.getTopicReplicationFactorUpdated(store.getState()) - ).toEqual(false); - expect( - selectors.getTopicsConsumerGroupsFetched(store.getState()) - ).toEqual(false); - expect(selectors.getTopicList(store.getState())).toEqual([]); - }); - }); -}); diff --git a/kafka-ui-react-app/src/redux/reducers/topics/selectors.ts b/kafka-ui-react-app/src/redux/reducers/topics/selectors.ts deleted file mode 100644 index 031d86a0fbe..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/topics/selectors.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { - RootState, - TopicName, - TopicsState, - TopicConfigByName, -} from 'redux/interfaces'; -import { CleanUpPolicy } from 'generated-sources'; -import { createFetchingSelector } from 'redux/reducers/loader/selectors'; -import { - fetchTopicsList, - fetchTopicDetails, - fetchTopicConfig, - updateTopic, - fetchTopicMessageSchema, - fetchTopicConsumerGroups, - createTopic, - deleteTopic, - updateTopicPartitionsCount, - updateTopicReplicationFactor, -} from 'redux/reducers/topics/topicsSlice'; -import { AsyncRequestStatus } from 'lib/constants'; - -const topicsState = ({ topics }: RootState): TopicsState => topics; - -const getAllNames = (state: RootState) => topicsState(state).allNames; -const getTopicMap = (state: RootState) => topicsState(state).byName; - -export const getTopicListTotalPages = (state: RootState) => - topicsState(state).totalPages; - -const getTopicDeletingStatus = createFetchingSelector(deleteTopic.typePrefix); - -export const getIsTopicDeleted = createSelector( - getTopicDeletingStatus, - (status) => status === AsyncRequestStatus.fulfilled -); - -const getAreTopicsFetchingStatus = createFetchingSelector( - fetchTopicsList.typePrefix -); - -export const getAreTopicsFetching = createSelector( - getAreTopicsFetchingStatus, - (status) => status === AsyncRequestStatus.pending -); -export const getAreTopicsFetched = createSelector( - getAreTopicsFetchingStatus, - (status) => status === AsyncRequestStatus.fulfilled -); - -const getTopicDetailsFetchingStatus = createFetchingSelector( - fetchTopicDetails.typePrefix -); - -export const getIsTopicDetailsFetching = createSelector( - getTopicDetailsFetchingStatus, - (status) => status === AsyncRequestStatus.pending -); - -export const getIsTopicDetailsFetched = createSelector( - getTopicDetailsFetchingStatus, - (status) => status === AsyncRequestStatus.fulfilled -); - -const getTopicConfigFetchingStatus = createFetchingSelector( - fetchTopicConfig.typePrefix -); - -export const getTopicConfigFetched = createSelector( - getTopicConfigFetchingStatus, - (status) => status === AsyncRequestStatus.fulfilled -); - -const getTopicCreationStatus = createFetchingSelector(createTopic.typePrefix); - -export const getTopicCreated = createSelector( - getTopicCreationStatus, - (status) => status === AsyncRequestStatus.fulfilled -); - -const getTopicUpdateStatus = createFetchingSelector(updateTopic.typePrefix); - -export const getTopicUpdated = createSelector( - getTopicUpdateStatus, - (status) => status === AsyncRequestStatus.fulfilled -); - -const getTopicMessageSchemaFetchingStatus = createFetchingSelector( - fetchTopicMessageSchema.typePrefix -); - -export const getTopicMessageSchemaFetched = createSelector( - getTopicMessageSchemaFetchingStatus, - (status) => status === AsyncRequestStatus.fulfilled -); - -const getPartitionsCountIncreaseStatus = createFetchingSelector( - updateTopicPartitionsCount.typePrefix -); - -export const getTopicPartitionsCountIncreased = createSelector( - getPartitionsCountIncreaseStatus, - (status) => status === AsyncRequestStatus.fulfilled -); - -const getReplicationFactorUpdateStatus = createFetchingSelector( - updateTopicReplicationFactor.typePrefix -); - -export const getTopicReplicationFactorUpdated = createSelector( - getReplicationFactorUpdateStatus, - (status) => status === AsyncRequestStatus.fulfilled -); - -const getTopicConsumerGroupsStatus = createFetchingSelector( - fetchTopicConsumerGroups.typePrefix -); - -export const getTopicsConsumerGroupsFetched = createSelector( - getTopicConsumerGroupsStatus, - (status) => status === AsyncRequestStatus.fulfilled -); - -export const getTopicList = createSelector( - getAreTopicsFetched, - getAllNames, - getTopicMap, - (isFetched, allNames, byName) => { - if (!isFetched) { - return []; - } - return allNames.map((name) => byName[name]); - } -); - -const getTopicName = (_: RootState, topicName: TopicName) => topicName; - -export const getTopicByName = createSelector( - getTopicMap, - getTopicName, - (topics, topicName) => topics[topicName] || {} -); - -export const getPartitionsByTopicName = createSelector( - getTopicMap, - getTopicName, - (topics, topicName) => topics[topicName]?.partitions || [] -); - -export const getFullTopic = createSelector(getTopicByName, (topic) => - topic && topic.config && !!topic.partitionCount ? topic : undefined -); - -export const getTopicConfig = createSelector( - getTopicByName, - ({ config }) => config -); - -export const getTopicConfigByParamName = createSelector( - getTopicConfig, - (config) => { - const byParamName: TopicConfigByName = { - byName: {}, - }; - - if (config) { - config.forEach((param) => { - byParamName.byName[param.name] = param; - }); - } - - return byParamName; - } -); - -export const getIsTopicDeletePolicy = createSelector( - getTopicByName, - (topic) => { - return topic?.cleanUpPolicy === CleanUpPolicy.DELETE; - } -); - -export const getTopicsSearch = createSelector( - topicsState, - (state) => state.search -); - -export const getTopicsOrderBy = createSelector( - topicsState, - (state) => state.orderBy -); - -export const getTopicsSortOrder = createSelector( - topicsState, - (state) => state.sortOrder -); - -export const getIsTopicInternal = createSelector( - getTopicByName, - (topic) => !!topic?.internal -); - -export const getTopicConsumerGroups = createSelector( - getTopicByName, - ({ consumerGroups }) => consumerGroups || [] -); - -export const getMessageSchemaByTopicName = createSelector( - getTopicByName, - (topic) => topic?.messageSchema -); diff --git a/kafka-ui-react-app/src/redux/reducers/topics/topicsSlice.ts b/kafka-ui-react-app/src/redux/reducers/topics/topicsSlice.ts deleted file mode 100644 index dfeb437b242..00000000000 --- a/kafka-ui-react-app/src/redux/reducers/topics/topicsSlice.ts +++ /dev/null @@ -1,434 +0,0 @@ -import { v4 } from 'uuid'; -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; -import { - Configuration, - TopicsApi, - ConsumerGroupsApi, - TopicsResponse, - TopicDetails, - GetTopicsRequest, - GetTopicDetailsRequest, - GetTopicConfigsRequest, - TopicConfig, - TopicCreation, - ConsumerGroup, - Topic, - TopicUpdate, - DeleteTopicRequest, - RecreateTopicRequest, - SortOrder, - TopicColumnsToSort, - MessagesApi, - GetTopicSchemaRequest, - TopicMessageSchema, -} from 'generated-sources'; -import { - TopicsState, - TopicName, - TopicFormData, - TopicFormFormattedParams, - TopicFormDataRaw, - ClusterName, -} from 'redux/interfaces'; -import { BASE_PARAMS } from 'lib/constants'; -import { getResponse } from 'lib/errorHandling'; -import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice'; -import { showSuccessAlert } from 'redux/reducers/alerts/alertsSlice'; - -const apiClientConf = new Configuration(BASE_PARAMS); -const topicsApiClient = new TopicsApi(apiClientConf); -const topicConsumerGroupsApiClient = new ConsumerGroupsApi(apiClientConf); -const messagesApiClient = new MessagesApi(apiClientConf); - -export const fetchTopicsList = createAsyncThunk< - TopicsResponse, - GetTopicsRequest ->('topic/fetchTopicsList', async (payload, { rejectWithValue }) => { - try { - return await topicsApiClient.getTopics(payload); - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } -}); - -export const fetchTopicDetails = createAsyncThunk< - { topicDetails: TopicDetails; topicName: TopicName }, - GetTopicDetailsRequest ->('topic/fetchTopicDetails', async (payload, { rejectWithValue }) => { - try { - const { topicName } = payload; - const topicDetails = await topicsApiClient.getTopicDetails(payload); - - return { topicDetails, topicName }; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } -}); - -export const fetchTopicConfig = createAsyncThunk< - { topicConfig: TopicConfig[]; topicName: TopicName }, - GetTopicConfigsRequest ->('topic/fetchTopicConfig', async (payload, { rejectWithValue }) => { - try { - const { topicName } = payload; - const topicConfig = await topicsApiClient.getTopicConfigs(payload); - - return { topicConfig, topicName }; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } -}); - -const topicReducer = ( - result: TopicFormFormattedParams, - customParam: TopicConfig -) => { - return { - ...result, - [customParam.name]: customParam.value, - }; -}; - -export const formatTopicCreation = (form: TopicFormData): TopicCreation => { - const { - name, - partitions, - replicationFactor, - cleanupPolicy, - retentionBytes, - retentionMs, - maxMessageBytes, - minInsyncReplicas, - customParams, - } = form; - - return { - name, - partitions, - replicationFactor, - configs: { - 'cleanup.policy': cleanupPolicy, - 'retention.ms': retentionMs.toString(), - 'retention.bytes': retentionBytes.toString(), - 'max.message.bytes': maxMessageBytes.toString(), - 'min.insync.replicas': minInsyncReplicas.toString(), - ...Object.values(customParams || {}).reduce(topicReducer, {}), - }, - }; -}; - -export const createTopic = createAsyncThunk< - undefined, - { - clusterName: ClusterName; - data: TopicFormData; - } ->('topic/createTopic', async (payload, { rejectWithValue }) => { - try { - const { data, clusterName } = payload; - await topicsApiClient.createTopic({ - clusterName, - topicCreation: formatTopicCreation(data), - }); - - return undefined; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } -}); - -export const fetchTopicConsumerGroups = createAsyncThunk< - { consumerGroups: ConsumerGroup[]; topicName: TopicName }, - GetTopicConfigsRequest ->('topic/fetchTopicConsumerGroups', async (payload, { rejectWithValue }) => { - try { - const { topicName } = payload; - const consumerGroups = - await topicConsumerGroupsApiClient.getTopicConsumerGroups(payload); - - return { consumerGroups, topicName }; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } -}); - -const formatTopicUpdate = (form: TopicFormDataRaw): TopicUpdate => { - const { - cleanupPolicy, - retentionBytes, - retentionMs, - maxMessageBytes, - minInsyncReplicas, - customParams, - } = form; - - return { - configs: { - 'cleanup.policy': cleanupPolicy, - 'retention.ms': retentionMs, - 'retention.bytes': retentionBytes, - 'max.message.bytes': maxMessageBytes, - 'min.insync.replicas': minInsyncReplicas, - ...Object.values(customParams || {}).reduce(topicReducer, {}), - }, - }; -}; - -export const updateTopic = createAsyncThunk< - { topic: Topic }, - { - clusterName: ClusterName; - topicName: TopicName; - form: TopicFormDataRaw; - } ->( - 'topic/updateTopic', - async ({ clusterName, topicName, form }, { rejectWithValue }) => { - try { - const topic = await topicsApiClient.updateTopic({ - clusterName, - topicName, - topicUpdate: formatTopicUpdate(form), - }); - - return { topic }; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } - } -); - -export const deleteTopic = createAsyncThunk< - { topicName: TopicName }, - DeleteTopicRequest ->('topic/deleteTopic', async (payload, { rejectWithValue, dispatch }) => { - try { - const { topicName, clusterName } = payload; - await topicsApiClient.deleteTopic(payload); - dispatch( - showSuccessAlert({ - id: `message-${topicName}-${clusterName}`, - message: 'Topic successfully deleted!', - }) - ); - return { topicName }; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } -}); - -export const recreateTopic = createAsyncThunk< - { topic: Topic }, - RecreateTopicRequest ->('topic/recreateTopic', async (payload, { rejectWithValue }) => { - try { - const topic = await topicsApiClient.recreateTopic(payload); - return { topic }; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } -}); - -export const fetchTopicMessageSchema = createAsyncThunk< - { schema: TopicMessageSchema; topicName: TopicName }, - GetTopicSchemaRequest ->('topic/fetchTopicMessageSchema', async (payload, { rejectWithValue }) => { - try { - const { topicName } = payload; - const schema = await messagesApiClient.getTopicSchema(payload); - return { schema, topicName }; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } -}); - -export const updateTopicPartitionsCount = createAsyncThunk< - undefined, - { - clusterName: ClusterName; - topicName: TopicName; - partitions: number; - } ->( - 'topic/updateTopicPartitionsCount', - async (payload, { rejectWithValue, dispatch }) => { - try { - const { clusterName, topicName, partitions } = payload; - - await topicsApiClient.increaseTopicPartitions({ - clusterName, - topicName, - partitionsIncrease: { totalPartitionsCount: partitions }, - }); - dispatch( - showSuccessAlert({ - id: `message-${topicName}-${clusterName}-${partitions}`, - message: 'Number of partitions successfully increased!', - }) - ); - dispatch(fetchTopicDetails({ clusterName, topicName })); - return undefined; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } - } -); - -export const updateTopicReplicationFactor = createAsyncThunk< - undefined, - { - clusterName: ClusterName; - topicName: TopicName; - replicationFactor: number; - } ->( - 'topic/updateTopicReplicationFactor', - async (payload, { rejectWithValue }) => { - try { - const { clusterName, topicName, replicationFactor } = payload; - - await topicsApiClient.changeReplicationFactor({ - clusterName, - topicName, - replicationFactorChange: { totalReplicationFactor: replicationFactor }, - }); - - return undefined; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } - } -); - -export const deleteTopics = createAsyncThunk< - undefined, - { - clusterName: ClusterName; - topicNames: TopicName[]; - } ->('topic/deleteTopics', async (payload, { rejectWithValue, dispatch }) => { - try { - const { clusterName, topicNames } = payload; - - topicNames.forEach((topicName) => { - dispatch(deleteTopic({ clusterName, topicName })); - }); - - return undefined; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } -}); - -export const clearTopicsMessages = createAsyncThunk< - undefined, - { - clusterName: ClusterName; - topicNames: TopicName[]; - } ->('topic/clearTopicsMessages', async (payload, { rejectWithValue }) => { - try { - const { clusterName, topicNames } = payload; - - topicNames.forEach((topicName) => { - clearTopicMessages({ clusterName, topicName }); - }); - - return undefined; - } catch (err) { - return rejectWithValue(await getResponse(err as Response)); - } -}); - -export const initialState: TopicsState = { - byName: {}, - allNames: [], - totalPages: 1, - search: '', - orderBy: TopicColumnsToSort.NAME, - sortOrder: SortOrder.ASC, - consumerGroups: [], -}; - -const topicsSlice = createSlice({ - name: 'topics', - initialState, - reducers: { - setTopicsSearch: (state, action) => { - state.search = action.payload; - }, - setTopicsOrderBy: (state, action) => { - state.sortOrder = - state.orderBy === action.payload && state.sortOrder === SortOrder.ASC - ? SortOrder.DESC - : SortOrder.ASC; - state.orderBy = action.payload; - }, - }, - extraReducers: (builder) => { - builder.addCase(fetchTopicsList.fulfilled, (state, { payload }) => { - if (payload.topics) { - state.allNames = []; - state.totalPages = payload.pageCount || 1; - - payload.topics.forEach((topic) => { - state.allNames.push(topic.name); - state.byName[topic.name] = { - ...state.byName[topic.name], - ...topic, - id: v4(), - }; - }); - } - }); - builder.addCase(fetchTopicDetails.fulfilled, (state, { payload }) => { - state.byName[payload.topicName] = { - ...state.byName[payload.topicName], - ...payload.topicDetails, - }; - }); - builder.addCase(fetchTopicConfig.fulfilled, (state, { payload }) => { - state.byName[payload.topicName] = { - ...state.byName[payload.topicName], - config: payload.topicConfig, - }; - }); - builder.addCase( - fetchTopicConsumerGroups.fulfilled, - (state, { payload }) => { - state.byName[payload.topicName] = { - ...state.byName[payload.topicName], - ...payload.consumerGroups, - }; - } - ); - builder.addCase(updateTopic.fulfilled, (state, { payload }) => { - state.byName[payload.topic.name] = { - ...state.byName[payload.topic.name], - ...payload.topic, - }; - }); - builder.addCase(deleteTopic.fulfilled, (state, { payload }) => { - delete state.byName[payload.topicName]; - state.allNames = state.allNames.filter( - (name) => name !== payload.topicName - ); - }); - builder.addCase(recreateTopic.fulfilled, (state, { payload }) => { - state.byName = { - ...state.byName, - [payload.topic.name]: { ...payload.topic }, - }; - }); - builder.addCase(fetchTopicMessageSchema.fulfilled, (state, { payload }) => { - state.byName[payload.topicName] = { - ...state.byName[payload.topicName], - messageSchema: payload.schema, - }; - }); - }, -}); - -export const { setTopicsSearch, setTopicsOrderBy } = topicsSlice.actions; - -export default topicsSlice.reducer; diff --git a/kafka-ui-react-app/src/redux/store/configureStore/mockStoreCreator.ts b/kafka-ui-react-app/src/redux/store/configureStore/mockStoreCreator.ts deleted file mode 100644 index 67bab43dd6b..00000000000 --- a/kafka-ui-react-app/src/redux/store/configureStore/mockStoreCreator.ts +++ /dev/null @@ -1,12 +0,0 @@ -import configureMockStore, { MockStoreCreator } from 'redux-mock-store'; -import thunk, { ThunkDispatch } from 'redux-thunk'; -import { AnyAction, Middleware } from 'redux'; -import { RootState } from 'redux/interfaces'; - -const middlewares: Array = [thunk]; -type DispatchExts = ThunkDispatch; - -const mockStoreCreator: MockStoreCreator = - configureMockStore(middlewares); - -export default mockStoreCreator(); diff --git a/kafka-ui-react-app/src/serviceWorker.ts b/kafka-ui-react-app/src/serviceWorker.ts deleted file mode 100644 index f2566365332..00000000000 --- a/kafka-ui-react-app/src/serviceWorker.ts +++ /dev/null @@ -1,145 +0,0 @@ -// This optional code is used to register a service worker. -// register() is not called by default. - -// This lets the app load faster on subsequent visits in production, and gives -// it offline capabilities. However, it also means that developers (and users) -// will only see deployed updates on subsequent visits to a page, after all the -// existing tabs open on the page have been closed, since previously cached -// resources are updated in the background. - -// To learn more about the benefits of this model and instructions on how to -// opt-in, read https://bit.ly/CRA-PWA -/* eslint-disable no-console */ - -const isLocalhost = Boolean( - window.location.hostname === 'localhost' || - // [::1] is the IPv6 localhost address. - window.location.hostname === '[::1]' || - // 127.0.0.0/8 are considered localhost for IPv4. - window.location.hostname.match( - /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ - ) -); - -type Config = { - onSuccess?: (registration: ServiceWorkerRegistration) => void; - onUpdate?: (registration: ServiceWorkerRegistration) => void; -}; - -function registerValidSW(swUrl: string, config?: Config) { - navigator.serviceWorker - .register(swUrl) - .then((registration) => { - // eslint-disable-next-line no-param-reassign - registration.onupdatefound = () => { - const installingWorker = registration.installing; - if (installingWorker == null) { - return; - } - installingWorker.onstatechange = () => { - if (installingWorker.state === 'installed') { - if (navigator.serviceWorker.controller) { - // At this point, the updated precached content has been fetched, - // but the previous service worker will still serve the older - // content until all client tabs are closed. - console.log( - 'New content is available and will be used when all ' + - 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' - ); - - // Execute callback - if (config && config.onUpdate) { - config.onUpdate(registration); - } - } else { - // At this point, everything has been precached. - // It's the perfect time to display a - // "Content is cached for offline use." message. - console.log('Content is cached for offline use.'); - - // Execute callback - if (config && config.onSuccess) { - config.onSuccess(registration); - } - } - } - }; - }; - }) - .catch((error) => { - console.error('Error during service worker registration:', error); - }); -} - -function checkValidServiceWorker(swUrl: string, config?: Config) { - // Check if the service worker can be found. If it can't reload the page. - fetch(swUrl, { - headers: { 'Service-Worker': 'script' }, - }) - .then((response) => { - // Ensure service worker exists, and that we really are getting a JS file. - const contentType = response.headers.get('content-type'); - if ( - response.status === 404 || - (contentType != null && contentType.indexOf('javascript') === -1) - ) { - // No service worker found. Probably a different app. Reload the page. - navigator.serviceWorker.ready.then((registration) => { - registration.unregister().then(() => { - window.location.reload(); - }); - }); - } else { - // Service worker found. Proceed as normal. - registerValidSW(swUrl, config); - } - }) - .catch(() => { - console.log( - 'No internet connection found. App is running in offline mode.' - ); - }); -} - -export function register(config?: Config) { - if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { - const url = process.env.PUBLIC_URL || 'localhost'; - // The URL constructor is available in all browsers that support SW. - const publicUrl = new URL(url, window.location.href); - if (publicUrl.origin !== window.location.origin) { - // Our service worker won't work if PUBLIC_URL is on a different origin - // from what our page is served on. This might happen if a CDN is used to - // serve assets; see https://github.com/facebook/create-react-app/issues/2374 - return; - } - - window.addEventListener('load', () => { - const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; - - if (isLocalhost) { - // This is running on localhost. Let's check if a service worker still exists or not. - checkValidServiceWorker(swUrl, config); - - // Add some additional logging to localhost, pointing developers to the - // service worker/PWA documentation. - navigator.serviceWorker.ready.then(() => { - console.log( - 'This web app is being served cache-first by a service ' + - 'worker. To learn more, visit https://bit.ly/CRA-PWA' - ); - }); - } else { - // Is not localhost. Just register service worker - registerValidSW(swUrl, config); - } - }); - } -} - -export function unregister() { - if ('serviceWorker' in navigator) { - navigator.serviceWorker.ready.then((registration) => { - registration.unregister(); - }); - } -} diff --git a/kafka-ui-react-app/src/setupProxy.js b/kafka-ui-react-app/src/setupProxy.js deleted file mode 100644 index d6f12e7798a..00000000000 --- a/kafka-ui-react-app/src/setupProxy.js +++ /dev/null @@ -1,15 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-var-requires -const { createProxyMiddleware } = require('http-proxy-middleware'); - -module.exports = (app) => { - if (process.env.DEV_PROXY) { - app.use( - '/api', - createProxyMiddleware({ - target: process.env.DEV_PROXY, - changeOrigin: true, - secure: false, - }) - ); - } -}; diff --git a/kafka-ui-react-app/src/setupTests.ts b/kafka-ui-react-app/src/setupTests.ts index ddf707598a1..50c0339dab1 100644 --- a/kafka-ui-react-app/src/setupTests.ts +++ b/kafka-ui-react-app/src/setupTests.ts @@ -1,3 +1,4 @@ +import 'whatwg-fetch'; import 'jest-styled-components'; import '@testing-library/jest-dom/extend-expect'; import '@testing-library/jest-dom'; diff --git a/kafka-ui-react-app/src/theme/bulma_overrides.scss b/kafka-ui-react-app/src/theme/bulma_overrides.scss deleted file mode 100644 index f7e8a234e90..00000000000 --- a/kafka-ui-react-app/src/theme/bulma_overrides.scss +++ /dev/null @@ -1,95 +0,0 @@ -.has { - &-text-overflow-ellipsis { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - &-text-nowrap { - white-space: nowrap; - } - - &-content-overflow-ellipsis { - max-height: 73px; - overflow: hidden; - text-overflow: ellipsis; - background-color: #fff; - - background: -webkit-linear-gradient( - 90deg, - rgba(0, 0, 0, 0.1) 0%, - rgba(0, 0, 0, 1) 40% - ); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - } -} - -.breadcrumb li { - &.is-active > span { - padding: 0 0.75em; - } - - &:first-child > span { - padding-left: 0; - } -} - -.section { - animation: fadein 0.5s; -} - -.select.is-block select { - width: 100%; -} - -.notification { - &.is-light { - &.is-primary { - background-color: #ebfffc; - color: #00947e; - } - - &.is-danger { - background-color: #feecf0; - color: #cc0f35; - } - } -} - -.is-family-code { - font-family: 'Roboto Mono', sans-serif !important; - font-size: 0.9rem; - line-height: 1.5em; -} - -@keyframes fadein { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -.level.level-multiline { - flex-wrap: wrap; - .level-item.is-one-third { - flex-basis: 26%; - } - .level-item.is-one-third:nth-child(n + 4) { - margin-top: 10px; - } -} - -.is-size-8 { - font-size: $size-8; -} - -.tag:not(body) { - font-size: 0.75rem; - height: 1.75em; - line-height: 1.75; -} diff --git a/kafka-ui-react-app/src/theme/index.scss b/kafka-ui-react-app/src/theme/index.scss index 04db81e19bb..d57d3a4d12e 100644 --- a/kafka-ui-react-app/src/theme/index.scss +++ b/kafka-ui-react-app/src/theme/index.scss @@ -1,79 +1 @@ -@import '@fortawesome/fontawesome-free/css/all.min.css'; -@import 'src/theme/variables'; - -// utilities -@import "bulma/sass/utilities/initial-variables"; -@import "bulma/sass/utilities/functions"; -@import "bulma/sass/utilities/derived-variables"; -@import "bulma/sass/utilities/mixins"; -@import "bulma/sass/utilities/controls"; -@import "bulma/sass/utilities/extends"; - -// Base -@import "bulma/sass/base/minireset"; -@import "bulma/sass/base/generic"; -@import "bulma/sass/base/animations"; - -// Elements - -// Form -@import "bulma/sass/form/shared"; -@import "bulma/sass/form/input-textarea"; -@import "bulma/sass/form/checkbox-radio"; -@import "bulma/sass/form/select"; -@import "bulma/sass/form/file"; -@import "bulma/sass/form/tools"; - -// Components -@import "bulma/sass/components/breadcrumb"; -@import "bulma/sass/components/card"; -@import "bulma/sass/components/dropdown"; -@import "bulma/sass/components/level"; -@import "bulma/sass/components/media"; -@import "bulma/sass/components/menu"; -@import "bulma/sass/components/message"; -@import "bulma/sass/components/modal"; -@import "bulma/sass/components/navbar"; -@import "bulma/sass/components/pagination"; -@import "bulma/sass/components/panel"; -@import "bulma/sass/components/tabs"; - -// Grid -@import "bulma/sass/grid/columns"; -@import "bulma/sass/grid/tiles"; - -// Helpers -@import "bulma/sass/helpers/color"; -@import "bulma/sass/helpers/flexbox"; -@import "bulma/sass/helpers/float"; -@import "bulma/sass/helpers/other"; -@import "bulma/sass/helpers/overflow"; -@import "bulma/sass/helpers/position"; -@import "bulma/sass/helpers/spacing"; -@import "bulma/sass/helpers/typography"; -@import "bulma/sass/helpers/visibility"; - -@import 'src/theme/bulma_overrides'; - -@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;500&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap'); - -#root, -body, -html { - width: 100%; - position: relative; - margin: 0; - font-family: 'Inter', sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - background-color: #fff; -} - -input, select, textarea, button { - font-family: inherit; -} - -code { - font-family: 'Roboto Mono', sans-serif; -} +@import "./minireset.css"; diff --git a/kafka-ui-react-app/src/theme/minireset.css b/kafka-ui-react-app/src/theme/minireset.css new file mode 100644 index 00000000000..896003eb17c --- /dev/null +++ b/kafka-ui-react-app/src/theme/minireset.css @@ -0,0 +1 @@ +/*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */html,body,p,ol,ul,li,dl,dt,dd,blockquote,figure,fieldset,legend,textarea,pre,iframe,hr,h1,h2,h3,h4,h5,h6{margin:0;padding:0}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal}ul{list-style:none}button,input,select{margin:0}html{box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}img,video{height:auto;max-width:100%}iframe{border:0}table{border-collapse:collapse;border-spacing:0}td,th{padding:0} diff --git a/kafka-ui-react-app/src/theme/theme.ts b/kafka-ui-react-app/src/theme/theme.ts index fba48c6c85d..8426d3c2fcd 100644 --- a/kafka-ui-react-app/src/theme/theme.ts +++ b/kafka-ui-react-app/src/theme/theme.ts @@ -1,5 +1,4 @@ -/* eslint-disable import/prefer-default-export */ -export const Colors = { +const Colors = { neutral: { '0': '#FFFFFF', '3': '#f9fafa', @@ -14,8 +13,12 @@ export const Colors = { '50': '#73848C', '60': '#5C6970', '70': '#454F54', + '75': '#394246', '80': '#2F3639', + '85': '#22282A', + '87': '#1E2224', '90': '#171A1C', + '95': '#0B0D0E', '100': '#000', }, transparency: { @@ -28,12 +31,16 @@ export const Colors = { '15': '#C2F0D1', '30': '#85E0A3', '40': '#5CD685', + '50': '#33CC66', '60': '#29A352', }, brand: { '5': '#E8E8FC', '10': '#D1D1FA', + '15': '#B8BEF9', '20': '#A3A3F5', + '30': '#7E7EF1', + '40': '#6666FF', '50': '#4C4CFF', '60': '#1717CF', '70': '#1414B8', @@ -54,41 +61,16 @@ export const Colors = { '20': '#bbdefb', '30': '#90caf9', '40': '#64b5f6', + '45': '#5865F2', + '50': '#5B67E3', + '60': '#7A7AB8', + '70': '#5959A6', + '80': '#3E3E74', }, }; -const theme = { - layout: { - minWidth: '1200px', - navBarWidth: '201px', - navBarHeight: '3.25rem', - stuffColor: Colors.neutral[5], - stuffBorderColor: Colors.neutral[10], - overlay: { - backgroundColor: Colors.neutral[50], - }, - }, - panelColor: Colors.neutral[0], - breadcrumb: Colors.neutral[30], - connectEditWarning: Colors.yellow[10], - dropdown: { - color: Colors.red[50], - }, - ksqlDb: { - query: { - editor: { - readonly: { - background: Colors.neutral[3], - selection: { - backgroundColor: 'transparent', - }, - cursor: { - color: 'transparent', - }, - }, - }, - }, - }, +const baseTheme = { + defaultIconColor: Colors.neutral[50], heading: { h1: { color: Colors.neutral[90], @@ -97,6 +79,7 @@ const theme = { color: Colors.neutral[50], fontSize: '14px', }, + h4: Colors.neutral[90], base: { fontFamily: 'Inter, sans-serif', fontStyle: 'normal', @@ -105,7 +88,7 @@ const theme = { }, variants: { 1: { - fontSize: '24px', + fontSize: '20px', lineHeight: '32px', }, 2: { @@ -115,10 +98,13 @@ const theme = { 3: { fontSize: '16px', lineHeight: '24px', + fontWeight: 400, + marginBottom: '16px', }, 4: { fontSize: '14px', lineHeight: '20px', + fontWeight: 500, }, 5: { fontSize: '12px', @@ -130,10 +116,23 @@ const theme = { }, }, }, - lastestVersionItem: { - metaDataLabel: { - color: Colors.neutral[50], + code: { + backgroundColor: Colors.neutral[5], + color: Colors.red[55], + }, + layout: { + minWidth: '1200px', + navBarWidth: '201px', + navBarHeight: '51px', + rightSidebarWidth: '70vw', + filtersSidebarWidth: '300px', + + stuffColor: Colors.neutral[5], + stuffBorderColor: Colors.neutral[10], + overlay: { + backgroundColor: Colors.neutral[50], }, + socialLink: Colors.neutral[20], }, alert: { color: { @@ -141,6 +140,9 @@ const theme = { success: Colors.green[10], warning: Colors.yellow[10], info: Colors.neutral[10], + loading: Colors.neutral[10], + blank: Colors.neutral[10], + custom: Colors.neutral[10], }, shadow: Colors.transparency[20], }, @@ -152,15 +154,249 @@ const theme = { info: Colors.neutral[10], }, }, + connectEditWarning: Colors.yellow[10], + lastestVersionItem: { + metaDataLabel: { + color: Colors.neutral[50], + }, + }, + icons: { + chevronDownIcon: Colors.neutral[0], + editIcon: { + normal: Colors.neutral[30], + hover: Colors.neutral[90], + active: Colors.neutral[100], + border: Colors.neutral[10], + }, + closeIcon: { + normal: Colors.neutral[30], + hover: Colors.neutral[90], + active: Colors.neutral[100], + border: Colors.neutral[10], + }, + cancelIcon: Colors.neutral[30], + autoIcon: Colors.neutral[95], + fileIcon: Colors.neutral[90], + clockIcon: Colors.neutral[90], + arrowDownIcon: Colors.neutral[90], + moonIcon: Colors.neutral[95], + sunIcon: Colors.neutral[95], + infoIcon: Colors.neutral[30], + closeCircleIcon: Colors.neutral[30], + deleteIcon: Colors.red[20], + warningIcon: Colors.yellow[20], + warningRedIcon: { + rectFill: Colors.red[10], + pathFill: Colors.red[50], + }, + messageToggleIcon: { + normal: Colors.brand[30], + hover: Colors.brand[40], + active: Colors.brand[50], + }, + verticalElipsisIcon: Colors.neutral[50], + liveIcon: { + circleBig: Colors.red[10], + circleSmall: Colors.red[50], + }, + newFilterIcon: Colors.brand[50], + closeModalIcon: Colors.neutral[25], + savedIcon: Colors.brand[50], + dropdownArrowIcon: Colors.neutral[50], + git: { + hover: Colors.neutral[90], + active: Colors.neutral[70], + }, + discord: { + normal: Colors.neutral[20], + hover: Colors.blue[45], + active: Colors.brand[15], + }, + }, + textArea: { + borderColor: { + normal: Colors.neutral[30], + hover: Colors.neutral[50], + focus: Colors.neutral[70], + disabled: Colors.neutral[10], + }, + color: { + placeholder: { + normal: Colors.neutral[30], + focus: { + normal: 'transparent', + readOnly: Colors.neutral[30], + }, + }, + disabled: Colors.neutral[30], + readOnly: Colors.neutral[90], + }, + backgroundColor: { + readOnly: Colors.neutral[5], + }, + }, + tag: { + backgroundColor: { + green: Colors.green[10], + gray: Colors.neutral[5], + yellow: Colors.yellow[10], + white: Colors.neutral[10], + red: Colors.red[10], + blue: Colors.blue[10], + secondary: Colors.neutral[15], + }, + color: Colors.neutral[90], + }, + switch: { + unchecked: Colors.neutral[20], + hover: Colors.neutral[40], + checked: Colors.brand[50], + circle: Colors.neutral[0], + disabled: Colors.neutral[10], + checkedIcon: { + backgroundColor: Colors.neutral[10], + }, + }, + pageLoader: { + borderColor: Colors.brand[50], + borderBottomColor: Colors.neutral[0], + }, + topicFormLabel: { + color: Colors.neutral[50], + }, + dangerZone: { + borderColor: Colors.red[60], + color: { + title: Colors.red[50], + warningMessage: Colors.neutral[50], + }, + }, + configList: { + color: Colors.neutral[30], + }, + tooltip: { + bg: Colors.neutral[80], + text: Colors.neutral[0], + }, + topicsList: { + color: { + normal: Colors.neutral[90], + hover: Colors.neutral[50], + active: Colors.neutral[90], + }, + backgroundColor: { + hover: Colors.neutral[5], + active: Colors.neutral[10], + }, + }, + statictics: { + createdAtColor: Colors.neutral[50], + progressPctColor: Colors.neutral[100], + }, + progressBar: { + backgroundColor: Colors.neutral[3], + compleatedColor: Colors.green[40], + borderColor: Colors.neutral[10], + }, + clusterConfigForm: { + inputHintText: { + secondary: Colors.neutral[60], + }, + groupField: { + backgroundColor: Colors.neutral[3], + }, + fileInput: { + color: Colors.neutral[85], + }, + }, +}; + +export const theme = { + ...baseTheme, + version: { + currentVersion: { + color: Colors.neutral[30], + }, + commitLink: { + color: Colors.brand[50], + }, + }, + default: { + color: { + normal: Colors.neutral[90], + }, + backgroundColor: Colors.neutral[0], + transparentColor: 'transparent', + }, + link: { + color: Colors.brand[50], + hoverColor: Colors.brand[60], + }, + hr: { + backgroundColor: Colors.neutral[5], + }, + pageHeading: { + height: '64px', + dividerColor: Colors.neutral[30], + backLink: { + color: { + normal: Colors.brand[70], + hover: Colors.brand[60], + }, + }, + }, + panelColor: { + borderTop: 'none', + }, + dropdown: { + backgroundColor: Colors.neutral[0], + borderColor: Colors.neutral[5], + shadow: Colors.transparency[20], + item: { + color: { + normal: Colors.neutral[90], + danger: Colors.red[60], + }, + backgroundColor: { + default: Colors.neutral[0], + hover: Colors.neutral[5], + }, + }, + }, + ksqlDb: { + query: { + editor: { + readonly: { + background: Colors.neutral[3], + }, + activeLine: { + backgroundColor: Colors.neutral[5], + }, + cell: { + backgroundColor: Colors.neutral[10], + }, + layer: { + backgroundColor: Colors.neutral[5], + }, + cursor: Colors.neutral[90], + variable: Colors.red[50], + aceString: Colors.green[60], + codeMarker: Colors.yellow[20], + }, + }, + }, button: { primary: { backgroundColor: { - normal: Colors.brand[5], - hover: Colors.brand[10], - active: Colors.brand[20], + normal: Colors.brand[50], + hover: Colors.brand[60], + active: Colors.brand[70], disabled: Colors.neutral[5], }, - color: Colors.neutral[90], + color: { + normal: Colors.neutral[0], + disabled: Colors.neutral[30], + }, invertedColors: { normal: Colors.brand[50], hover: Colors.brand[60], @@ -169,15 +405,21 @@ const theme = { }, secondary: { backgroundColor: { - normal: Colors.neutral[5], - hover: Colors.neutral[10], - active: Colors.neutral[15], + normal: Colors.brand[5], + hover: Colors.brand[10], + active: Colors.brand[30], + disabled: Colors.neutral[5], }, - color: Colors.neutral[90], + color: { + normal: Colors.neutral[90], + disabled: Colors.neutral[30], + }, + isActiveColor: Colors.neutral[0], invertedColors: { normal: Colors.neutral[50], hover: Colors.neutral[70], active: Colors.neutral[90], + disabled: Colors.neutral[75], }, }, danger: { @@ -187,7 +429,10 @@ const theme = { active: Colors.red[60], disabled: Colors.red[20], }, - color: Colors.neutral[90], + color: { + normal: Colors.neutral[0], + disabled: Colors.neutral[0], + }, invertedColors: { normal: Colors.brand[50], hover: Colors.brand[60], @@ -210,6 +455,20 @@ const theme = { active: Colors.neutral[90], }, }, + chips: { + backgroundColor: { + normal: Colors.neutral[5], + hover: Colors.neutral[10], + active: Colors.neutral[50], + hoverActive: Colors.neutral[60], + }, + color: { + normal: Colors.neutral[70], + hover: Colors.neutral[70], + active: Colors.neutral[0], + hoverActive: Colors.neutral[0], + }, + }, menu: { backgroundColor: { normal: Colors.neutral[0], @@ -228,38 +487,42 @@ const theme = { initializing: Colors.yellow[20], }, chevronIconColor: Colors.neutral[50], - }, - version: { - currentVersion: { - color: Colors.neutral[30], - }, - symbolWrapper: { - color: Colors.neutral[30], - }, + titleColor: Colors.neutral[90], }, schema: { backgroundColor: { tr: Colors.neutral[5], div: Colors.neutral[0], + p: Colors.neutral[80], + textarea: Colors.neutral[3], }, }, modal: { + color: Colors.neutral[80], backgroundColor: Colors.neutral[0], border: { top: Colors.neutral[5], bottom: Colors.neutral[5], + contrast: Colors.neutral[30], }, overlay: Colors.transparency[10], shadow: Colors.transparency[20], - deletionTextColor: Colors.neutral[70], + contentColor: Colors.neutral[70], + }, + confirmModal: { + backgroundColor: Colors.neutral[0], }, table: { + actionBar: { + backgroundColor: Colors.neutral[0], + }, th: { backgroundColor: { normal: Colors.neutral[0], }, color: { - normal: Colors.neutral[50], + sortable: Colors.neutral[30], + normal: Colors.neutral[60], hover: Colors.brand[50], active: Colors.brand[50], }, @@ -268,32 +531,55 @@ const theme = { }, }, td: { + borderTop: Colors.neutral[5], color: { normal: Colors.neutral[90], }, }, tr: { backgroundColor: { + normal: Colors.neutral[0], hover: Colors.neutral[5], }, }, link: { color: { normal: Colors.neutral[90], + hover: Colors.neutral[50], + active: Colors.neutral[90], + }, + }, + colored: { + color: { + attention: Colors.red[50], + warning: Colors.yellow[20], + }, + }, + expander: { + normal: Colors.brand[30], + hover: Colors.brand[40], + active: Colors.brand[50], + disabled: Colors.neutral[10], + }, + pagination: { + button: { + background: Colors.neutral[90], + border: Colors.neutral[80], }, + info: Colors.neutral[90], }, }, primaryTab: { + height: '41px', color: { normal: Colors.neutral[50], hover: Colors.neutral[90], active: Colors.neutral[90], + disabled: Colors.neutral[20], }, borderColor: { - normal: 'transparent', - hover: 'transparent', active: Colors.brand[50], - nav: Colors.neutral[10], + nav: Colors.neutral[5], }, }, secondaryTab: { @@ -331,6 +617,7 @@ const theme = { backgroundColor: Colors.neutral[30], }, }, + label: Colors.neutral[50], }, input: { borderColor: { @@ -340,6 +627,7 @@ const theme = { disabled: Colors.neutral[10], }, color: { + normal: Colors.neutral[90], placeholder: { normal: Colors.neutral[30], readOnly: Colors.neutral[30], @@ -348,161 +636,630 @@ const theme = { readOnly: Colors.neutral[90], }, backgroundColor: { + normal: Colors.neutral[0], readOnly: Colors.neutral[5], + disabled: Colors.neutral[0], }, error: Colors.red[50], icon: { color: Colors.neutral[70], + hover: Colors.neutral[90], }, label: { color: Colors.neutral[70], }, }, - textArea: { - borderColor: { - normal: Colors.neutral[30], - hover: Colors.neutral[50], - focus: Colors.neutral[70], - disabled: Colors.neutral[10], + metrics: { + backgroundColor: Colors.neutral[5], + sectionTitle: Colors.neutral[90], + indicator: { + titleColor: Colors.neutral[50], + warningTextColor: Colors.red[50], + lightTextColor: Colors.neutral[30], }, - color: { - placeholder: { - normal: Colors.neutral[30], - focus: { - normal: 'transparent', - readOnly: Colors.neutral[30], + wrapper: Colors.neutral[0], + filters: { + color: { + icon: Colors.neutral[90], + normal: Colors.neutral[50], + }, + }, + }, + scrollbar: { + trackColor: { + normal: Colors.neutral[0], + active: Colors.neutral[5], + }, + thumbColor: { + normal: Colors.neutral[0], + active: Colors.neutral[50], + }, + }, + consumerTopicContent: { + td: { + backgroundColor: Colors.neutral[5], + }, + }, + topicMetaData: { + backgroundColor: Colors.neutral[5], + color: { + label: Colors.neutral[50], + value: Colors.neutral[80], + meta: Colors.neutral[30], + }, + liderReplica: { + color: Colors.green[60], + }, + outOfSync: { + color: Colors.red[50], + }, + }, + viewer: { + wrapper: { + backgroundColor: Colors.neutral[3], + color: Colors.neutral[80], + }, + }, + activeFilter: { + color: Colors.neutral[70], + backgroundColor: Colors.neutral[5], + }, + savedFilter: { + filterName: Colors.neutral[90], + color: Colors.neutral[30], + }, + editFilter: { + textColor: Colors.brand[50], + deleteIconColor: Colors.brand[50], + }, + acl: { + table: { + deleteIcon: Colors.neutral[50], + }, + create: { + radioButtons: { + green: { + normal: { + background: Colors.neutral[0], + text: Colors.neutral[50], + }, + active: { + background: Colors.green[50], + text: Colors.neutral[0], + }, + hover: { + background: Colors.green[10], + text: Colors.neutral[90], + }, + }, + gray: { + normal: { + background: Colors.neutral[0], + text: Colors.neutral[50], + }, + active: { + background: Colors.neutral[10], + text: Colors.neutral[90], + }, + hover: { + background: Colors.neutral[5], + text: Colors.neutral[90], + }, }, + red: {}, + }, + }, + }, +}; + +export type ThemeType = typeof theme; + +export const darkTheme: ThemeType = { + ...baseTheme, + version: { + currentVersion: { + color: Colors.neutral[50], + }, + commitLink: { + color: Colors.brand[30], + }, + }, + default: { + color: { + normal: Colors.neutral[0], + }, + backgroundColor: Colors.neutral[90], + transparentColor: 'transparent', + }, + link: { + color: Colors.brand[50], + hoverColor: Colors.brand[30], + }, + hr: { + backgroundColor: Colors.neutral[80], + }, + pageHeading: { + height: '64px', + dividerColor: Colors.neutral[50], + backLink: { + color: { + normal: Colors.brand[30], + hover: Colors.brand[15], + }, + }, + }, + panelColor: { + borderTop: Colors.neutral[80], + }, + dropdown: { + backgroundColor: Colors.neutral[85], + borderColor: Colors.neutral[80], + shadow: Colors.transparency[20], + item: { + color: { + normal: Colors.neutral[0], + danger: Colors.red[60], + }, + backgroundColor: { + default: Colors.neutral[85], + hover: Colors.neutral[80], }, - disabled: Colors.neutral[30], - readOnly: Colors.neutral[90], }, + }, + ksqlDb: { + query: { + editor: { + readonly: { + background: Colors.neutral[3], + }, + activeLine: { + backgroundColor: Colors.neutral[80], + }, + cell: { + backgroundColor: Colors.neutral[75], + }, + layer: { + backgroundColor: Colors.neutral[80], + }, + cursor: Colors.neutral[0], + variable: Colors.red[50], + aceString: Colors.green[60], + codeMarker: Colors.yellow[20], + }, + }, + }, + button: { + primary: { + backgroundColor: { + normal: Colors.brand[30], + hover: Colors.brand[20], + active: Colors.brand[10], + disabled: Colors.neutral[75], + }, + color: { + normal: Colors.neutral[0], + disabled: Colors.neutral[60], + }, + invertedColors: { + normal: Colors.brand[30], + hover: Colors.brand[60], + active: Colors.brand[60], + }, + }, + secondary: { + backgroundColor: { + normal: Colors.blue[80], + hover: Colors.blue[70], + active: Colors.blue[60], + disabled: Colors.neutral[75], + }, + color: { + normal: Colors.neutral[0], + disabled: Colors.neutral[60], + }, + isActiveColor: Colors.neutral[90], + invertedColors: { + normal: Colors.neutral[50], + hover: Colors.neutral[70], + active: Colors.neutral[90], + disabled: Colors.neutral[75], + }, + }, + danger: { + backgroundColor: { + normal: Colors.red[50], + hover: Colors.red[55], + active: Colors.red[60], + disabled: Colors.red[20], + }, + color: { + normal: Colors.neutral[0], + disabled: Colors.neutral[0], + }, + invertedColors: { + normal: Colors.brand[50], + hover: Colors.brand[60], + active: Colors.brand[60], + }, + }, + height: { + S: '24px', + M: '32px', + L: '40px', + }, + fontSize: { + S: '14px', + M: '14px', + L: '16px', + }, + border: { + normal: Colors.neutral[50], + hover: Colors.neutral[70], + active: Colors.neutral[90], + }, + }, + chips: { backgroundColor: { - readOnly: Colors.neutral[5], + normal: Colors.neutral[80], + hover: Colors.neutral[70], + active: Colors.neutral[50], + hoverActive: Colors.neutral[40], + }, + color: { + normal: Colors.neutral[0], + hover: Colors.neutral[0], + active: Colors.neutral[90], + hoverActive: Colors.neutral[90], }, }, - tag: { + menu: { backgroundColor: { - green: Colors.green[10], - gray: Colors.neutral[5], - yellow: Colors.yellow[10], - white: Colors.neutral[10], - red: Colors.red[10], - blue: Colors.blue[10], + normal: Colors.neutral[90], + hover: Colors.neutral[87], + active: Colors.neutral[85], }, - color: Colors.neutral[90], + color: { + normal: Colors.neutral[40], + hover: Colors.neutral[20], + active: Colors.brand[20], + isOpen: Colors.neutral[90], + }, + statusIconColor: { + online: Colors.green[40], + offline: Colors.red[50], + initializing: Colors.yellow[20], + }, + chevronIconColor: Colors.neutral[50], + titleColor: Colors.neutral[0], }, - pagination: { - backgroundColor: Colors.neutral[0], - currentPage: Colors.neutral[10], + schema: { + backgroundColor: { + tr: Colors.neutral[5], + div: Colors.neutral[0], + p: Colors.neutral[0], + textarea: Colors.neutral[85], + }, + }, + modal: { + color: Colors.neutral[0], + backgroundColor: Colors.neutral[85], + border: { + top: Colors.neutral[75], + bottom: Colors.neutral[75], + contrast: Colors.neutral[75], + }, + overlay: Colors.transparency[10], + shadow: Colors.transparency[20], + contentColor: Colors.neutral[30], + }, + confirmModal: { + backgroundColor: Colors.neutral[80], + }, + table: { + actionBar: { + backgroundColor: Colors.neutral[90], + }, + th: { + backgroundColor: { + normal: Colors.neutral[90], + }, + color: { + sortable: Colors.neutral[30], + normal: Colors.neutral[60], + hover: Colors.brand[50], + active: Colors.brand[50], + }, + previewColor: { + normal: Colors.brand[50], + }, + }, + td: { + borderTop: Colors.neutral[80], + color: { + normal: Colors.neutral[0], + }, + }, + tr: { + backgroundColor: { + normal: Colors.neutral[90], + hover: Colors.neutral[85], + }, + }, + link: { + color: { + normal: Colors.neutral[0], + hover: Colors.neutral[0], + active: Colors.neutral[0], + }, + }, + colored: { + color: { + attention: Colors.red[50], + warning: Colors.yellow[20], + }, + }, + expander: { + normal: Colors.brand[30], + hover: Colors.brand[40], + active: Colors.brand[50], + disabled: Colors.neutral[10], + }, + pagination: { + button: { + background: Colors.neutral[90], + border: Colors.neutral[80], + }, + info: Colors.neutral[0], + }, + }, + primaryTab: { + height: '41px', + color: { + normal: Colors.neutral[50], + hover: Colors.neutral[0], + active: Colors.brand[30], + disabled: Colors.neutral[75], + }, borderColor: { - normal: Colors.neutral[30], + active: Colors.brand[50], + nav: Colors.neutral[80], + }, + }, + secondaryTab: { + backgroundColor: { + normal: Colors.neutral[90], + hover: Colors.neutral[85], + active: Colors.neutral[80], + }, + color: { + normal: Colors.neutral[50], + hover: Colors.neutral[0], + active: Colors.neutral[0], + }, + }, + select: { + backgroundColor: { + normal: Colors.neutral[85], + hover: Colors.neutral[80], + active: Colors.neutral[70], + }, + color: { + normal: Colors.neutral[0], + hover: Colors.neutral[0], + active: Colors.neutral[0], + disabled: Colors.neutral[60], + }, + borderColor: { + normal: Colors.neutral[70], hover: Colors.neutral[50], active: Colors.neutral[70], - disabled: Colors.neutral[20], + disabled: Colors.neutral[70], + }, + optionList: { + scrollbar: { + backgroundColor: Colors.neutral[30], + }, + }, + label: Colors.neutral[50], + }, + input: { + borderColor: { + normal: Colors.neutral[70], + hover: Colors.neutral[50], + focus: Colors.neutral[0], + disabled: Colors.neutral[80], }, color: { + normal: Colors.neutral[0], + placeholder: { + normal: Colors.neutral[60], + readOnly: Colors.neutral[0], + }, + disabled: Colors.neutral[80], + readOnly: Colors.neutral[0], + }, + backgroundColor: { normal: Colors.neutral[90], - hover: Colors.neutral[90], - active: Colors.neutral[90], - disabled: Colors.neutral[20], + readOnly: Colors.neutral[80], + disabled: Colors.neutral[90], + }, + error: Colors.red[50], + icon: { + color: Colors.neutral[30], + hover: Colors.neutral[0], + }, + label: { + color: Colors.neutral[30], }, - }, - switch: { - unchecked: Colors.brand[20], - checked: Colors.brand[50], - circle: Colors.neutral[0], - disabled: Colors.neutral[10], - }, - pageLoader: { - borderColor: Colors.brand[50], - borderBottomColor: Colors.neutral[0], }, metrics: { - backgroundColor: Colors.neutral[5], + backgroundColor: Colors.neutral[95], + sectionTitle: Colors.neutral[0], indicator: { - backgroundColor: Colors.neutral[0], - titleColor: Colors.neutral[50], + titleColor: Colors.neutral[0], warningTextColor: Colors.red[50], - lightTextColor: Colors.neutral[30], + lightTextColor: Colors.neutral[60], }, + wrapper: Colors.neutral[0], filters: { color: { - icon: Colors.neutral[90], + icon: Colors.neutral[0], normal: Colors.neutral[50], }, }, }, scrollbar: { trackColor: { - normal: Colors.neutral[0], - active: Colors.neutral[5], + normal: Colors.neutral[90], + active: Colors.neutral[85], }, thumbColor: { - normal: Colors.neutral[0], + normal: Colors.neutral[75], active: Colors.neutral[50], }, }, consumerTopicContent: { - backgroundColor: Colors.neutral[5], - }, - topicFormLabel: { - color: Colors.neutral[50], + td: { + backgroundColor: Colors.neutral[95], + }, }, topicMetaData: { - backgroundColor: Colors.neutral[5], + backgroundColor: Colors.neutral[90], color: { label: Colors.neutral[50], - value: Colors.neutral[80], - meta: Colors.neutral[30], + value: Colors.neutral[0], + meta: Colors.neutral[60], + }, + liderReplica: { + color: Colors.green[60], + }, + outOfSync: { + color: Colors.red[50], }, }, - dangerZone: { - borderColor: Colors.neutral[10], - color: Colors.red[50], + viewer: { + wrapper: { + backgroundColor: Colors.neutral[85], + color: Colors.neutral[0], + }, }, - configList: { - color: Colors.neutral[30], + activeFilter: { + color: Colors.neutral[0], + backgroundColor: Colors.neutral[80], }, - topicsList: { - color: { - normal: Colors.neutral[90], - hover: Colors.neutral[50], - active: Colors.neutral[90], - }, - backgroundColor: { - hover: Colors.neutral[5], - active: Colors.neutral[10], + savedFilter: { + filterName: Colors.neutral[0], + color: Colors.neutral[70], + }, + editFilter: { + textColor: Colors.brand[30], + deleteIconColor: Colors.brand[30], + }, + heading: { + ...baseTheme.heading, + h4: Colors.neutral[0], + base: { + ...baseTheme.heading.base, + color: Colors.neutral[0], }, }, + code: { + ...baseTheme.code, + backgroundColor: Colors.neutral[95], + }, + layout: { + ...baseTheme.layout, + stuffColor: Colors.neutral[75], + stuffBorderColor: Colors.neutral[75], + socialLink: Colors.neutral[30], + }, icons: { - closeIcon: Colors.neutral[30], - warningIcon: Colors.yellow[20], - messageToggleIcon: { - normal: Colors.brand[50], - hover: Colors.brand[20], - active: Colors.brand[60], + ...baseTheme.icons, + editIcon: { + normal: Colors.neutral[50], + hover: Colors.neutral[30], + active: Colors.neutral[40], + border: Colors.neutral[70], }, - verticalElipsisIcon: Colors.neutral[50], - liveIcon: { - circleBig: Colors.red[10], - circleSmall: Colors.red[50], + closeIcon: { + normal: Colors.neutral[50], + hover: Colors.neutral[30], + active: Colors.neutral[40], + border: Colors.neutral[70], + }, + cancelIcon: Colors.neutral[0], + autoIcon: Colors.neutral[0], + fileIcon: Colors.neutral[0], + clockIcon: Colors.neutral[0], + arrowDownIcon: Colors.neutral[0], + moonIcon: Colors.neutral[0], + sunIcon: Colors.neutral[0], + infoIcon: Colors.neutral[70], + savedIcon: Colors.brand[30], + git: { + ...baseTheme.icons.git, + hover: Colors.neutral[70], + active: Colors.neutral[90], + }, + discord: { + ...baseTheme.icons.discord, + normal: Colors.neutral[30], }, - newFilterIcon: Colors.brand[50], - closeModalIcon: Colors.neutral[25], - savedIcon: Colors.brand[50], - dropdownArrowIcon: Colors.neutral[30], }, - viewer: { - wrapper: Colors.neutral[3], + textArea: { + ...baseTheme.textArea, + borderColor: { + ...baseTheme.textArea.borderColor, + normal: Colors.neutral[70], + hover: Colors.neutral[30], + focus: Colors.neutral[0], + }, }, - savedFilterDivider: { - color: Colors.neutral[15], + clusterConfigForm: { + ...baseTheme.clusterConfigForm, + groupField: { + backgroundColor: Colors.neutral[85], + }, + fileInput: { + color: Colors.neutral[0], + }, }, - editFilterText: { - color: Colors.brand[50], + acl: { + table: { + deleteIcon: Colors.neutral[50], + }, + create: { + radioButtons: { + green: { + normal: { + background: Colors.neutral[0], + text: Colors.neutral[50], + }, + active: { + background: Colors.green[50], + text: Colors.neutral[0], + }, + hover: { + background: Colors.green[10], + text: Colors.neutral[0], + }, + }, + gray: { + normal: { + background: Colors.neutral[0], + text: Colors.neutral[50], + }, + active: { + background: Colors.neutral[10], + text: Colors.neutral[90], + }, + hover: { + background: Colors.neutral[5], + text: Colors.neutral[90], + }, + }, + red: {}, + }, + }, }, }; - -export type ThemeType = typeof theme; - -export default theme; diff --git a/kafka-ui-react-app/src/theme/variables.scss b/kafka-ui-react-app/src/theme/variables.scss deleted file mode 100644 index d3ce861d2a2..00000000000 --- a/kafka-ui-react-app/src/theme/variables.scss +++ /dev/null @@ -1,12 +0,0 @@ -// Typography -$size-7: 0.875rem; -$size-8: 0.75rem; -$body-line-height: 1.34; -$body-color: hsl(0, 100%, 0%); - -// Section -$section-padding-desktop: 0 0 0.5rem; -$section-padding: 1rem 1.5rem 3rem; - -// Tag -$tag-radius: 16px; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/ClusterConfigForm.styled.ts b/kafka-ui-react-app/src/widgets/ClusterConfigForm/ClusterConfigForm.styled.ts new file mode 100644 index 00000000000..5a3452a9346 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/ClusterConfigForm.styled.ts @@ -0,0 +1,62 @@ +import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper'; +import styled from 'styled-components'; + +export const GroupFieldWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + background-color: ${({ theme }) => + theme.clusterConfigForm.groupField.backgroundColor}; + padding: 8px; + border-radius: 4px; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 15%); + + hr { + margin: 10px 0 5px; + } +`; +const InputContainer = styled.div` + display: grid; + grid-template-columns: 1fr 1fr 30px; + gap: 8px; + align-items: stretch; + max-width: 500px; +`; +export const ButtonWrapper = styled.div` + display: flex; + gap: 10px; +`; +export const RemoveButton = styled(IconButtonWrapper)` + align-self: center; +`; +export const FlexRow = styled.div` + display: flex; + flex-direction: row; + gap: 8px; + align-items: flex-start; +`; +export const FlexGrow1 = styled.div` + flex-grow: 1; + row-gap: 8px; + flex-direction: column; + display: flex; +`; +// KafkaCluster +export const BootstrapServer = styled(InputContainer)` + grid-template-columns: 3fr 110px 30px; +`; +export const BootstrapServerActions = styled(IconButtonWrapper)` + align-self: stretch; + margin-top: 12px; + margin-left: 8px; +`; +export const Port = styled.div` + width: 110px; +`; + +export const FileUploadInputWrapper = styled.div` + display: flex; + height: 40px; + align-items: center; + color: ${({ theme }) => theme.clusterConfigForm.fileInput.color}}; +`; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/Authentication/Authentication.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/Authentication/Authentication.tsx new file mode 100644 index 00000000000..0fad2cbf11c --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/Authentication/Authentication.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { useFormContext } from 'react-hook-form'; +import { AUTH_OPTIONS, SECURITY_PROTOCOL_OPTIONS } from 'lib/constants'; +import ControlledSelect from 'components/common/Select/ControlledSelect'; +import SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader'; + +import AuthenticationMethods from './AuthenticationMethods'; + +const Authentication: React.FC = () => { + const { watch, setValue } = useFormContext(); + const hasAuth = !!watch('auth'); + const authMethod = watch('auth.method'); + const hasSecurityProtocolField = + authMethod && !['Delegation tokens', 'mTLS'].includes(authMethod); + + const toggle = () => + setValue('auth', hasAuth ? undefined : {}, { + shouldValidate: true, + shouldDirty: true, + shouldTouch: true, + }); + + return ( + <> + + {hasAuth && ( + <> + + {hasSecurityProtocolField && ( + + )} + + + )} + + ); +}; + +export default Authentication; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/Authentication/AuthenticationMethods.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/Authentication/AuthenticationMethods.tsx new file mode 100644 index 00000000000..aaf2a7eae8b --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/Authentication/AuthenticationMethods.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import Input from 'components/common/Input/Input'; +import Checkbox from 'components/common/Checkbox/Checkbox'; +import Fileupload from 'widgets/ClusterConfigForm/common/Fileupload'; +import SSLForm from 'widgets/ClusterConfigForm/common/SSLForm'; +import Credentials from 'widgets/ClusterConfigForm/common/Credentials'; + +const AuthenticationMethods: React.FC<{ method: string }> = ({ method }) => { + switch (method) { + case 'SASL/JAAS': + return ( + <> + + + + ); + case 'SASL/GSSAPI': + return ( + <> + + + + + + ); + case 'SASL/OAUTHBEARER': + return ( + + ); + case 'SASL/PLAIN': + case 'SASL/SCRAM-256': + case 'SASL/SCRAM-512': + case 'SASL/LDAP': + return ; + case 'Delegation tokens': + return ( + <> + + + + ); + case 'SASL/AWS IAM': + return ( + + ); + case 'mTLS': + return ; + default: + return null; + } +}; + +export default AuthenticationMethods; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/CustomAuthentication.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/CustomAuthentication.tsx new file mode 100644 index 00000000000..c751b9c56d9 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/CustomAuthentication.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { useFormContext } from 'react-hook-form'; +import Input from 'components/common/Input/Input'; +import { convertFormKeyToPropsKey } from 'widgets/ClusterConfigForm/utils/convertFormKeyToPropsKey'; +import SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader'; + +const CustomAuthentication: React.FC = () => { + const { watch, setValue } = useFormContext(); + const customConf = watch('customAuth'); + const hasCustomConfig = + customConf && Object.values(customConf).some((v) => !!v); + + const remove = () => + setValue('customAuth', undefined, { + shouldValidate: true, + shouldDirty: true, + shouldTouch: true, + }); + return ( + <> + + {hasCustomConfig && ( + <> + {Object.keys(customConf).map((key) => ( + + ))} + + )} + + ); +}; + +export default CustomAuthentication; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KSQL.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KSQL.tsx new file mode 100644 index 00000000000..8d28ad65c61 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KSQL.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import Input from 'components/common/Input/Input'; +import { useFormContext } from 'react-hook-form'; +import SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader'; +import SSLForm from 'widgets/ClusterConfigForm/common/SSLForm'; +import Credentials from 'widgets/ClusterConfigForm/common/Credentials'; + +const KSQL = () => { + const { setValue, watch } = useFormContext(); + const ksql = watch('ksql'); + const toggleConfig = () => { + setValue('ksql', ksql ? undefined : { url: '', isAuth: false }, { + shouldValidate: true, + shouldDirty: true, + shouldTouch: true, + }); + }; + return ( + <> + + {ksql && ( + <> + + + + + )} + + ); +}; +export default KSQL; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KafkaCluster.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KafkaCluster.tsx new file mode 100644 index 00000000000..8a1be62228c --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KafkaCluster.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import Input from 'components/common/Input/Input'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { FormError, InputHint } from 'components/common/Input/Input.styled'; +import { ErrorMessage } from '@hookform/error-message'; +import CloseCircleIcon from 'components/common/Icons/CloseCircleIcon'; +import { Button } from 'components/common/Button/Button'; +import PlusIcon from 'components/common/Icons/PlusIcon'; +import * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled'; +import Heading from 'components/common/heading/Heading.styled'; +import { InputLabel } from 'components/common/Input/InputLabel.styled'; +import Checkbox from 'components/common/Checkbox/Checkbox'; +import SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader'; +import SSLForm from 'widgets/ClusterConfigForm/common/SSLForm'; + +const KafkaCluster: React.FC = () => { + const { control, watch, setValue } = useFormContext(); + + const { fields, append, remove } = useFieldArray({ + control, + name: 'bootstrapServers', + }); + + const hasTrustStore = !!watch('truststore'); + + const toggleSection = (section: string) => () => + setValue( + section, + watch(section) + ? undefined + : { + location: '', + password: '', + }, + { shouldValidate: true, shouldDirty: true, shouldTouch: true } + ); + + return ( + <> + Kafka Cluster + + +
    + Bootstrap Servers * + + the list of Kafka brokers that you want to connect to + + + {fields.map((field, index) => ( + +
    + +
    +
    + +
    + remove(index)} + > + + +
    + ))} + + + +
    + +
    +
    +
    +
    + + {hasTrustStore && } + + ); +}; +export default KafkaCluster; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KafkaConnect.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KafkaConnect.tsx new file mode 100644 index 00000000000..e51c9d5ffcd --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KafkaConnect.tsx @@ -0,0 +1,91 @@ +import * as React from 'react'; +import * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled'; +import { Button } from 'components/common/Button/Button'; +import Input from 'components/common/Input/Input'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import PlusIcon from 'components/common/Icons/PlusIcon'; +import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper'; +import CloseCircleIcon from 'components/common/Icons/CloseCircleIcon'; +import { + FlexGrow1, + FlexRow, +} from 'widgets/ClusterConfigForm/ClusterConfigForm.styled'; +import SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader'; +import Credentials from 'widgets/ClusterConfigForm/common/Credentials'; +import SSLForm from 'widgets/ClusterConfigForm/common/SSLForm'; + +const KafkaConnect = () => { + const { control } = useFormContext(); + const { fields, append, remove } = useFieldArray({ + control, + name: 'kafkaConnect', + }); + const handleAppend = () => append({ name: '', address: '' }); + const toggleConfig = () => (fields.length === 0 ? handleAppend() : remove()); + + const hasFields = fields.length > 0; + + return ( + <> + + {hasFields && ( + + {fields.map((item, index) => ( +
    + + + + + + + + remove(index)}> + + + + + + +
    +
    + ))} + +
    + )} + + ); +}; +export default KafkaConnect; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/Metrics.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/Metrics.tsx new file mode 100644 index 00000000000..e9d67f2014f --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/Metrics.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import Input from 'components/common/Input/Input'; +import { useFormContext } from 'react-hook-form'; +import ControlledSelect from 'components/common/Select/ControlledSelect'; +import { METRICS_OPTIONS } from 'lib/constants'; +import * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled'; +import SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader'; +import SSLForm from 'widgets/ClusterConfigForm/common/SSLForm'; +import Credentials from 'widgets/ClusterConfigForm/common/Credentials'; + +const Metrics = () => { + const { setValue, watch } = useFormContext(); + const visibleMetrics = !!watch('metrics'); + const toggleMetrics = () => + setValue( + 'metrics', + visibleMetrics + ? undefined + : { + type: '', + port: 0, + isAuth: false, + }, + { shouldValidate: true, shouldDirty: true, shouldTouch: true } + ); + + return ( + <> + + {visibleMetrics && ( + <> + + + + + + + + )} + + ); +}; +export default Metrics; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/SchemaRegistry.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/SchemaRegistry.tsx new file mode 100644 index 00000000000..bd36ee89348 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/SchemaRegistry.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import Input from 'components/common/Input/Input'; +import { useFormContext } from 'react-hook-form'; +import SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader'; +import SSLForm from 'widgets/ClusterConfigForm/common/SSLForm'; +import Credentials from 'widgets/ClusterConfigForm/common/Credentials'; + +const SchemaRegistry = () => { + const { setValue, watch } = useFormContext(); + const schemaRegistry = watch('schemaRegistry'); + const toggleConfig = () => { + setValue( + 'schemaRegistry', + schemaRegistry ? undefined : { url: '', isAuth: false }, + { shouldValidate: true, shouldDirty: true, shouldTouch: true } + ); + }; + return ( + <> + + {schemaRegistry && ( + <> + + + + + )} + + ); +}; +export default SchemaRegistry; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/Credentials.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/Credentials.tsx new file mode 100644 index 00000000000..f991b11096f --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/Credentials.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import Input from 'components/common/Input/Input'; +import * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled'; +import Checkbox from 'components/common/Checkbox/Checkbox'; +import { useFormContext } from 'react-hook-form'; + +type CredentialsProps = { + prefix: string; + title?: string; +}; + +const Credentials: React.FC = ({ + prefix, + title = 'Secured with auth?', +}) => { + const { watch } = useFormContext(); + + return ( + + + {watch(`${prefix}.isAuth`) && ( + + + + + + + + + )} + + ); +}; + +export default Credentials; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/Fileupload.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/Fileupload.tsx new file mode 100644 index 00000000000..4a6244ada00 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/Fileupload.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; +import { FormError } from 'components/common/Input/Input.styled'; +import { InputLabel } from 'components/common/Input/InputLabel.styled'; +import { ErrorMessage } from '@hookform/error-message'; +import { useFormContext } from 'react-hook-form'; +import Input from 'components/common/Input/Input'; +import { Button } from 'components/common/Button/Button'; +import * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled'; +import { useAppConfigFilesUpload } from 'lib/hooks/api/appConfig'; + +const Fileupload: React.FC<{ name: string; label: string }> = ({ + name, + label, +}) => { + const upload = useAppConfigFilesUpload(); + + const id = React.useId(); + const { watch, setValue } = useFormContext(); + const loc = watch(name); + + const handleFileChange = async (e: React.ChangeEvent) => { + if (e.target.files) { + const formData = new FormData(); + const file = e.target.files[0]; + formData.append('file', file); + const resp = await upload.mutateAsync(formData); + setValue(name, resp.location, { + shouldValidate: true, + shouldDirty: true, + }); + } + }; + + const onReset = () => { + setValue(name, '', { shouldValidate: true, shouldDirty: true }); + }; + + return ( +
    + {label} + + {loc ? ( + + + + + + + ) : ( + + {upload.isLoading ? ( +

    Uploading...

    + ) : ( + + )} +
    + )} + + + +
    + ); +}; + +export default Fileupload; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/SSLForm.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/SSLForm.tsx new file mode 100644 index 00000000000..98877a3d29e --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/SSLForm.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import Input from 'components/common/Input/Input'; +import Fileupload from 'widgets/ClusterConfigForm/common/Fileupload'; +import * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled'; + +type SSLFormProps = { + prefix: string; + title: string; +}; + +const SSLForm: React.FC = ({ prefix, title }) => { + return ( + + + + + ); +}; + +export default SSLForm; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/SectionHeader.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/SectionHeader.tsx new file mode 100644 index 00000000000..1265459b5c1 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/common/SectionHeader.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { Button } from 'components/common/Button/Button'; +import Heading from 'components/common/heading/Heading.styled'; +import * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled'; + +interface SectionHeaderProps { + title: string; + addButtonText: string; + adding?: boolean; + onClick: () => void; +} + +const SectionHeader: React.FC = ({ + adding, + title, + addButtonText, + onClick, +}) => { + return ( + + + {title} + + + + ); +}; + +export default SectionHeader; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/index.tsx b/kafka-ui-react-app/src/widgets/ClusterConfigForm/index.tsx new file mode 100644 index 00000000000..d4007cf4883 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/index.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import { Button } from 'components/common/Button/Button'; +import { useForm, FormProvider } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import formSchema from 'widgets/ClusterConfigForm/schema'; +import { FlexFieldset, StyledForm } from 'components/common/Form/Form.styled'; +import { + useUpdateAppConfig, + useValidateAppConfig, +} from 'lib/hooks/api/appConfig'; +import { ClusterConfigFormValues } from 'widgets/ClusterConfigForm/types'; +import { transformFormDataToPayload } from 'widgets/ClusterConfigForm/utils/transformFormDataToPayload'; +import { showAlert, showSuccessAlert } from 'lib/errorHandling'; +import { getIsValidConfig } from 'widgets/ClusterConfigForm/utils/getIsValidConfig'; +import * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled'; +import { useNavigate } from 'react-router-dom'; +import useBoolean from 'lib/hooks/useBoolean'; +import KafkaCluster from 'widgets/ClusterConfigForm/Sections/KafkaCluster'; +import SchemaRegistry from 'widgets/ClusterConfigForm/Sections/SchemaRegistry'; +import KafkaConnect from 'widgets/ClusterConfigForm/Sections/KafkaConnect'; +import Metrics from 'widgets/ClusterConfigForm/Sections/Metrics'; +import CustomAuthentication from 'widgets/ClusterConfigForm/Sections/CustomAuthentication'; +import Authentication from 'widgets/ClusterConfigForm/Sections/Authentication/Authentication'; +import KSQL from 'widgets/ClusterConfigForm/Sections/KSQL'; + +interface ClusterConfigFormProps { + hasCustomConfig?: boolean; + initialValues?: Partial; +} + +const CLUSTER_CONFIG_FORM_DEFAULT_VALUES: Partial = { + bootstrapServers: [{ host: '', port: '' }], +}; + +const ClusterConfigForm: React.FC = ({ + initialValues = {}, + hasCustomConfig, +}) => { + const navigate = useNavigate(); + const methods = useForm({ + mode: 'all', + resolver: yupResolver(formSchema), + defaultValues: { + ...CLUSTER_CONFIG_FORM_DEFAULT_VALUES, + ...initialValues, + }, + }); + const { + formState: { isSubmitting, isDirty }, + trigger, + } = methods; + + const validate = useValidateAppConfig(); + const update = useUpdateAppConfig({ initialName: initialValues.name }); + const { + value: isFormDisabled, + setTrue: disableForm, + setFalse: enableForm, + } = useBoolean(); + + const onSubmit = async (data: ClusterConfigFormValues) => { + const config = transformFormDataToPayload(data); + try { + await update.mutateAsync(config); + navigate('/'); + } catch (e) { + showAlert('error', { + id: 'app-config-update-error', + title: 'Error updating application config', + message: 'There was an error updating the application config', + }); + } + }; + + const onReset = () => methods.reset(); + + const onValidate = async () => { + await trigger(undefined, { shouldFocus: true }); + if (!methods.formState.isValid) return; + disableForm(); + const data = methods.getValues(); + const config = transformFormDataToPayload(data); + + try { + const response = await validate.mutateAsync(config); + const isConfigValid = getIsValidConfig(response, data.name); + if (isConfigValid) { + showSuccessAlert({ + message: 'Configuration is valid', + }); + } + } catch (e) { + showAlert('error', { + id: 'app-config-validate-error', + title: 'Error validating application config', + message: 'There was an error validating the application config', + }); + } + enableForm(); + }; + + const showCustomConfig = methods.watch('customAuth') && hasCustomConfig; + + const isValidateDisabled = isSubmitting; + const isSubmitDisabled = isSubmitting || !isDirty; + + return ( + + + + +
    + {showCustomConfig ? : } +
    + +
    + +
    + +
    + +
    + + + + + +
    +
    +
    + ); +}; + +export default ClusterConfigForm; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/schema.ts b/kafka-ui-react-app/src/widgets/ClusterConfigForm/schema.ts new file mode 100644 index 00000000000..abd17f34bf3 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/schema.ts @@ -0,0 +1,194 @@ +import { isArray } from 'lodash'; +import { object, string, number, array, boolean, mixed, lazy } from 'yup'; + +const requiredString = string().required('required field'); + +const portSchema = number() + .positive('positive only') + .typeError('numbers only') + .required('required'); + +const bootstrapServerSchema = object({ + host: requiredString, + port: portSchema, +}); + +const sslSchema = lazy((value) => { + if (typeof value === 'object') { + return object({ + location: string().when('password', { + is: (v: string) => !!v, + then: (schema) => schema.required('required field'), + }), + password: string(), + }); + } + return mixed().optional(); +}); + +const urlWithAuthSchema = lazy((value) => { + if (typeof value === 'object') { + return object({ + url: requiredString, + isAuth: boolean(), + username: string().when('isAuth', { + is: true, + then: (schema) => schema.required('required field'), + }), + password: string().when('isAuth', { + is: true, + then: (schema) => schema.required('required field'), + }), + keystore: sslSchema, + }); + } + return mixed().optional(); +}); + +const kafkaConnectSchema = object({ + name: requiredString, + address: requiredString, + isAuth: boolean(), + username: string().when('isAuth', { + is: true, + then: (schema) => schema.required('required field'), + }), + password: string().when('isAuth', { + is: true, + then: (schema) => schema.required('required field'), + }), + keystore: sslSchema, +}); + +const kafkaConnectsSchema = lazy((value) => { + if (isArray(value)) { + return array().of(kafkaConnectSchema); + } + return mixed().optional(); +}); + +const metricsSchema = lazy((value) => { + if (typeof value === 'object') { + return object({ + type: string().oneOf(['JMX', 'PROMETHEUS']).required('required field'), + port: portSchema, + isAuth: boolean(), + username: string().when('isAuth', { + is: true, + then: (schema) => schema.required('required field'), + }), + password: string().when('isAuth', { + is: true, + then: (schema) => schema.required('required field'), + }), + keystore: sslSchema, + }); + } + return mixed().optional(); +}); + +const authPropsSchema = lazy((_, { parent }) => { + switch (parent.method) { + case 'SASL/JAAS': + return object({ + saslJaasConfig: requiredString, + saslMechanism: requiredString, + }); + case 'SASL/GSSAPI': + return object({ + saslKerberosServiceName: requiredString, + keyTabFile: string(), + storeKey: boolean(), + principal: requiredString, + }); + case 'SASL/OAUTHBEARER': + return object({ + unsecuredLoginStringClaim_sub: requiredString, + }); + case 'SASL/PLAIN': + case 'SASL/SCRAM-256': + case 'SASL/SCRAM-512': + case 'SASL/LDAP': + return object({ + username: requiredString, + password: requiredString, + }); + case 'Delegation tokens': + return object({ + tokenId: requiredString, + tokenValue: requiredString, + }); + case 'SASL/AWS IAM': + return object({ + awsProfileName: string(), + }); + case 'mTLS': + default: + return mixed().optional(); + } +}); + +const authSchema = lazy((value) => { + if (typeof value === 'object') { + return object({ + method: string() + .required('required field') + .oneOf([ + 'SASL/JAAS', + 'SASL/GSSAPI', + 'SASL/OAUTHBEARER', + 'SASL/PLAIN', + 'SASL/SCRAM-256', + 'SASL/SCRAM-512', + 'Delegation tokens', + 'SASL/LDAP', + 'SASL/AWS IAM', + 'mTLS', + ]), + securityProtocol: string() + .oneOf(['SASL_SSL', 'SASL_PLAINTEXT']) + .when('method', { + is: (v: string) => { + return [ + 'SASL/JAAS', + 'SASL/GSSAPI', + 'SASL/OAUTHBEARER', + 'SASL/PLAIN', + 'SASL/SCRAM-256', + 'SASL/SCRAM-512', + 'SASL/LDAP', + 'SASL/AWS IAM', + ].includes(v); + }, + then: (schema) => schema.required('required field'), + }), + keystore: lazy((_, { parent }) => { + if (parent.method === 'mTLS') { + return object({ + location: requiredString, + password: string(), + }); + } + return mixed().optional(); + }), + props: authPropsSchema, + }); + } + return mixed().optional(); +}); + +const formSchema = object({ + name: string() + .required('required field') + .min(3, 'Cluster name must be at least 3 characters'), + readOnly: boolean().required('required field'), + bootstrapServers: array().of(bootstrapServerSchema).min(1), + truststore: sslSchema, + auth: authSchema, + schemaRegistry: urlWithAuthSchema, + ksql: urlWithAuthSchema, + kafkaConnect: kafkaConnectsSchema, + metrics: metricsSchema, +}); + +export default formSchema; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/types.ts b/kafka-ui-react-app/src/widgets/ClusterConfigForm/types.ts new file mode 100644 index 00000000000..6af74b5f848 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/types.ts @@ -0,0 +1,56 @@ +type SecurityProtocol = 'SASL_SSL' | 'SASL_PLAINTEXT'; +type BootstrapServer = { + host: string; + port: string; +}; + +type WithKeystore = { + keystore?: { + location: string; + password: string; + }; +}; + +type WithAuth = { + isAuth: boolean; + username?: string; + password?: string; +}; + +type URLWithAuth = WithAuth & + WithKeystore & { + url?: string; + }; + +type KafkaConnect = WithAuth & + WithKeystore & { + name: string; + address: string; + }; + +type Metrics = WithAuth & + WithKeystore & { + type: string; + port: string; + }; + +export type ClusterConfigFormValues = { + name: string; + readOnly: boolean; + bootstrapServers: BootstrapServer[]; + truststore?: { + location: string; + password: string; + }; + auth?: WithKeystore & { + method: string; + securityProtocol: SecurityProtocol; + props: Record; + }; + schemaRegistry?: URLWithAuth; + ksql?: URLWithAuth; + properties?: Record; + kafkaConnect?: KafkaConnect[]; + metrics?: Metrics; + customAuth: Record; +}; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/convertFormKeyToPropsKey.ts b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/convertFormKeyToPropsKey.ts new file mode 100644 index 00000000000..1759b3b5f6b --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/convertFormKeyToPropsKey.ts @@ -0,0 +1,3 @@ +export const convertFormKeyToPropsKey = (key: string) => { + return key.split('___').join('.'); +}; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/convertPropsKeyToFormKey.ts b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/convertPropsKeyToFormKey.ts new file mode 100644 index 00000000000..37312be7456 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/convertPropsKeyToFormKey.ts @@ -0,0 +1,3 @@ +export const convertPropsKeyToFormKey = (key: string) => { + return key.split('.').join('___'); +}; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getInitialFormData.ts b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getInitialFormData.ts new file mode 100644 index 00000000000..73a945818df --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getInitialFormData.ts @@ -0,0 +1,121 @@ +import { + ApplicationConfigPropertiesKafkaClusters, + ApplicationConfigPropertiesKafkaSchemaRegistrySsl, +} from 'generated-sources'; +import { ClusterConfigFormValues } from 'widgets/ClusterConfigForm/types'; + +import { convertPropsKeyToFormKey } from './convertPropsKeyToFormKey'; + +const parseBootstrapServers = (bootstrapServers?: string) => + bootstrapServers?.split(',').map((url) => { + const [host, port] = url.split(':'); + return { host, port }; + }); + +const parseKeystore = ( + keystore?: ApplicationConfigPropertiesKafkaSchemaRegistrySsl +) => { + if (!keystore) return undefined; + const { keystoreLocation, keystorePassword } = keystore; + return { + keystore: { + location: keystoreLocation as string, + password: keystorePassword as string, + }, + }; +}; + +const parseCredentials = (username?: string, password?: string) => { + if (!username || !password) return { isAuth: false }; + return { isAuth: true, username, password }; +}; + +export const getInitialFormData = ( + payload: ApplicationConfigPropertiesKafkaClusters +) => { + const { + ssl, + schemaRegistry, + schemaRegistryAuth, + schemaRegistrySsl, + kafkaConnect, + metrics, + ksqldbServer, + ksqldbServerAuth, + ksqldbServerSsl, + } = payload; + + const initialValues: Partial = { + name: payload.name as string, + readOnly: !!payload.readOnly, + bootstrapServers: parseBootstrapServers(payload.bootstrapServers), + }; + + const { truststoreLocation, truststorePassword } = ssl || {}; + + if (truststoreLocation && truststorePassword) { + initialValues.truststore = { + location: truststoreLocation, + password: truststorePassword, + }; + } + + if (schemaRegistry) { + initialValues.schemaRegistry = { + url: schemaRegistry, + ...parseCredentials( + schemaRegistryAuth?.username, + schemaRegistryAuth?.password + ), + ...parseKeystore(schemaRegistrySsl), + }; + } + if (ksqldbServer) { + initialValues.ksql = { + url: ksqldbServer, + ...parseCredentials( + ksqldbServerAuth?.username, + ksqldbServerAuth?.password + ), + ...parseKeystore(ksqldbServerSsl), + }; + } + + if (kafkaConnect && kafkaConnect.length > 0) { + initialValues.kafkaConnect = kafkaConnect.map((c) => ({ + name: c.name as string, + address: c.address as string, + ...parseCredentials(c.username, c.password), + ...parseKeystore(c), + })); + } + + if (metrics) { + initialValues.metrics = { + type: metrics.type as string, + ...parseCredentials(metrics.username, metrics.password), + ...parseKeystore(metrics), + port: `${metrics.port}`, + }; + } + + const properties = payload.properties || {}; + + // Authentification + initialValues.customAuth = {}; + + Object.entries(properties).forEach(([key, val]) => { + if ( + key.startsWith('security.') || + key.startsWith('sasl.') || + key.startsWith('ssl.') + ) { + initialValues.customAuth = { + ...initialValues.customAuth, + [convertPropsKeyToFormKey(key)]: val, + }; + } + }); + + return initialValues as ClusterConfigFormValues; +}; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getIsValidConfig.ts b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getIsValidConfig.ts new file mode 100644 index 00000000000..d39f1aebffe --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getIsValidConfig.ts @@ -0,0 +1,49 @@ +import { ApplicationConfigValidation } from 'generated-sources'; +import { showAlert } from 'lib/errorHandling'; + +export const getIsValidConfig = ( + { clusters }: ApplicationConfigValidation, + name: string +) => { + let isValid = true; + const prefix = `cluster-${name}`; + const clusterErrors = clusters?.[name]; + + if (clusterErrors?.kafka?.error) { + isValid = false; + showAlert('error', { + id: `${prefix}-kafka`, + title: 'Kafka Cluster', + message: clusterErrors?.kafka.errorMessage, + }); + } + if (clusterErrors?.schemaRegistry?.error) { + isValid = false; + showAlert('error', { + id: `${prefix}-schemaRegistry`, + title: 'Schema Registry', + message: clusterErrors?.schemaRegistry.errorMessage, + }); + } + if (clusterErrors?.ksqldb?.error) { + isValid = false; + showAlert('error', { + id: `${prefix}-ksqldb`, + title: 'KSQL DB', + message: clusterErrors?.ksqldb?.errorMessage, + }); + } + if (clusterErrors?.kafkaConnects) { + Object.entries(clusterErrors.kafkaConnects).forEach(([key, val]) => { + if (val?.error) { + isValid = false; + showAlert('error', { + id: `${prefix}-kafkaConnects-${key}`, + title: `Kafka Connect. ${key}`, + message: val.errorMessage, + }); + } + }); + } + return isValid; +}; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getJaasConfig.ts b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getJaasConfig.ts new file mode 100644 index 00000000000..23578159d44 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getJaasConfig.ts @@ -0,0 +1,33 @@ +import { isUndefined } from 'lodash'; + +const JAAS_CONFIGS = { + 'SASL/GSSAPI': 'com.sun.security.auth.module.Krb5LoginModule', + 'SASL/OAUTHBEARER': + 'org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule', + 'SASL/PLAIN': 'org.apache.kafka.common.security.plain.PlainLoginModule', + 'SASL/SCRAM-256': 'org.apache.kafka.common.security.scram.ScramLoginModule', + 'SASL/SCRAM-512': 'org.apache.kafka.common.security.scram.ScramLoginModule', + 'Delegation tokens': + 'org.apache.kafka.common.security.scram.ScramLoginModule', + 'SASL/LDAP': 'org.apache.kafka.common.security.plain.PlainLoginModule', + 'SASL/AWS IAM': 'software.amazon.msk.auth.iam.IAMLoginModule', +}; + +type MethodName = keyof typeof JAAS_CONFIGS; + +export const getJaasConfig = ( + method: MethodName, + options: Record +) => { + const optionsString = Object.entries(options) + .map(([key, value]) => { + if (isUndefined(value)) return null; + if (value === 'true' || value === 'false') { + return ` ${key}=${value}`; + } + return ` ${key}="${value}"`; + }) + .join(''); + + return `${JAAS_CONFIGS[method]} required${optionsString};`; +}; diff --git a/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/transformFormDataToPayload.ts b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/transformFormDataToPayload.ts new file mode 100644 index 00000000000..91f9ad1e289 --- /dev/null +++ b/kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/transformFormDataToPayload.ts @@ -0,0 +1,231 @@ +import { ClusterConfigFormValues } from 'widgets/ClusterConfigForm/types'; +import { ApplicationConfigPropertiesKafkaClusters } from 'generated-sources'; + +import { getJaasConfig } from './getJaasConfig'; +import { convertFormKeyToPropsKey } from './convertFormKeyToPropsKey'; + +const transformToKeystore = (keystore?: { + location: string; + password: string; +}) => { + if (!keystore || !keystore.location) return undefined; + return { + keystoreLocation: keystore.location, + keystorePassword: keystore.password, + }; +}; + +const transformToCredentials = ( + isAuth: boolean, + username?: string, + password?: string +) => { + if (!isAuth || !username || !password) return undefined; + return { username, password }; +}; + +const transformCustomProps = (props: Record) => { + const config: Record = {}; + if (!props) return config; + + Object.entries(props).forEach(([key, val]) => { + if (props[key]) config[convertFormKeyToPropsKey(key)] = val; + }); + + return config; +}; + +export const transformFormDataToPayload = (data: ClusterConfigFormValues) => { + const config: ApplicationConfigPropertiesKafkaClusters = { + name: data.name, + bootstrapServers: data.bootstrapServers + .map(({ host, port }) => `${host}:${port}`) + .join(','), + readOnly: data.readOnly, + }; + + if (data.truststore) { + config.ssl = { + truststoreLocation: data.truststore?.location, + truststorePassword: data.truststore?.password, + }; + } + + // Schema Registry + if (data.schemaRegistry) { + config.schemaRegistry = data.schemaRegistry.url; + config.schemaRegistryAuth = transformToCredentials( + data.schemaRegistry.isAuth, + data.schemaRegistry.username, + data.schemaRegistry.password + ); + config.schemaRegistrySsl = transformToKeystore( + data.schemaRegistry.keystore + ); + } + + // KSQL + if (data.ksql) { + config.ksqldbServer = data.ksql.url; + config.ksqldbServerAuth = transformToCredentials( + data.ksql.isAuth, + data.ksql.username, + data.ksql.password + ); + config.ksqldbServerSsl = transformToKeystore(data.ksql.keystore); + } + + // Kafka Connect + if (data.kafkaConnect && data.kafkaConnect.length > 0) { + config.kafkaConnect = data.kafkaConnect.map( + ({ name, address, isAuth, username, password, keystore }) => ({ + name, + address, + ...transformToKeystore(keystore), + ...transformToCredentials(isAuth, username, password), + }) + ); + } + + // Metrics + if (data.metrics) { + config.metrics = { + type: data.metrics.type, + port: Number(data.metrics.port), + ...transformToKeystore(data.metrics.keystore), + ...transformToCredentials( + data.metrics.isAuth, + data.metrics.username, + data.metrics.password + ), + }; + } + + config.properties = { + ...transformCustomProps(data.customAuth), + }; + + // Authentication + if (data.auth) { + const { method, props, securityProtocol, keystore } = data.auth; + switch (method) { + case 'SASL/JAAS': + config.properties = { + 'security.protocol': securityProtocol, + 'sasl.jaas.config': props.saslJaasConfig, + 'sasl.mechanism': props.saslMechanism, + }; + break; + case 'SASL/GSSAPI': + config.properties = { + 'security.protocol': securityProtocol, + 'sasl.mechanism': 'GSSAPI', + 'sasl.kerberos.service.name': props.saslKerberosServiceName, + 'sasl.jaas.config': getJaasConfig('SASL/GSSAPI', { + useKeyTab: props.keyTabFile ? 'true' : 'false', + keyTab: props.keyTabFile, + storeKey: String(!!props.storeKey), + principal: props.principal, + }), + }; + break; + case 'SASL/OAUTHBEARER': + config.properties = { + 'security.protocol': securityProtocol, + 'sasl.mechanism': 'OAUTHBEARER', + 'sasl.jaas.config': getJaasConfig('SASL/OAUTHBEARER', { + unsecuredLoginStringClaim_sub: props.unsecuredLoginStringClaim_sub, + }), + }; + break; + case 'SASL/PLAIN': + config.properties = { + 'security.protocol': securityProtocol, + 'sasl.mechanism': 'PLAIN', + 'sasl.jaas.config': getJaasConfig( + 'SASL/PLAIN', + transformToCredentials( + Boolean(props.isAuth), + props.username, + props.password + ) || {} + ), + }; + break; + case 'SASL/SCRAM-256': + config.properties = { + 'security.protocol': securityProtocol, + 'sasl.mechanism': 'SCRAM-SHA-256', + 'sasl.jaas.config': getJaasConfig( + 'SASL/SCRAM-256', + transformToCredentials( + Boolean(props.isAuth), + props.username, + props.password + ) || {} + ), + }; + break; + case 'SASL/SCRAM-512': + config.properties = { + 'security.protocol': securityProtocol, + 'sasl.mechanism': 'SCRAM-SHA-512', + 'sasl.jaas.config': getJaasConfig( + 'SASL/SCRAM-512', + transformToCredentials( + Boolean(props.isAuth), + props.username, + props.password + ) || {} + ), + }; + break; + case 'Delegation tokens': + config.properties = { + 'security.protocol': securityProtocol, + 'sasl.jaas.config': getJaasConfig('Delegation tokens', { + username: props.tokenId, + password: props.tokenValue, + tokenauth: 'true', + }), + }; + break; + case 'SASL/LDAP': + config.properties = { + 'security.protocol': securityProtocol, + 'sasl.mechanism': 'PLAIN', + 'sasl.jaas.config': getJaasConfig( + 'SASL/LDAP', + transformToCredentials( + Boolean(props.isAuth), + props.username, + props.password + ) || {} + ), + }; + break; + case 'SASL/AWS IAM': + config.properties = { + 'security.protocol': securityProtocol, + 'sasl.mechanism': 'AWS_MSK_IAM', + 'sasl.client.callback.handler.class': + 'software.amazon.msk.auth.iam.IAMClientCallbackHandler', + 'sasl.jaas.config': getJaasConfig('SASL/AWS IAM', { + awsProfileName: props.awsProfileName, + }), + }; + break; + case 'mTLS': + config.properties = { + 'security.protocol': 'SSL', + 'ssl.keystore.location': keystore?.location, + 'ssl.keystore.password': keystore?.password, + }; + break; + default: + // do nothing + } + } + + return config; +}; diff --git a/kafka-ui-react-app/tsconfig.json b/kafka-ui-react-app/tsconfig.json index 71b9322edb2..8af0e99764a 100644 --- a/kafka-ui-react-app/tsconfig.json +++ b/kafka-ui-react-app/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "esnext", "lib": [ "dom", "dom.iterable", @@ -19,9 +19,12 @@ "noEmit": true, "jsx": "react-jsx", "baseUrl": "src", - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "types": ["vite/client"] }, "include": [ "src", + "vite.config.ts", + "jest.config.ts", ] } diff --git a/kafka-ui-react-app/vite.config.ts b/kafka-ui-react-app/vite.config.ts new file mode 100644 index 00000000000..b4cdd022a9f --- /dev/null +++ b/kafka-ui-react-app/vite.config.ts @@ -0,0 +1,86 @@ +import { + defineConfig, + loadEnv, + UserConfigExport, + splitVendorChunkPlugin, +} from 'vite'; +import react from '@vitejs/plugin-react-swc'; +import tsconfigPaths from 'vite-tsconfig-paths'; +import { ViteEjsPlugin } from 'vite-plugin-ejs'; + +export default defineConfig(({ mode }) => { + process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }; + + const defaultConfig: UserConfigExport = { + plugins: [ + react(), + tsconfigPaths(), + splitVendorChunkPlugin(), + ViteEjsPlugin({ + PUBLIC_PATH: mode !== 'development' ? 'PUBLIC-PATH-VARIABLE' : '', + }), + ], + server: { + port: 3000, + }, + build: { + outDir: 'build', + rollupOptions: { + output: { + manualChunks: { + ace: ['ace-builds', 'react-ace'], + }, + }, + }, + }, + experimental: { + renderBuiltUrl( + filename: string, + { + hostType, + }: { + hostId: string; + hostType: 'js' | 'css' | 'html'; + type: 'asset' | 'public'; + } + ) { + if (hostType === 'js') { + return { + runtime: `window.__assetsPathBuilder(${JSON.stringify(filename)})`, + }; + } + + return filename; + }, + }, + define: { + 'process.env.NODE_ENV': `"${mode}"`, + 'process.env.VITE_TAG': `"${process.env.VITE_TAG}"`, + 'process.env.VITE_COMMIT': `"${process.env.VITE_COMMIT}"`, + }, + }; + const proxy = process.env.VITE_DEV_PROXY; + if (mode === 'development' && proxy) { + return { + ...defaultConfig, + server: { + ...defaultConfig.server, + open: true, + proxy: { + '/api': { + target: proxy, + changeOrigin: true, + secure: false, + }, + '/actuator/info': { + target: proxy, + changeOrigin: true, + secure: false, + }, + }, + }, + }; + } + + return defaultConfig; +}); diff --git a/kafka-ui-serde-api/pom.xml b/kafka-ui-serde-api/pom.xml new file mode 100644 index 00000000000..729beead531 --- /dev/null +++ b/kafka-ui-serde-api/pom.xml @@ -0,0 +1,123 @@ + + + 4.0.0 + jar + + 17 + 17 + + + + ossrh + https://s01.oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + + + kafka-ui-serde-api + kafka-ui-serde-api + http://github.com/provectus/kafka-ui + + + The Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + Provectus + maintainers.kafka-ui@provectus.com + Provectus + https://provectus.com + + + + scm:git:git://github.com/provectus/kafka-ui.git + scm:git:ssh://github.com:provectus/kafka-ui.git + https://github.com/provectus/kafka-ui + + com.provectus + kafka-ui-serde-api + 1.0.0 + + + + + org.apache.maven.plugins + maven-install-plugin + 2.5.2 + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.13 + true + + ossrh + https://s01.oss.sonatype.org/ + true + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + 8 + + 3.5.0 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + + --pinentry-mode + loopback + + + + + + + diff --git a/kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/DeserializeResult.java b/kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/DeserializeResult.java new file mode 100644 index 00000000000..1aa111dc60b --- /dev/null +++ b/kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/DeserializeResult.java @@ -0,0 +1,85 @@ +package com.provectus.kafka.ui.serde.api; + +import java.util.Collections; +import java.util.Map; +import java.util.Objects; + +/** + * Result of {@code Deserializer} work. + */ +public final class DeserializeResult { + + public enum Type { + STRING, JSON + } + + // nullable + private final String result; + private final Type type; + private final Map additionalProperties; + + /** + * @param result string representation of deserialized binary data + * @param type type of string - can it be converted to json or not + * @param additionalProperties additional information about deserialized value (will be shown in UI) + */ + public DeserializeResult(String result, Type type, Map additionalProperties) { + this.result = result; + this.type = type != null ? type : Type.STRING; + this.additionalProperties = additionalProperties != null ? additionalProperties : Collections.emptyMap(); + } + + /** + * @return string representation of deserialized binary data, can be null + */ + public String getResult() { + return result; + } + + /** + * @return additional information about deserialized value. + * Will be show as json dictionary in UI (serialized with Jackson object mapper). + * It is recommended to use primitive types and strings for values. + */ + public Map getAdditionalProperties() { + return additionalProperties; + } + + /** + * @return type of deserialized result. Will be used as hint for some internal logic + * (ex. if type==STRING smart filters won't try to parse it as json for further usage) + */ + public Type getType() { + return type; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DeserializeResult that = (DeserializeResult) o; + return Objects.equals(result, that.result) + && type == that.type + && additionalProperties.equals(that.additionalProperties); + } + + @Override + public int hashCode() { + return Objects.hash(result, type, additionalProperties); + } + + @Override + public String toString() { + return "DeserializeResult{" + + "result='" + result + + '\'' + + ", type=" + type + + ", additionalProperties=" + + additionalProperties + + '}'; + } +} diff --git a/kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/PropertyResolver.java b/kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/PropertyResolver.java new file mode 100644 index 00000000000..363b98bd58f --- /dev/null +++ b/kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/PropertyResolver.java @@ -0,0 +1,44 @@ +package com.provectus.kafka.ui.serde.api; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Provides access to configuration properties. + *Actual implementation uses {@code org.springframework.boot.context.properties.bind.Binder} class + * to bind values to target types. Target type params can be custom configs classes, not only simple types and strings. + * + */ +public interface PropertyResolver { + + /** + * Get property value by name. + * + * @param key property name + * @param targetType type of property value + * @return property value or empty {@code Optional} if property not found + */ + Optional getProperty(String key, Class targetType); + + + /** + * Get list-property value by name + * + * @param key list property name + * @param itemType type of list element + * @return list property value or empty {@code Optional} if property not found + */ + Optional> getListProperty(String key, Class itemType); + + /** + * Get map-property value by name + * + * @param key map-property name + * @param keyType type of map key + * @param valueType type of map value + * @return map-property value or empty {@code Optional} if property not found + */ + Optional> getMapProperty(String key, Class keyType, Class valueType); + +} \ No newline at end of file diff --git a/kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/RecordHeader.java b/kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/RecordHeader.java new file mode 100644 index 00000000000..41762c0c800 --- /dev/null +++ b/kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/RecordHeader.java @@ -0,0 +1,9 @@ +package com.provectus.kafka.ui.serde.api; + +public interface RecordHeader { + + String key(); + + byte[] value(); + +} diff --git a/kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/RecordHeaders.java b/kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/RecordHeaders.java new file mode 100644 index 00000000000..800adca26ad --- /dev/null +++ b/kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/RecordHeaders.java @@ -0,0 +1,5 @@ +package com.provectus.kafka.ui.serde.api; + + +public interface RecordHeaders extends Iterable { +} diff --git a/kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/SchemaDescription.java b/kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/SchemaDescription.java new file mode 100644 index 00000000000..9eb5d9da0ca --- /dev/null +++ b/kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/SchemaDescription.java @@ -0,0 +1,37 @@ +package com.provectus.kafka.ui.serde.api; + +import java.util.Map; + +/** + * Description of topic's key/value schema. + */ +public final class SchemaDescription { + + private final String schema; + private final Map additionalProperties; + + /** + * + * @param schema schema descriptions. + * If contains json-schema (preferred) UI will use it for validation and sample data generation. + * @param additionalProperties additional properties about schema (may be rendered in UI in the future) + */ + public SchemaDescription(String schema, Map additionalProperties) { + this.schema = schema; + this.additionalProperties = additionalProperties; + } + + /** + * @return schema description text. Preferably contains json-schema. Can be null. + */ + public String getSchema() { + return schema; + } + + /** + * @return additional properties about schema + */ + public Map getAdditionalProperties() { + return additionalProperties; + } +} diff --git a/kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/Serde.java b/kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/Serde.java new file mode 100644 index 00000000000..49552a1440a --- /dev/null +++ b/kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/Serde.java @@ -0,0 +1,108 @@ +package com.provectus.kafka.ui.serde.api; + +import java.io.Closeable; +import java.util.Optional; + +/** + * Main interface of serialization/deserialization logic. + * It provides ability to serialize, deserialize topic's keys and values, and optionally provides + * information about data schema inside topic. + *

    + * Lifecycle:
    + * 1. on application startup kafka-ui scans configs and finds all custom serde definitions
    + * 2. for each custom serde its own separated child-first classloader is created
    + * 3. kafka-ui loads class defined in configuration and instantiates instance of that class using default, non-arg constructor
    + * 4. {@code configure(...)} method called
    + * 5. various methods called during application runtime
    + * 6. on application shutdown kafka-ui calls {@code close()} method on serde instance
    + *

    + * Implementation considerations:
    + * 1. Implementation class should have default/non-arg contructor
    + * 2. All methods except {@code configure(...)} and {@code close()} can be called from different threads. So, your code should be thread-safe.
    + * 3. All methods will be executed in separate child-first classloader.
    + */ +public interface Serde extends Closeable { + + /** + * Kafka record's part that Serde will be applied to. + */ + enum Target { + KEY, VALUE + } + + /** + * Reads configuration using property resolvers and sets up serde's internal state. + * + * @param serdeProperties specific serde instance's properties + * @param kafkaClusterProperties properties of the custer for what serde is instantiated + * @param globalProperties global application properties + */ + void configure( + PropertyResolver serdeProperties, + PropertyResolver kafkaClusterProperties, + PropertyResolver globalProperties + ); + + /** + * @return Serde's description. Treated as Markdown text. Will be shown in UI. + */ + Optional getDescription(); + + /** + * @return SchemaDescription for specified topic's key/value. + * {@code Optional.empty} if there is not information about schema. + */ + Optional getSchema(String topic, Target type); + + /** + * @return true if this Serde can be applied to specified topic's key/value deserialization + */ + boolean canDeserialize(String topic, Target type); + + /** + * @return true if this Serde can be applied to specified topic's key/value serialization + */ + boolean canSerialize(String topic, Target type); + + /** + * Closes resources opened by Serde. + */ + @Override + default void close() { + //intentionally left blank + } + + //---------------------------------------------------------------------------- + + /** + * Creates {@code Serializer} for specified topic's key/value. + * Kafka-ui doesn't cache {@code Serializes} - new one will be created each time user's message needs to be serialized. + * (Unless kafka-ui supports batch inserts). + */ + Serializer serializer(String topic, Target type); + + /** + * Creates {@code Deserializer} for specified topic's key/value. + * {@code Deserializer} will be created for each kafka polling and will be used for all messages within that polling cycle. + */ + Deserializer deserializer(String topic, Target type); + + /** + * Serializes client's input to {@code bytes[]} that will be sent to kafka as key/value (depending on what {@code Type} it was created for). + */ + interface Serializer { + + /** + * @param input string entered by user into UI text field.
    Note: this input is not formatted in any way. + */ + byte[] serialize(String input); + } + + /** + * Deserializes polled record's key/value (depending on what {@code Type} it was created for). + */ + interface Deserializer { + DeserializeResult deserialize(RecordHeaders headers, byte[] data); + } + +} diff --git a/pom.xml b/pom.xml index e7c5898b15f..aa02a56f0e9 100644 --- a/pom.xml +++ b/pom.xml @@ -6,45 +6,61 @@ kafka-ui-contract kafka-ui-api + kafka-ui-serde-api kafka-ui-e2e-checks - 13 - 13 + 17 UTF-8 - 2.6.7 - 0.2.2 - 1.4.2.Final - 1.18.20 - 1.18.20 - latest - 2.8.0 - v16.15.0 - 1.4.10 - 1.12.1 - 3.8.1 - 3.1.0 - 3.2.0 - 2.22.2 - 4.3.0 - 1.6.0 - 1.2.32 - 1.11.0 - 7.0.1 - 2.11.1 - 1.16.2 - 5.7.2 - 2.21.0 - 3.19.0 - 4.7.1 - 3.0.9 - ..//kafka-ui-react-app/src/generated-sources provectus https://sonarcloud.io + latest + + + 4.12.0 + 2.11.1 + 3.19.0 + 1.11.1 + 1.12.19 + 7.4.0 + 3.1.0 + 3.0.13 + 2.14.0 + 3.5.0 + 1.5.5.Final + 1.18.24 + 3.23.3 + 2.13.9 + 2.0 + 3.1.3 + 1.0.0 + 0.1.17 + 0.1.26 + 20230227 + + + 5.9.1 + 5.3.1 + 4.10.0 + 1.17.5 + + + v18.17.1 + v8.6.12 + + + 0.42.1 + 1.12.1 + 3.2.0 + 3.10.1 + 3.2.0 + 3.1.2 + 6.6.0 + 1.2.32 @@ -82,6 +98,81 @@ + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + + org.scala-lang + scala-library + ${scala-lang.library.version} + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + com.google.protobuf + protobuf-java + ${protobuf-java.version} + + + org.junit + junit-bom + ${junit.version} + pom + import + + + org.testcontainers + testcontainers-bom + ${testcontainers.version} + pom + import + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + org.apache.maven.plugins + maven-clean-plugin + ${maven-clean-plugin.version} + + + + + com.provectus kafka-ui 0.0.1-SNAPSHOT diff --git a/settings.xml b/settings.xml new file mode 100644 index 00000000000..7935a5f5390 --- /dev/null +++ b/settings.xml @@ -0,0 +1,17 @@ + + + + ossrh + ${server.username} + ${server.password} + + + + + ossrh + + true + + + + \ No newline at end of file