[
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "{\n\t\"name\": \"Java\",\n\n\t\"image\": \"mcr.microsoft.com/devcontainers/java:0-17\",\n\n\t\"features\": {\n\t\t\"ghcr.io/devcontainers/features/java:1\": {\n\t\t\t\"version\": \"none\",\n\t\t\t\"installMaven\": \"true\",\n\t\t\t\"installGradle\": \"false\"\n\t\t},\n\t\t\"ghcr.io/devcontainers/features/docker-in-docker:2\": {}\n\t},\n\n\t// Use 'forwardPorts' to make a list of ports inside the container available locally.\n\t// \"forwardPorts\": [],\n\n\t// Use 'postCreateCommand' to run commands after the container is created.\n\t// \"postCreateCommand\": \"java -version\",\n\n\t\"customizations\": {\n\t\t\"vscode\": {\n\t\t\t\"extensions\" : [\n\t\t\t\t\"vscjava.vscode-java-pack\",\n\t\t\t\t\"vscjava.vscode-maven\",\n\t\t\t\t\"vscjava.vscode-java-debug\",\n\t\t\t\t\"EditorConfig.EditorConfig\",\n\t\t\t\t\"ms-azuretools.vscode-docker\",\n\t\t\t\t\"antfu.vite\",\n\t\t\t\t\"ms-kubernetes-tools.vscode-kubernetes-tools\",\n                \"github.vscode-pull-request-github\"\n\t\t\t]\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": ".editorconfig",
    "content": "[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 4\nindent_style = space\ninsert_final_newline = true\nmax_line_length = 120\ntab_width = 4\nij_continuation_indent_size = 8\nij_formatter_off_tag = @formatter:off\nij_formatter_on_tag = @formatter:on\nij_formatter_tags_enabled = true\nij_smart_tabs = false\nij_visual_guides = none\nij_wrap_on_typing = false\ntrim_trailing_whitespace = true\n\n[*.java]\nindent_size = 2\nij_continuation_indent_size = 4\nij_java_align_consecutive_assignments = false\nij_java_align_consecutive_variable_declarations = false\nij_java_align_group_field_declarations = false\nij_java_align_multiline_annotation_parameters = false\nij_java_align_multiline_array_initializer_expression = false\nij_java_align_multiline_assignment = false\nij_java_align_multiline_binary_operation = false\nij_java_align_multiline_chained_methods = false\nij_java_align_multiline_extends_list = false\nij_java_align_multiline_for = true\nij_java_align_multiline_method_parentheses = false\nij_java_align_multiline_parameters = true\nij_java_align_multiline_parameters_in_calls = false\nij_java_align_multiline_parenthesized_expression = false\nij_java_align_multiline_records = true\nij_java_align_multiline_resources = true\nij_java_align_multiline_ternary_operation = false\nij_java_align_multiline_text_blocks = false\nij_java_align_multiline_throws_list = false\nij_java_align_subsequent_simple_methods = false\nij_java_align_throws_keyword = false\nij_java_align_types_in_multi_catch = true\nij_java_annotation_parameter_wrap = off\nij_java_array_initializer_new_line_after_left_brace = false\nij_java_array_initializer_right_brace_on_new_line = false\nij_java_array_initializer_wrap = normal\nij_java_assert_statement_colon_on_next_line = false\nij_java_assert_statement_wrap = normal\nij_java_assignment_wrap = normal\nij_java_binary_operation_sign_on_next_line = false\nij_java_binary_operation_wrap = normal\nij_java_blank_lines_after_anonymous_class_header = 0\nij_java_blank_lines_after_class_header = 0\nij_java_blank_lines_after_imports = 1\nij_java_blank_lines_after_package = 1\nij_java_blank_lines_around_class = 1\nij_java_blank_lines_around_field = 0\nij_java_blank_lines_around_field_in_interface = 0\nij_java_blank_lines_around_initializer = 1\nij_java_blank_lines_around_method = 1\nij_java_blank_lines_around_method_in_interface = 1\nij_java_blank_lines_before_class_end = 0\nij_java_blank_lines_before_imports = 1\nij_java_blank_lines_before_method_body = 0\nij_java_blank_lines_before_package = 1\nij_java_block_brace_style = end_of_line\nij_java_block_comment_add_space = false\nij_java_block_comment_at_first_column = true\nij_java_builder_methods = none\nij_java_call_parameters_new_line_after_left_paren = false\nij_java_call_parameters_right_paren_on_new_line = false\nij_java_call_parameters_wrap = normal\nij_java_case_statement_on_separate_line = true\nij_java_catch_on_new_line = false\nij_java_class_annotation_wrap = split_into_lines\nij_java_class_brace_style = end_of_line\nij_java_class_count_to_use_import_on_demand = 999\nij_java_class_names_in_javadoc = 1\nij_java_do_not_indent_top_level_class_members = false\nij_java_do_not_wrap_after_single_annotation = false\nij_java_do_not_wrap_after_single_annotation_in_parameter = false\nij_java_do_while_brace_force = always\nij_java_doc_add_blank_line_after_description = true\nij_java_doc_add_blank_line_after_param_comments = false\nij_java_doc_add_blank_line_after_return = false\nij_java_doc_add_p_tag_on_empty_lines = true\nij_java_doc_align_exception_comments = true\nij_java_doc_align_param_comments = true\nij_java_doc_do_not_wrap_if_one_line = false\nij_java_doc_enable_formatting = true\nij_java_doc_enable_leading_asterisks = true\nij_java_doc_indent_on_continuation = false\nij_java_doc_keep_empty_lines = true\nij_java_doc_keep_empty_parameter_tag = true\nij_java_doc_keep_empty_return_tag = true\nij_java_doc_keep_empty_throws_tag = true\nij_java_doc_keep_invalid_tags = true\nij_java_doc_param_description_on_new_line = false\nij_java_doc_preserve_line_breaks = false\nij_java_doc_use_throws_not_exception_tag = true\nij_java_else_on_new_line = false\nij_java_entity_dd_suffix = EJB\nij_java_entity_eb_suffix = Bean\nij_java_entity_hi_suffix = Home\nij_java_entity_lhi_prefix = Local\nij_java_entity_lhi_suffix = Home\nij_java_entity_li_prefix = Local\nij_java_entity_pk_class = java.lang.String\nij_java_entity_vo_suffix = VO\nij_java_enum_constants_wrap = normal\nij_java_extends_keyword_wrap = normal\nij_java_extends_list_wrap = normal\nij_java_field_annotation_wrap = split_into_lines\nij_java_finally_on_new_line = false\nij_java_for_brace_force = always\nij_java_for_statement_new_line_after_left_paren = false\nij_java_for_statement_right_paren_on_new_line = false\nij_java_for_statement_wrap = normal\nij_java_generate_final_locals = false\nij_java_generate_final_parameters = false\nij_java_if_brace_force = always\nij_java_imports_layout = $*,|,*\nij_java_indent_case_from_switch = true\nij_java_insert_inner_class_imports = false\nij_java_insert_override_annotation = true\nij_java_keep_blank_lines_before_right_brace = 2\nij_java_keep_blank_lines_between_package_declaration_and_header = 2\nij_java_keep_blank_lines_in_code = 2\nij_java_keep_blank_lines_in_declarations = 2\nij_java_keep_builder_methods_indents = false\nij_java_keep_control_statement_in_one_line = true\nij_java_keep_first_column_comment = true\nij_java_keep_indents_on_empty_lines = false\nij_java_keep_line_breaks = true\nij_java_keep_multiple_expressions_in_one_line = false\nij_java_keep_simple_blocks_in_one_line = false\nij_java_keep_simple_classes_in_one_line = false\nij_java_keep_simple_lambdas_in_one_line = false\nij_java_keep_simple_methods_in_one_line = false\nij_java_label_indent_absolute = false\nij_java_label_indent_size = 0\nij_java_lambda_brace_style = end_of_line\nij_java_layout_static_imports_separately = true\nij_java_line_comment_add_space = false\nij_java_line_comment_add_space_on_reformat = false\nij_java_line_comment_at_first_column = true\nij_java_message_dd_suffix = EJB\nij_java_message_eb_suffix = Bean\nij_java_method_annotation_wrap = split_into_lines\nij_java_method_brace_style = end_of_line\nij_java_method_call_chain_wrap = normal\nij_java_method_parameters_new_line_after_left_paren = false\nij_java_method_parameters_right_paren_on_new_line = false\nij_java_method_parameters_wrap = normal\nij_java_modifier_list_wrap = false\nij_java_multi_catch_types_wrap = normal\nij_java_names_count_to_use_import_on_demand = 999\nij_java_new_line_after_lparen_in_annotation = false\nij_java_new_line_after_lparen_in_record_header = false\nij_java_parameter_annotation_wrap = normal\nij_java_parentheses_expression_new_line_after_left_paren = false\nij_java_parentheses_expression_right_paren_on_new_line = false\nij_java_place_assignment_sign_on_next_line = false\nij_java_prefer_longer_names = true\nij_java_prefer_parameters_wrap = false\nij_java_record_components_wrap = normal\nij_java_repeat_synchronized = true\nij_java_replace_instanceof_and_cast = false\nij_java_replace_null_check = true\nij_java_replace_sum_lambda_with_method_ref = true\nij_java_resource_list_new_line_after_left_paren = false\nij_java_resource_list_right_paren_on_new_line = false\nij_java_resource_list_wrap = normal\nij_java_rparen_on_new_line_in_annotation = false\nij_java_rparen_on_new_line_in_record_header = false\nij_java_session_dd_suffix = EJB\nij_java_session_eb_suffix = Bean\nij_java_session_hi_suffix = Home\nij_java_session_lhi_prefix = Local\nij_java_session_lhi_suffix = Home\nij_java_session_li_prefix = Local\nij_java_session_si_suffix = Service\nij_java_space_after_closing_angle_bracket_in_type_argument = false\nij_java_space_after_colon = true\nij_java_space_after_comma = true\nij_java_space_after_comma_in_type_arguments = true\nij_java_space_after_for_semicolon = true\nij_java_space_after_quest = true\nij_java_space_after_type_cast = true\nij_java_space_before_annotation_array_initializer_left_brace = false\nij_java_space_before_annotation_parameter_list = false\nij_java_space_before_array_initializer_left_brace = true\nij_java_space_before_catch_keyword = true\nij_java_space_before_catch_left_brace = true\nij_java_space_before_catch_parentheses = true\nij_java_space_before_class_left_brace = true\nij_java_space_before_colon = true\nij_java_space_before_colon_in_foreach = true\nij_java_space_before_comma = false\nij_java_space_before_do_left_brace = true\nij_java_space_before_else_keyword = true\nij_java_space_before_else_left_brace = true\nij_java_space_before_finally_keyword = true\nij_java_space_before_finally_left_brace = true\nij_java_space_before_for_left_brace = true\nij_java_space_before_for_parentheses = true\nij_java_space_before_for_semicolon = false\nij_java_space_before_if_left_brace = true\nij_java_space_before_if_parentheses = true\nij_java_space_before_method_call_parentheses = false\nij_java_space_before_method_left_brace = true\nij_java_space_before_method_parentheses = false\nij_java_space_before_opening_angle_bracket_in_type_parameter = false\nij_java_space_before_quest = true\nij_java_space_before_switch_left_brace = true\nij_java_space_before_switch_parentheses = true\nij_java_space_before_synchronized_left_brace = true\nij_java_space_before_synchronized_parentheses = true\nij_java_space_before_try_left_brace = true\nij_java_space_before_try_parentheses = true\nij_java_space_before_type_parameter_list = false\nij_java_space_before_while_keyword = true\nij_java_space_before_while_left_brace = true\nij_java_space_before_while_parentheses = true\nij_java_space_inside_one_line_enum_braces = false\nij_java_space_within_empty_array_initializer_braces = false\nij_java_space_within_empty_method_call_parentheses = false\nij_java_space_within_empty_method_parentheses = false\nij_java_spaces_around_additive_operators = true\nij_java_spaces_around_annotation_eq = true\nij_java_spaces_around_assignment_operators = true\nij_java_spaces_around_bitwise_operators = true\nij_java_spaces_around_equality_operators = true\nij_java_spaces_around_lambda_arrow = true\nij_java_spaces_around_logical_operators = true\nij_java_spaces_around_method_ref_dbl_colon = false\nij_java_spaces_around_multiplicative_operators = true\nij_java_spaces_around_relational_operators = true\nij_java_spaces_around_shift_operators = true\nij_java_spaces_around_type_bounds_in_type_parameters = true\nij_java_spaces_around_unary_operator = false\nij_java_spaces_within_angle_brackets = false\nij_java_spaces_within_annotation_parentheses = false\nij_java_spaces_within_array_initializer_braces = false\nij_java_spaces_within_braces = false\nij_java_spaces_within_brackets = false\nij_java_spaces_within_cast_parentheses = false\nij_java_spaces_within_catch_parentheses = false\nij_java_spaces_within_for_parentheses = false\nij_java_spaces_within_if_parentheses = false\nij_java_spaces_within_method_call_parentheses = false\nij_java_spaces_within_method_parentheses = false\nij_java_spaces_within_parentheses = false\nij_java_spaces_within_record_header = false\nij_java_spaces_within_switch_parentheses = false\nij_java_spaces_within_synchronized_parentheses = false\nij_java_spaces_within_try_parentheses = false\nij_java_spaces_within_while_parentheses = false\nij_java_special_else_if_treatment = true\nij_java_subclass_name_suffix = Impl\nij_java_ternary_operation_signs_on_next_line = false\nij_java_ternary_operation_wrap = normal\nij_java_test_name_suffix = Test\nij_java_throws_keyword_wrap = normal\nij_java_throws_list_wrap = normal\nij_java_use_external_annotations = false\nij_java_use_fq_class_names = false\nij_java_use_relative_indents = false\nij_java_use_single_class_imports = true\nij_java_variable_annotation_wrap = normal\nij_java_visibility = public\nij_java_while_brace_force = always\nij_java_while_on_new_line = false\nij_java_wrap_comments = false\nij_java_wrap_first_method_in_call_chain = false\nij_java_wrap_long_lines = false\n\n[*.md]\ninsert_final_newline = false\ntrim_trailing_whitespace = false\n\n[*.yaml]\nindent_size = 2\n[*.yml]\nindent_size = 2\n\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "*                           @Haarolean\n\n\n# BACKEND\n/pom.xml                    @provectus/kafka-backend\n\n/kafka-ui-contract/         @provectus/kafka-backend\n\n/kafka-ui-api/              @provectus/kafka-backend\n\n# FRONTEND\n/kafka-ui-react-app/        @provectus/kafka-frontend\n\n# TESTS\n/kafka-ui-e2e-checks/       @provectus/kafka-qa\n\n# INFRA\n/.github/workflows/         @provectus/kafka-devops\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.yml",
    "content": "name: \"\\U0001F41E  Bug report\"\ndescription: File a bug report\nlabels: [\"status/triage\", \"type/bug\"]\nassignees: []\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Hi, thanks for raising the issue(-s), all contributions really matter!\n        Please, note that we'll close the issue without further explanation if you don't follow\n        this template and don't provide the information requested within this template.\n\n  - type: checkboxes\n    id: terms\n    attributes:\n      label: Issue submitter TODO list\n      description: By you checking these checkboxes we can be sure you've done the essential things.\n      options:\n        - label: I've looked up my issue in [FAQ](https://docs.kafka-ui.provectus.io/faq/common-problems)\n          required: true\n        - label: I've searched for an already existing issues [here](https://github.com/provectus/kafka-ui/issues)\n          required: true\n        - label: I've tried running `master`-labeled docker image and the issue still persists there\n          required: true\n        - label: I'm running a supported version of the application which is listed [here](https://github.com/provectus/kafka-ui/blob/master/SECURITY.md)\n          required: true\n\n  - type: textarea\n    attributes:\n      label: Describe the bug (actual behavior)\n      description: A clear and concise description of what the bug is. Use a list, if there is more than one problem\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Expected behavior\n      description: A clear and concise description of what you expected to happen\n    validations:\n      required: false\n\n  - type: textarea\n    attributes:\n      label: Your installation details\n      description: |\n        How do you run the app? Please provide as much info as possible:\n        1. App version (commit hash in the top left corner of the UI)\n        2. Helm chart version, if you use one\n        3. Your application config. Please remove the sensitive info like passwords or API keys.\n        4. Any IAAC configs\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Steps to reproduce\n      description: |\n        Please write down the order of the actions required to reproduce the issue.\n        For the advanced setups/complicated issue, we might need you to provide\n        a minimal [reproducible example](https://stackoverflow.com/help/minimal-reproducible-example).\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Screenshots\n      description: |\n        If applicable, add screenshots to help explain your problem\n    validations:\n      required: false\n\n  - type: textarea\n    attributes:\n      label: Logs\n      description: |\n        If applicable, *upload* screenshots to help explain your problem\n    validations:\n      required: false\n\n  - type: textarea\n    attributes:\n      label: Additional context\n      description: |\n        Add any other context about the problem here. E.G.:\n        1. Are there any alternative scenarios (different data/methods/configuration/setup) you have tried?\n          Were they successful or the same issue occurred? Please provide steps as well.\n        2. Related issues (if there are any).\n        3. Logs (if available)\n        4. Is there any serious impact or behaviour on the end-user because of this issue, that can be overlooked?\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Report helm issue\n    url: https://github.com/provectus/kafka-ui-charts\n    about: Our helm charts are located in another repo. Please raise issues/PRs regarding charts in that repo.\n  - name: Official documentation\n    url: https://docs.kafka-ui.provectus.io/\n    about: Before reaching out for support, please refer to our documentation. Read \"FAQ\" and \"Common problems\", also try using search there.\n  - name: Community Discord\n    url: https://discord.gg/4DWzD7pGE5\n    about: Chat with other users, get some support or ask questions.\n  - name: GitHub Discussions\n    url: https://github.com/provectus/kafka-ui/discussions\n    about: An alternative place to ask questions or to get some support.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature.yml",
    "content": "name: \"\\U0001F680 Feature request\"\ndescription: Propose a new feature\nlabels: [\"status/triage\", \"type/feature\"]\nassignees: []\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Hi, thanks for raising the issue(-s), all contributions really matter!\n        Please, note that we'll close the issue without further explanation if you don't follow\n        this template and don't provide the information requested within this template.\n\n  - type: checkboxes\n    id: terms\n    attributes:\n      label: Issue submitter TODO list\n      description: By you checking these checkboxes we can be sure you've done the essential things.\n      options:\n        - label: I've searched for an already existing issues [here](https://github.com/provectus/kafka-ui/issues)\n          required: true\n        - 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\n          required: true\n\n  - type: textarea\n    attributes:\n      label: Is your proposal related to a problem?\n      description: |\n        Provide a clear and concise description of what the problem is.\n        For example, \"I'm always frustrated when...\"\n    validations:\n      required: false\n\n  - type: textarea\n    attributes:\n      label: Describe the feature you're interested in\n      description: |\n        Provide a clear and concise description of what you want to happen.\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Describe alternatives you've considered\n      description: |\n        Let us know about other solutions you've tried or researched.\n    validations:\n      required: false\n\n  - type: input\n    attributes:\n      label: Version you're running\n      description: |\n        Please provide the app version you're currently running:\n        1. App version (commit hash in the top left corner of the UI)\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Additional context\n      description: |\n        Is there anything else you can add about the proposal?\n        You might want to link to related issues here, if you haven't already.\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!-- ignore-task-list-start -->\n- [ ] **Breaking change?** (if so, please describe the impact and migration path for existing application instances)\n\n\n<!-- ignore-task-list-end -->\n**What changes did you make?** (Give an overview)\n\n**Is there anything you'd like reviewers to focus on?**\n\n\n**How Has This Been Tested?** (put an \"x\" (case-sensitive!) next to an item)\n<!-- ignore-task-list-start -->\n- [ ] No need to\n- [ ] Manually (please, describe, if necessary)\n- [ ] Unit checks\n- [ ] Integration checks\n- [ ] Covered by existing automation\n<!-- ignore-task-list-end -->\n\n**Checklist** (put an \"x\" (case-sensitive!) next to all the items, otherwise the build will fail)\n- [ ] I have performed a self-review of my own code\n- [ ] I have commented my code, particularly in hard-to-understand areas\n- [ ] I have made corresponding changes to the documentation (e.g. **ENVIRONMENT VARIABLES**)\n- [ ] My changes generate no new warnings (e.g. Sonar is happy)\n- [ ] I have added tests that prove my fix is effective or that my feature works\n- [ ] New and existing unit tests pass locally with my changes\n- [ ] Any dependent changes have been merged\n\nCheck out [Contributing](https://github.com/provectus/kafka-ui/blob/master/CONTRIBUTING.md) and [Code of Conduct](https://github.com/provectus/kafka-ui/blob/master/CODE-OF-CONDUCT.md)\n\n**A picture of a cute animal (not mandatory but encouraged)**\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n- package-ecosystem: maven\n  directory: \"/\"\n  schedule:\n    interval: daily\n    time: \"10:00\"\n    timezone: Europe/Moscow\n  reviewers:\n    - \"Haarolean\"\n  labels:\n    - \"scope/backend\"\n    - \"type/dependencies\"\n- package-ecosystem: npm\n  directory: \"/kafka-ui-react-app\"\n  schedule:\n    interval: weekly\n    time: \"10:00\"\n    timezone: Europe/Moscow\n  open-pull-requests-limit: 10\n  versioning-strategy: increase-if-necessary\n  labels:\n    - \"scope/frontend\"\n    - \"type/dependencies\"\n  ignore:\n  - dependency-name: react-hook-form\n    versions:\n    - 6.15.5\n    - 7.0.0\n    - 7.0.6\n  - dependency-name: \"@hookform/error-message\"\n    versions:\n    - 1.1.0\n  - dependency-name: use-debounce\n    versions:\n    - 6.0.0\n    - 6.0.1\n  - dependency-name: \"@rooks/use-outside-click-ref\"\n    versions:\n    - 4.10.1\n  - dependency-name: react-multi-select-component\n    versions:\n    - 3.1.6\n    - 4.0.0\n  - dependency-name: husky\n    versions:\n    - 5.1.3\n    - 5.2.0\n    - 6.0.0\n  - dependency-name: \"@types/node-fetch\"\n    versions:\n    - 2.5.9\n  - dependency-name: \"@testing-library/jest-dom\"\n    versions:\n    - 5.11.10\n  - dependency-name: \"@typescript-eslint/eslint-plugin\"\n    versions:\n    - 4.20.0\n  - dependency-name: \"@openapitools/openapi-generator-cli\"\n    versions:\n    - 2.2.5\n  - dependency-name: \"@typescript-eslint/parser\"\n    versions:\n    - 4.20.0\n  - dependency-name: react-datepicker\n    versions:\n    - 3.7.0\n  - dependency-name: eslint\n    versions:\n    - 7.23.0\n  - dependency-name: \"@testing-library/user-event\"\n    versions:\n    - 13.0.6\n  - dependency-name: immer\n    versions:\n    - 9.0.1\n  - dependency-name: react-scripts\n    versions:\n    - 4.0.3\n  - dependency-name: eslint-config-prettier\n    versions:\n    - 8.1.0\n  - dependency-name: \"@testing-library/react\"\n    versions:\n    - 11.2.5\n  - dependency-name: lodash\n    versions:\n    - 4.17.21\n  - dependency-name: react-json-tree\n    versions:\n    - 0.15.0\n- package-ecosystem: \"github-actions\"\n  directory: \"/\"\n  schedule:\n    interval: weekly\n    time: \"10:00\"\n    timezone: Europe/Moscow\n  reviewers:\n    - \"Haarolean\"\n  labels:\n    - \"scope/infrastructure\"\n    - \"type/dependencies\"\n"
  },
  {
    "path": ".github/release_drafter.yaml",
    "content": "name-template: '$RESOLVED_VERSION'\ntag-template: 'v$RESOLVED_VERSION'\ntemplate: |\n  ## Changes\n  $CHANGES\n  ## Contributors\n  $CONTRIBUTORS\n\nexclude-labels:\n  - 'scope/infrastructure'\n  - 'scope/QA'\n  - 'scope/AQA'\n  - 'type/dependencies'\n  - 'type/chore'\n  - 'type/documentation'\n  - 'type/refactoring'\n\ncategories:\n  - title: '🚩 Breaking Changes'\n    labels:\n      - 'impact/changelog'\n\n  - title: '⚙️Features'\n    labels:\n      - 'type/feature'\n\n  - title: '🪛Enhancements'\n    labels:\n      - 'type/enhancement'\n\n  - title: '🔨Bug Fixes'\n    labels:\n      - 'type/bug'\n\n  - title: 'Security'\n    labels:\n      - 'type/security'\n\n  - title: '⎈ Helm/K8S Changes'\n    labels:\n      - 'scope/k8s'\n\nchange-template: '- $TITLE @$AUTHOR (#$NUMBER)'\n\nversion-resolver:\n  major:\n    labels:\n      - 'major'\n  minor:\n    labels:\n      - 'minor'\n  patch:\n    labels:\n      - 'patch'\n  default: patch\n"
  },
  {
    "path": ".github/workflows/aws_publisher.yaml",
    "content": "name: \"Infra: Release: AWS Marketplace Publisher\"\non:\n  workflow_dispatch:\n    inputs:\n      KafkaUIInfraBranch:\n        description: 'Branch name of Kafka-UI-Infra repo, build commands will be executed from this branch'\n        required: true\n        default: 'master'\n      KafkaUIReleaseVersion:\n        description: 'Version of KafkaUI'\n        required: true\n        default: '0.3.2'\n      PublishOnMarketplace:\n        description: 'If set to true, the request to update AWS Server product version will be raised'\n        required: true\n        default: false\n        type: boolean\n\njobs:\n  build-ami:\n    name: Build AMI\n    runs-on: ubuntu-latest\n    steps:\n      - name: Clone infra repo\n        run: |\n          echo \"Cloning repo...\"\n          git clone https://infra-tech:${{ secrets.INFRA_USER_ACCESS_TOKEN }}@github.com/provectus/kafka-ui-infra.git --branch ${{ github.event.inputs.KafkaUIInfraBranch }}\n          echo \"Cd to packer DIR...\"\n          cd kafka-ui-infra/ami\n          echo \"WORK_DIR=$(pwd)\" >> $GITHUB_ENV\n          echo \"Packer will be triggered in this dir $WORK_DIR\"\n\n      - name: Configure AWS credentials for Kafka-UI account\n        uses: aws-actions/configure-aws-credentials@v3\n        with:\n          aws-access-key-id: ${{ secrets.AWS_AMI_PUBLISH_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.AWS_AMI_PUBLISH_KEY_SECRET }}\n          aws-region: us-east-1\n\n      # validate templates\n      - name: Validate Template\n        uses: hashicorp/packer-github-actions@master\n        with:\n          command: validate\n          arguments: -syntax-only\n          target: kafka-ui-infra/ami/kafka-ui.pkr.hcl\n\n      # build artifact\n      - name: Build Artifact\n        uses: hashicorp/packer-github-actions@master\n        with:\n          command: build\n          arguments: \"-color=false -on-error=abort -var=kafka_ui_release_version=${{ github.event.inputs.KafkaUIReleaseVersion }}\"\n          target: kafka-ui.pkr.hcl\n          working_directory: ${{ env.WORK_DIR }}\n        env:\n          PACKER_LOG: 1\n\n      # add fresh AMI to AWS Marketplace\n      - name: Publish Artifact at Marketplace\n        if: ${{ github.event.inputs.PublishOnMarketplace == 'true' }}\n        env:\n          PRODUCT_ID: ${{ secrets.AWS_SERVER_PRODUCT_ID }}\n          RELEASE_VERSION: \"${{ github.event.inputs.KafkaUIReleaseVersion }}\"\n          RELEASE_NOTES: \"https://github.com/provectus/kafka-ui/releases/tag/v${{ github.event.inputs.KafkaUIReleaseVersion }}\"\n          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\n          AMI_OS_VERSION: \"amzn2-ami-kernel-5.10-hvm-*-x86_64-gp2\"\n        run: |\n          set -x\n          pwd\n          ls -la kafka-ui-infra/ami\n          echo $WORK_DIR/manifest.json\n          export AMI_ID=$(jq -r '.builds[-1].artifact_id' kafka-ui-infra/ami/manifest.json | cut -d \":\" -f2)\n          /bin/bash kafka-ui-infra/aws-marketplace/prepare_changeset.sh > changeset.json\n          aws marketplace-catalog start-change-set \\\n            --catalog \"AWSMarketplace\" \\\n            --change-set \"$(cat changeset.json)\"\n"
  },
  {
    "path": ".github/workflows/backend.yml",
    "content": "name: \"Backend: PR/master build & test\"\non:\n  push:\n    branches:\n      - master\n  pull_request_target:\n    types: [\"opened\", \"edited\", \"reopened\", \"synchronize\"]\n    paths:\n      - \"kafka-ui-api/**\"\n      - \"pom.xml\"\npermissions:\n  checks: write\n  pull-requests: write\njobs:\n  build-and-test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 0\n          ref: ${{ github.event.pull_request.head.sha }}\n      - name: Set up JDK\n        uses: actions/setup-java@v3\n        with:\n          java-version: '17'\n          distribution: 'zulu'\n          cache: 'maven'\n      - name: Cache SonarCloud packages\n        uses: actions/cache@v3\n        with:\n          path: ~/.sonar/cache\n          key: ${{ runner.os }}-sonar\n          restore-keys: ${{ runner.os }}-sonar\n      - name: Build and analyze pull request target\n        if: ${{ github.event_name == 'pull_request' }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN_BACKEND }}\n          HEAD_REF: ${{ github.head_ref }}\n          BASE_REF: ${{ github.base_ref }}\n        run: |\n          ./mvnw -B -ntp versions:set -DnewVersion=${{ github.event.pull_request.head.sha }}\n          ./mvnw -B -V -ntp verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \\\n          -Dsonar.projectKey=com.provectus:kafka-ui_backend \\\n          -Dsonar.pullrequest.key=${{ github.event.pull_request.number }} \\\n          -Dsonar.pullrequest.branch=$HEAD_REF \\\n          -Dsonar.pullrequest.base=$BASE_REF\n      - name: Build and analyze push master\n        if: ${{ github.event_name == 'push' }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN_BACKEND }}\n        run: |\n          ./mvnw -B -ntp versions:set -DnewVersion=$GITHUB_SHA\n          ./mvnw -B -V -ntp verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \\\n          -Dsonar.projectKey=com.provectus:kafka-ui_backend\n"
  },
  {
    "path": ".github/workflows/block_merge.yml",
    "content": "name: \"Infra: PR block merge\"\non:\n  pull_request:\n    types: [opened, labeled, unlabeled, synchronize]\njobs:\n  block_merge:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: mheap/github-action-required-labels@v5\n        with:\n          mode: exactly\n          count: 0\n          labels: \"status/blocked, status/needs-attention, status/on-hold, status/pending, status/triage, status/pending-backend, status/pending-frontend, status/pending-QA\"\n"
  },
  {
    "path": ".github/workflows/branch-deploy.yml",
    "content": "name: \"Infra: Feature Testing: Init env\"\non:\n  workflow_dispatch:\n\n  pull_request:\n    types: ['labeled']\njobs:\n  build:\n    if: ${{ github.event.label.name == 'status/feature_testing' || github.event.label.name == 'status/feature_testing_public' }}\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          ref: ${{ github.event.pull_request.head.sha }}\n      - name: get branch name\n        id: extract_branch\n        run: |\n          tag='pr${{ github.event.pull_request.number }}'\n          echo \"tag=${tag}\" >> $GITHUB_OUTPUT\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      - name: Set up JDK\n        uses: actions/setup-java@v3\n        with:\n          java-version: '17'\n          distribution: 'zulu'\n          cache: 'maven'\n      - name: Build\n        id: build\n        run: |\n          ./mvnw -B -ntp versions:set -DnewVersion=$GITHUB_SHA\n          ./mvnw -B -V -ntp clean package -Pprod -DskipTests\n          export VERSION=$(./mvnw -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec)\n          echo \"version=${VERSION}\" >> $GITHUB_OUTPUT\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v2\n      - name: Set up Docker Buildx\n        id: buildx\n        uses: docker/setup-buildx-action@v2\n      - name: Cache Docker layers\n        uses: actions/cache@v3\n        with:\n          path: /tmp/.buildx-cache\n          key: ${{ runner.os }}-buildx-${{ github.sha }}\n          restore-keys: |\n            ${{ runner.os }}-buildx-\n      - name: Configure AWS credentials for Kafka-UI account\n        uses: aws-actions/configure-aws-credentials@v3\n        with:\n          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          aws-region: eu-central-1\n      - name: Login to Amazon ECR\n        id: login-ecr\n        uses: aws-actions/amazon-ecr-login@v1\n      - name: Build and push\n        id: docker_build_and_push\n        uses: docker/build-push-action@v4\n        with:\n          builder: ${{ steps.buildx.outputs.name }}\n          context: kafka-ui-api\n          push: true\n          tags: 297478128798.dkr.ecr.eu-central-1.amazonaws.com/kafka-ui:${{ steps.extract_branch.outputs.tag }}\n          build-args: |\n            JAR_FILE=kafka-ui-api-${{ steps.build.outputs.version }}.jar\n          cache-from: type=local,src=/tmp/.buildx-cache\n          cache-to: type=local,dest=/tmp/.buildx-cache\n    outputs:\n      tag: ${{ steps.extract_branch.outputs.tag }}\n  make-branch-env:\n    needs: build\n    runs-on: ubuntu-latest\n    steps:\n      - name: clone\n        run: |\n          git clone https://infra-tech:${{ secrets.INFRA_USER_ACCESS_TOKEN }}@github.com/provectus/kafka-ui-infra.git --branch envs\n      - name: create deployment\n        run: |\n          cd kafka-ui-infra/aws-infrastructure4eks/argocd/scripts\n          echo \"Branch:${{ needs.build.outputs.tag }}\"\n          ./kafka-ui-deployment-from-branch.sh ${{ needs.build.outputs.tag }} ${{ github.event.label.name }} ${{ secrets.FEATURE_TESTING_UI_PASSWORD }}\n          git config --global user.email \"infra-tech@provectus.com\"\n          git config --global user.name \"infra-tech\"\n          git add ../kafka-ui-from-branch/\n          git commit -m \"added env:${{ needs.build.outputs.deploy }}\" && git push || true\n\n      - name: update status check for private deployment\n        if: ${{ github.event.label.name == 'status/feature_testing' }}\n        uses: Sibz/github-status-action@v1.1.6\n        with:\n          authToken: ${{secrets.GITHUB_TOKEN}}\n          context: \"Click Details button to open custom deployment page\"\n          state: \"success\"\n          sha: ${{ github.event.pull_request.head.sha  || github.sha }}\n          target_url: \"http://${{ needs.build.outputs.tag }}.internal.kafka-ui.provectus.io\"\n\n      - name: update status check for public deployment\n        if: ${{ github.event.label.name == 'status/feature_testing_public' }}\n        uses: Sibz/github-status-action@v1.1.6\n        with:\n          authToken: ${{secrets.GITHUB_TOKEN}}\n          context: \"Click Details button to open custom deployment page\"\n          state: \"success\"\n          sha: ${{ github.event.pull_request.head.sha  || github.sha }}\n          target_url: \"http://${{ needs.build.outputs.tag }}.internal.kafka-ui.provectus.io\"\n"
  },
  {
    "path": ".github/workflows/branch-remove.yml",
    "content": "name: \"Infra: Feature Testing: Destroy env\"\non:\n  workflow_dispatch:\n  pull_request:\n    types: ['unlabeled', 'closed']\njobs:\n  remove:\n    runs-on: ubuntu-latest\n    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'))) }}\n    steps:\n      - uses: actions/checkout@v3\n      - name: clone\n        run: |\n          git clone https://infra-tech:${{ secrets.INFRA_USER_ACCESS_TOKEN }}@github.com/provectus/kafka-ui-infra.git --branch envs\n      - name: remove env\n        run: |\n          cd kafka-ui-infra/aws-infrastructure4eks/argocd/scripts\n          ./delete-env.sh pr${{ github.event.pull_request.number }} || true\n          git config --global user.email \"infra-tech@provectus.com\"\n          git config --global user.name \"infra-tech\"\n          git add ../kafka-ui-from-branch/\n          git commit -m \"removed env:${{ needs.build.outputs.deploy }}\" && git push || true\n"
  },
  {
    "path": ".github/workflows/build-public-image.yml",
    "content": "name: \"Infra: Image Testing: Deploy\"\non:\n  workflow_dispatch:\n  pull_request:\n    types: ['labeled']\njobs:\n  build:\n    if: ${{ github.event.label.name == 'status/image_testing' }}\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          ref: ${{ github.event.pull_request.head.sha }}\n      - name: get branch name\n        id: extract_branch\n        run: |\n          tag='${{ github.event.pull_request.number }}'\n          echo \"tag=${tag}\" >> $GITHUB_OUTPUT\n      - name: Set up JDK\n        uses: actions/setup-java@v3\n        with:\n          java-version: '17'\n          distribution: 'zulu'\n          cache: 'maven'\n      - name: Build\n        id: build\n        run: |\n          ./mvnw -B -ntp versions:set -DnewVersion=$GITHUB_SHA\n          ./mvnw -B -V -ntp clean package -Pprod -DskipTests\n          export VERSION=$(./mvnw -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec)\n          echo \"version=${VERSION}\" >> $GITHUB_OUTPUT\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v2\n      - name: Set up Docker Buildx\n        id: buildx\n        uses: docker/setup-buildx-action@v2\n      - name: Cache Docker layers\n        uses: actions/cache@v3\n        with:\n          path: /tmp/.buildx-cache\n          key: ${{ runner.os }}-buildx-${{ github.sha }}\n          restore-keys: |\n            ${{ runner.os }}-buildx-\n      - name: Configure AWS credentials for Kafka-UI account\n        uses: aws-actions/configure-aws-credentials@v3\n        with:\n          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          aws-region: us-east-1\n      - name: Login to Amazon ECR\n        id: login-ecr\n        uses: aws-actions/amazon-ecr-login@v1\n        with:\n          registry-type: 'public'\n      - name: Build and push\n        id: docker_build_and_push\n        uses: docker/build-push-action@v4\n        with:\n          builder: ${{ steps.buildx.outputs.name }}\n          context: kafka-ui-api\n          push: true\n          tags: public.ecr.aws/provectus/kafka-ui-custom-build:${{ steps.extract_branch.outputs.tag }}\n          build-args: |\n            JAR_FILE=kafka-ui-api-${{ steps.build.outputs.version }}.jar\n          cache-from: type=local,src=/tmp/.buildx-cache\n          cache-to: type=local,dest=/tmp/.buildx-cache\n      - name: make comment with private deployment link\n        uses: peter-evans/create-or-update-comment@v3\n        with:\n          issue-number: ${{ github.event.pull_request.number }}\n          body: |\n            Image published at public.ecr.aws/provectus/kafka-ui-custom-build:${{ steps.extract_branch.outputs.tag }}\n    outputs:\n      tag: ${{ steps.extract_branch.outputs.tag }}\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ master ]\n    paths:\n    - 'kafka-ui-contract/**'\n    - 'kafka-ui-react-app/**'\n    - 'kafka-ui-api/**'\n    - 'kafka-ui-serde-api/**'\n  schedule:\n    - cron: '39 15 * * 6'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'javascript', 'java' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]\n        # Learn more:\n        # 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\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v3\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v2\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n        # queries: ./path/to/local/query, your-org/your-repo/queries@main\n\n    - name: Set up JDK\n      uses: actions/setup-java@v3\n      with:\n          java-version: '17'\n          distribution: 'zulu'\n          cache: 'maven'\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v2\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 https://git.io/JvXDl\n\n    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines\n    #    and modify them (or add more) to build your code if your project\n    #    uses a compiled language\n\n    #- run: |\n    #   make bootstrap\n    #   make release\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v2\n"
  },
  {
    "path": ".github/workflows/cve.yaml",
    "content": "name: CVE checks docker master\non:\n  workflow_dispatch:\n  schedule:\n    # * is a special character in YAML so you have to quote this string\n    - cron:  '0 8 15 * *'\njobs:\n  build-and-test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Set up JDK\n        uses: actions/setup-java@v3\n        with:\n          java-version: '17'\n          distribution: 'zulu'\n          cache: 'maven'\n\n      - name: Build project\n        id: build\n        run: |\n          ./mvnw -B -ntp versions:set -DnewVersion=$GITHUB_SHA\n          ./mvnw -B -V -ntp clean package -DskipTests\n          export VERSION=$(./mvnw -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec)\n          echo \"version=${VERSION}\" >> $GITHUB_OUTPUT\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v2\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v2\n\n      - name: Cache Docker layers\n        uses: actions/cache@v3\n        with:\n          path: /tmp/.buildx-cache\n          key: ${{ runner.os }}-buildx-${{ github.sha }}\n          restore-keys: |\n            ${{ runner.os }}-buildx-\n\n      - name: Build docker image\n        uses: docker/build-push-action@v4\n        with:\n          builder: ${{ steps.buildx.outputs.name }}\n          context: kafka-ui-api\n          platforms: linux/amd64\n          push: false\n          load: true\n          tags: |\n            provectuslabs/kafka-ui:${{ steps.build.outputs.version }}\n          build-args: |\n            JAR_FILE=kafka-ui-api-${{ steps.build.outputs.version }}.jar\n          cache-from: type=local,src=/tmp/.buildx-cache\n          cache-to: type=local,dest=/tmp/.buildx-cache\n\n      - name: Run CVE checks\n        uses: aquasecurity/trivy-action@0.12.0\n        with:\n          image-ref: \"provectuslabs/kafka-ui:${{ steps.build.outputs.version }}\"\n          format: \"table\"\n          exit-code: \"1\"\n"
  },
  {
    "path": ".github/workflows/delete-public-image.yml",
    "content": "name: \"Infra: Image Testing: Delete\"\non:\n  workflow_dispatch:\n  pull_request:\n    types: ['unlabeled', 'closed']\njobs:\n  remove:\n    if: ${{ github.event.label.name == 'status/image_testing' || ( github.event.action == 'closed' && (contains(github.event.pull_request.labels, 'status/image_testing'))) }}\n    runs-on: ubuntu-latest\n    steps:\n      - name: get branch name\n        id: extract_branch\n        run: |\n          echo\n          tag='${{ github.event.pull_request.number }}'\n          echo \"tag=${tag}\" >> $GITHUB_OUTPUT\n      - name: Configure AWS credentials for Kafka-UI account\n        uses: aws-actions/configure-aws-credentials@v3\n        with:\n          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          aws-region: us-east-1\n      - name: Login to Amazon ECR\n        id: login-ecr\n        uses: aws-actions/amazon-ecr-login@v1\n        with:\n          registry-type: 'public'\n      - name: Remove from ECR\n        id: remove_from_ecr\n        run: |\n          aws ecr-public batch-delete-image \\\n                --repository-name kafka-ui-custom-build \\\n                --image-ids imageTag=${{ steps.extract_branch.outputs.tag }} \\\n                --region us-east-1\n"
  },
  {
    "path": ".github/workflows/documentation.yaml",
    "content": "name: \"Infra: Docs: URL linter\"\non:\n  pull_request:\n    types:\n      - opened\n      - labeled\n      - reopened\n      - synchronize\n    paths:\n      - 'documentation/**'\n      - '**.md'\njobs:\n  build-and-test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - name: Check URLs in files\n        uses: urlstechie/urlchecker-action@0.0.34\n        with:\n          exclude_patterns: localhost,127.0.,192.168.\n          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\n          print_all: false\n          file_types: .md\n"
  },
  {
    "path": ".github/workflows/e2e-automation.yml",
    "content": "name: \"E2E: Automation suite\"\non:\n  workflow_dispatch:\n    inputs:\n      test_suite:\n        description: 'Select test suite to run'\n        default: 'regression'\n        required: true\n        type: choice\n        options:\n          - regression\n          - sanity\n          - smoke\n      qase_token:\n        description: 'Set Qase token to enable integration'\n        required: false\n        type: string\n\njobs:\n  build-and-test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          ref: ${{ github.sha }}\n      - name: Configure AWS credentials\n        uses: aws-actions/configure-aws-credentials@v3\n        with:\n          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          aws-region: eu-central-1\n      - name: Set up environment\n        id: set_env_values\n        run: |\n          cat \"./kafka-ui-e2e-checks/.env.ci\" >> \"./kafka-ui-e2e-checks/.env\"\n      - name: Pull with Docker\n        id: pull_chrome\n        run: |\n          docker pull selenoid/vnc_chrome:103.0\n      - name: Set up JDK\n        uses: actions/setup-java@v3\n        with:\n          java-version: '17'\n          distribution: 'zulu'\n          cache: 'maven'\n      - name: Build with Maven\n        id: build_app\n        run: |\n          ./mvnw -B -ntp versions:set -DnewVersion=${{ github.sha }}\n          ./mvnw -B -V -ntp clean install -Pprod -Dmaven.test.skip=true ${{ github.event.inputs.extraMavenOptions }}\n      - name: Compose with Docker\n        id: compose_app\n        # use the following command until #819 will be fixed\n        run: |\n          docker-compose -f kafka-ui-e2e-checks/docker/selenoid-git.yaml up -d\n          docker-compose -f ./documentation/compose/e2e-tests.yaml up -d\n      - name: Run test suite\n        run: |\n          ./mvnw -B -ntp versions:set -DnewVersion=${{ github.sha }}\n          ./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\n      - name: Generate Allure report\n        uses: simple-elf/allure-report-action@master\n        if: always()\n        id: allure-report\n        with:\n          allure_results: ./kafka-ui-e2e-checks/allure-results\n          gh_pages: allure-results\n          allure_report: allure-report\n          subfolder: allure-results\n          report_url: \"http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com\"\n      - uses: jakejarvis/s3-sync-action@master\n        if: always()\n        env:\n          AWS_S3_BUCKET: 'kafkaui-allure-reports'\n          AWS_REGION: 'eu-central-1'\n          SOURCE_DIR: 'allure-history/allure-results'\n      - name: Deploy report to Amazon S3\n        if: always()\n        uses: Sibz/github-status-action@v1.1.6\n        with:\n          authToken: ${{secrets.GITHUB_TOKEN}}\n          context: \"Click Details button to open Allure report\"\n          state: \"success\"\n          sha: ${{ github.sha }}\n          target_url: http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com/${{ github.run_number }}\n      - name: Dump Docker logs on failure\n        if: failure()\n        uses: jwalton/gh-docker-logs@v2.2.1\n"
  },
  {
    "path": ".github/workflows/e2e-checks.yaml",
    "content": "name: \"E2E: PR healthcheck\"\non:\n  pull_request_target:\n    types: [ \"opened\", \"edited\", \"reopened\", \"synchronize\" ]\n    paths:\n      - \"kafka-ui-api/**\"\n      - \"kafka-ui-contract/**\"\n      - \"kafka-ui-react-app/**\"\n      - \"kafka-ui-e2e-checks/**\"\n      - \"pom.xml\"\npermissions:\n  statuses: write\njobs:\n  build-and-test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          ref: ${{ github.event.pull_request.head.sha }}\n      - name: Configure AWS credentials\n        uses: aws-actions/configure-aws-credentials@v3\n        with:\n          aws-access-key-id: ${{ secrets.S3_AWS_ACCESS_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.S3_AWS_SECRET_ACCESS_KEY }}\n          aws-region: eu-central-1\n      - name: Set up environment\n        id: set_env_values\n        run: |\n          cat \"./kafka-ui-e2e-checks/.env.ci\" >> \"./kafka-ui-e2e-checks/.env\"\n      - name: Pull with Docker\n        id: pull_chrome\n        run: |\n          docker pull selenoid/vnc_chrome:103.0\n      - name: Set up JDK\n        uses: actions/setup-java@v3\n        with:\n          java-version: '17'\n          distribution: 'zulu'\n          cache: 'maven'\n      - name: Build with Maven\n        id: build_app\n        run: |\n          ./mvnw -B -ntp versions:set -DnewVersion=${{ github.event.pull_request.head.sha }}\n          ./mvnw -B -V -ntp clean install -Pprod -Dmaven.test.skip=true ${{ github.event.inputs.extraMavenOptions }}\n      - name: Compose with Docker\n        id: compose_app\n        # use the following command until #819 will be fixed\n        run: |\n          docker-compose -f kafka-ui-e2e-checks/docker/selenoid-git.yaml up -d\n          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\n      - name: Run test suite\n        run: |\n          ./mvnw -B -ntp versions:set -DnewVersion=${{ github.event.pull_request.head.sha }}\n          ./mvnw -B -V -ntp -Dsurefire.suiteXmlFiles='src/test/resources/smoke.xml' -f 'kafka-ui-e2e-checks' test -Pprod\n      - name: Generate allure report\n        uses: simple-elf/allure-report-action@master\n        if: always()\n        id: allure-report\n        with:\n          allure_results: ./kafka-ui-e2e-checks/allure-results\n          gh_pages: allure-results\n          allure_report: allure-report\n          subfolder: allure-results\n          report_url: \"http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com\"\n      - uses: jakejarvis/s3-sync-action@master\n        if: always()\n        env:\n          AWS_S3_BUCKET: 'kafkaui-allure-reports'\n          AWS_REGION: 'eu-central-1'\n          SOURCE_DIR: 'allure-history/allure-results'\n      - name: Deploy report to Amazon S3\n        if: always()\n        uses: Sibz/github-status-action@v1.1.6\n        with:\n          authToken: ${{secrets.GITHUB_TOKEN}}\n          context: \"Click Details button to open Allure report\"\n          state: \"success\"\n          sha: ${{ github.event.pull_request.head.sha  || github.sha }}\n          target_url: http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com/${{ github.run_number }}\n      - name: Dump docker logs on failure\n        if: failure()\n        uses: jwalton/gh-docker-logs@v2.2.1\n"
  },
  {
    "path": ".github/workflows/e2e-manual.yml",
    "content": "name: \"E2E: Manual suite\"\non:\n  workflow_dispatch:\n    inputs:\n      test_suite:\n        description: 'Select test suite to run'\n        default: 'manual'\n        required: true\n        type: choice\n        options:\n          - manual\n          - qase\n      qase_token:\n        description: 'Set Qase token to enable integration'\n        required: true\n        type: string\n\njobs:\n  build-and-test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          ref: ${{ github.sha }}\n      - name: Set up environment\n        id: set_env_values\n        run: |\n          cat \"./kafka-ui-e2e-checks/.env.ci\" >> \"./kafka-ui-e2e-checks/.env\"\n      - name: Set up JDK\n        uses: actions/setup-java@v3\n        with:\n          java-version: '17'\n          distribution: 'zulu'\n          cache: 'maven'\n      - name: Build with Maven\n        id: build_app\n        run: |\n          ./mvnw -B -ntp versions:set -DnewVersion=${{ github.sha }}\n          ./mvnw -B -V -ntp clean install -Pprod -Dmaven.test.skip=true ${{ github.event.inputs.extraMavenOptions }}\n      - name: Run test suite\n        run: |\n          ./mvnw -B -ntp versions:set -DnewVersion=${{ github.sha }}\n          ./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\n"
  },
  {
    "path": ".github/workflows/e2e-weekly.yml",
    "content": "name: \"E2E: Weekly suite\"\non:\n  schedule:\n    - cron: '0 1 * * 1'\n\njobs:\n  build-and-test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          ref: ${{ github.sha }}\n      - name: Configure AWS credentials\n        uses: aws-actions/configure-aws-credentials@v3\n        with:\n          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          aws-region: eu-central-1\n      - name: Set up environment\n        id: set_env_values\n        run: |\n          cat \"./kafka-ui-e2e-checks/.env.ci\" >> \"./kafka-ui-e2e-checks/.env\"\n      - name: Pull with Docker\n        id: pull_chrome\n        run: |\n          docker pull selenoid/vnc_chrome:103.0\n      - name: Set up JDK\n        uses: actions/setup-java@v3\n        with:\n          java-version: '17'\n          distribution: 'zulu'\n          cache: 'maven'\n      - name: Build with Maven\n        id: build_app\n        run: |\n          ./mvnw -B -ntp versions:set -DnewVersion=${{ github.sha }}\n          ./mvnw -B -V -ntp clean install -Pprod -Dmaven.test.skip=true ${{ github.event.inputs.extraMavenOptions }}\n      - name: Compose with Docker\n        id: compose_app\n        # use the following command until #819 will be fixed\n        run: |\n          docker-compose -f kafka-ui-e2e-checks/docker/selenoid-git.yaml up -d\n          docker-compose -f ./documentation/compose/e2e-tests.yaml up -d\n      - name: Run test suite\n        run: |\n          ./mvnw -B -ntp versions:set -DnewVersion=${{ github.sha }}\n          ./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\n      - name: Generate Allure report\n        uses: simple-elf/allure-report-action@master\n        if: always()\n        id: allure-report\n        with:\n          allure_results: ./kafka-ui-e2e-checks/allure-results\n          gh_pages: allure-results\n          allure_report: allure-report\n          subfolder: allure-results\n          report_url: \"http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com\"\n      - uses: jakejarvis/s3-sync-action@master\n        if: always()\n        env:\n          AWS_S3_BUCKET: 'kafkaui-allure-reports'\n          AWS_REGION: 'eu-central-1'\n          SOURCE_DIR: 'allure-history/allure-results'\n      - name: Deploy report to Amazon S3\n        if: always()\n        uses: Sibz/github-status-action@v1.1.6\n        with:\n          authToken: ${{secrets.GITHUB_TOKEN}}\n          context: \"Click Details button to open Allure report\"\n          state: \"success\"\n          sha: ${{ github.sha }}\n          target_url: http://kafkaui-allure-reports.s3-website.eu-central-1.amazonaws.com/${{ github.run_number }}\n      - name: Dump Docker logs on failure\n        if: failure()\n        uses: jwalton/gh-docker-logs@v2.2.1\n"
  },
  {
    "path": ".github/workflows/frontend.yaml",
    "content": "name: \"Frontend: PR/master build & test\"\non:\n  push:\n    branches:\n      - master\n  pull_request_target:\n    types: [\"opened\", \"edited\", \"reopened\", \"synchronize\"]\n    paths:\n      - \"kafka-ui-contract/**\"\n      - \"kafka-ui-react-app/**\"\npermissions:\n  checks: write\n  pull-requests: write\njobs:\n  build-and-test:\n    env:\n      CI: true\n      NODE_ENV: dev\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          # Disabling shallow clone is recommended for improving relevancy of reporting\n          fetch-depth: 0\n          ref: ${{ github.event.pull_request.head.sha }}\n      - uses: pnpm/action-setup@v2.4.0\n        with:\n          version: 8.6.12\n      - name: Install node\n        uses: actions/setup-node@v3.8.1\n        with:\n          node-version: \"18.17.1\"\n          cache: \"pnpm\"\n          cache-dependency-path: \"./kafka-ui-react-app/pnpm-lock.yaml\"\n      - name: Install Node dependencies\n        run: |\n          cd kafka-ui-react-app/\n          pnpm install --frozen-lockfile\n      - name: Generate sources\n        run: |\n          cd kafka-ui-react-app/\n          pnpm gen:sources\n      - name: Linter\n        run: |\n          cd kafka-ui-react-app/\n          pnpm lint:CI\n      - name: Tests\n        run: |\n          cd kafka-ui-react-app/\n          pnpm test:CI\n      - name: SonarCloud Scan\n        uses: sonarsource/sonarcloud-github-action@master\n        with:\n          projectBaseDir: ./kafka-ui-react-app\n          args: -Dsonar.pullrequest.key=${{ github.event.pull_request.number }} -Dsonar.pullrequest.branch=${{ github.head_ref }} -Dsonar.pullrequest.base=${{ github.base_ref }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN_FRONTEND }}\n"
  },
  {
    "path": ".github/workflows/master.yaml",
    "content": "name: \"Master: Build & deploy\"\non:\n  workflow_dispatch:\n  push:\n    branches: [ \"master\" ]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          ref: ${{ github.event.pull_request.head.sha }}\n\n      - name: Set up JDK\n        uses: actions/setup-java@v3\n        with:\n          java-version: '17'\n          distribution: 'zulu'\n          cache: 'maven'\n\n      - name: Build\n        id: build\n        run: |\n          ./mvnw -B -ntp versions:set -DnewVersion=$GITHUB_SHA\n          ./mvnw -V -B -ntp clean package -Pprod -DskipTests\n          export VERSION=$(./mvnw -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec)\n          echo \"version=${VERSION}\" >> $GITHUB_OUTPUT\n#################\n#               #\n# Docker images #\n#               #\n#################\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v2\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v2\n\n      - name: Cache Docker layers\n        uses: actions/cache@v3\n        with:\n          path: /tmp/.buildx-cache\n          key: ${{ runner.os }}-buildx-${{ github.sha }}\n          restore-keys: |\n            ${{ runner.os }}-buildx-\n\n      - name: Login to DockerHub\n        uses: docker/login-action@v2\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Build and push\n        id: docker_build_and_push\n        uses: docker/build-push-action@v4\n        with:\n          builder: ${{ steps.buildx.outputs.name }}\n          context: kafka-ui-api\n          platforms: linux/amd64,linux/arm64\n          provenance: false\n          push: true\n          tags: |\n            provectuslabs/kafka-ui:${{ steps.build.outputs.version }}\n            provectuslabs/kafka-ui:master\n          build-args: |\n            JAR_FILE=kafka-ui-api-${{ steps.build.outputs.version }}.jar\n          cache-from: type=local,src=/tmp/.buildx-cache\n          cache-to: type=local,dest=/tmp/.buildx-cache\n#################################\n#                               #\n#   Master image digest update  #\n#                               #\n#################################\n      - name: update-master-deployment\n        run: |\n          git clone https://infra-tech:${{ secrets.INFRA_USER_ACCESS_TOKEN }}@github.com/provectus/kafka-ui-infra.git --branch master\n          cd kafka-ui-infra/aws-infrastructure4eks/argocd/scripts\n          echo \"Image digest is:${{ steps.docker_build_and_push.outputs.digest }}\"\n          ./kafka-ui-update-master-digest.sh ${{ steps.docker_build_and_push.outputs.digest }}\n          git config --global user.email \"infra-tech@provectus.com\"\n          git config --global user.name \"infra-tech\"\n          git add ../kafka-ui/*\n          git commit -m \"updated master image digest: ${{ steps.docker_build_and_push.outputs.digest }}\" && git push\n"
  },
  {
    "path": ".github/workflows/pr-checks.yaml",
    "content": "name: \"PR: Checklist linter\"\non:\n  pull_request_target:\n    types: [opened, edited, synchronize, reopened]\npermissions:\n  checks: write\njobs:\n  task-check:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: kentaro-m/task-completed-checker-action@v0.1.2\n        with:\n          repo-token: \"${{ secrets.GITHUB_TOKEN }}\"\n      - uses: dekinderfiets/pr-description-enforcer@0.0.1\n        with:\n          repo-token: \"${{ secrets.GITHUB_TOKEN }}\"\n"
  },
  {
    "path": ".github/workflows/release-serde-api.yaml",
    "content": "name: \"Infra: Release: Serde API\"\non: workflow_dispatch\n\njobs:\n  release-serde-api:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 0\n\n      - run: |\n          git config user.name github-actions\n          git config user.email github-actions@github.com\n\n      - name: Set up JDK\n        uses: actions/setup-java@v3\n        with:\n          java-version: \"17\"\n          distribution: \"zulu\"\n          cache: \"maven\"\n\n      - id: install-secret-key\n        name: Install GPG secret key\n        run: |\n          cat <(echo -e \"${{ secrets.GPG_PRIVATE_KEY }}\") | gpg --batch --import\n\n      - name: Publish to Maven Central\n        run: |\n          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\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "name: \"Infra: Release\"\non:\n  release:\n    types: [published]\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n    outputs:\n      version: ${{steps.build.outputs.version}}\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 0\n          ref: ${{ github.event.pull_request.head.sha }}\n\n      - run: |\n          git config user.name github-actions\n          git config user.email github-actions@github.com\n\n      - name: Set up JDK\n        uses: actions/setup-java@v3\n        with:\n          java-version: '17'\n          distribution: 'zulu'\n          cache: 'maven'\n\n      - name: Build with Maven\n        id: build\n        run: |\n          ./mvnw -B -ntp versions:set -DnewVersion=${{ github.event.release.tag_name }}\n          ./mvnw -B -V -ntp clean package -Pprod -DskipTests\n          export VERSION=$(./mvnw -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec)\n          echo \"version=${VERSION}\" >> $GITHUB_OUTPUT\n\n      - name: Upload files to a GitHub release\n        uses: svenstaro/upload-release-action@2.7.0\n        with:\n          repo_token: ${{ secrets.GITHUB_TOKEN }}\n          file: kafka-ui-api/target/kafka-ui-api-${{ steps.build.outputs.version }}.jar\n          tag: ${{ github.event.release.tag_name }}\n\n      - name: Archive JAR\n        uses: actions/upload-artifact@v3\n        with:\n          name: kafka-ui-${{ steps.build.outputs.version }}\n          path: kafka-ui-api/target/kafka-ui-api-${{ steps.build.outputs.version }}.jar\n#################\n#               #\n# Docker images #\n#               #\n#################\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v2\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v2\n\n      - name: Cache Docker layers\n        uses: actions/cache@v3\n        with:\n          path: /tmp/.buildx-cache\n          key: ${{ runner.os }}-buildx-${{ github.sha }}\n          restore-keys: |\n            ${{ runner.os }}-buildx-\n\n      - name: Login to DockerHub\n        uses: docker/login-action@v2\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Build and push\n        id: docker_build_and_push\n        uses: docker/build-push-action@v4\n        with:\n          builder: ${{ steps.buildx.outputs.name }}\n          context: kafka-ui-api\n          platforms: linux/amd64,linux/arm64\n          provenance: false\n          push: true\n          tags: |\n            provectuslabs/kafka-ui:${{ steps.build.outputs.version }}\n            provectuslabs/kafka-ui:latest\n          build-args: |\n            JAR_FILE=kafka-ui-api-${{ steps.build.outputs.version }}.jar\n          cache-from: type=local,src=/tmp/.buildx-cache\n          cache-to: type=local,dest=/tmp/.buildx-cache\n\n  charts:\n    runs-on: ubuntu-latest\n    needs: release\n    steps:\n      - name: Repository Dispatch\n        uses: peter-evans/repository-dispatch@v2\n        with:\n          token: ${{ secrets.CHARTS_ACTIONS_TOKEN }}\n          repository: provectus/kafka-ui-charts\n          event-type: prepare-helm-release\n          client-payload: '{\"appversion\": \"${{ needs.release.outputs.version }}\"}'\n"
  },
  {
    "path": ".github/workflows/release_drafter.yml",
    "content": "name: \"Infra: Release Drafter run\"\n\non:\n  push:\n    branches:\n      - master\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'Release version'\n        required: false\n      branch:\n        description: 'Target branch'\n        required: false\n        default: 'master'\n\npermissions:\n  contents: read\n\njobs:\n  update_release_draft:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n      pull-requests: write\n    steps:\n      - uses: release-drafter/release-drafter@v5\n        with:\n          config-name: release_drafter.yaml\n          disable-autolabeler: true\n          version: ${{ github.event.inputs.version }}\n          commitish: ${{ github.event.inputs.branch }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/separate_env_public_create.yml",
    "content": "name: \"Infra: Feature Testing Public: Init env\"\non:\n  workflow_dispatch:\n    inputs:\n      ENV_NAME:\n        description: 'Will be used as subdomain in the public URL.'\n        required: true\n        default: 'demo'\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          ref: ${{ github.event.pull_request.head.sha }}\n      - name: get branch name\n        id: extract_branch\n        run: |\n          tag=\"${{ github.event.inputs.ENV_NAME }}-$(date '+%F-%H-%M-%S')\"\n          echo \"tag=${tag}\" >> $GITHUB_OUTPUT\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      - name: Set up JDK\n        uses: actions/setup-java@v3\n        with:\n          java-version: '17'\n          distribution: 'zulu'\n          cache: 'maven'\n      - name: Build\n        id: build\n        run: |\n          ./mvnw -B -ntp versions:set -DnewVersion=$GITHUB_SHA\n          ./mvnw -B -V -ntp clean package -Pprod -DskipTests\n          export VERSION=$(./mvnw -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec)\n          echo \"version=${VERSION}\" >> $GITHUB_OUTPUT\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v2\n      - name: Set up Docker Buildx\n        id: buildx\n        uses: docker/setup-buildx-action@v2\n      - name: Cache Docker layers\n        uses: actions/cache@v3\n        with:\n          path: /tmp/.buildx-cache\n          key: ${{ runner.os }}-buildx-${{ github.sha }}\n          restore-keys: |\n            ${{ runner.os }}-buildx-\n      - name: Configure AWS credentials for Kafka-UI account\n        uses: aws-actions/configure-aws-credentials@v3\n        with:\n          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          aws-region: eu-central-1\n      - name: Login to Amazon ECR\n        id: login-ecr\n        uses: aws-actions/amazon-ecr-login@v1\n      - name: Build and push\n        id: docker_build_and_push\n        uses: docker/build-push-action@v4\n        with:\n          builder: ${{ steps.buildx.outputs.name }}\n          context: kafka-ui-api\n          push: true\n          tags: 297478128798.dkr.ecr.eu-central-1.amazonaws.com/kafka-ui:${{ steps.extract_branch.outputs.tag }}\n          build-args: |\n            JAR_FILE=kafka-ui-api-${{ steps.build.outputs.version }}.jar\n          cache-from: type=local,src=/tmp/.buildx-cache\n          cache-to: type=local,dest=/tmp/.buildx-cache\n    outputs:\n      tag: ${{ steps.extract_branch.outputs.tag }}\n\n  separate-env-create:\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - name: clone\n        run: |\n          git clone https://infra-tech:${{ secrets.INFRA_USER_ACCESS_TOKEN }}@github.com/provectus/kafka-ui-infra.git --branch envs\n\n      - name: separate env create\n        run: |\n          cd kafka-ui-infra/aws-infrastructure4eks/argocd/scripts\n          bash separate_env_create.sh ${{ github.event.inputs.ENV_NAME }} ${{ secrets.FEATURE_TESTING_UI_PASSWORD }} ${{ needs.build.outputs.tag }}\n          git config --global user.email \"infra-tech@provectus.com\"\n          git config --global user.name \"infra-tech\"\n          git add -A\n          git commit -m \"separate env added: ${{ github.event.inputs.ENV_NAME }}\" && git push || true\n\n      - name: echo separate environment public link\n        run: |\n          echo \"Please note, separate environment creation takes up to 5-10 minutes.\"\n          echo \"Separate environment will be available at http://${{ github.event.inputs.ENV_NAME }}.kafka-ui.provectus.io\"\n          echo \"Username: admin\"\n"
  },
  {
    "path": ".github/workflows/separate_env_public_remove.yml",
    "content": "name: \"Infra: Feature Testing Public: Destroy env\"\non:\n  workflow_dispatch:\n    inputs:\n      ENV_NAME:\n        description: 'Will be used to remove previously deployed separate environment.'\n        required: true\n        default: 'demo'\n\njobs:\n  separate-env-remove:\n    runs-on: ubuntu-latest\n    steps:\n      - name: clone\n        run: |\n          git clone https://infra-tech:${{ secrets.INFRA_USER_ACCESS_TOKEN }}@github.com/provectus/kafka-ui-infra.git --branch envs\n      - name: separate environment remove\n        run: |\n          cd kafka-ui-infra/aws-infrastructure4eks/argocd/scripts\n          bash separate_env_remove.sh ${{ github.event.inputs.ENV_NAME }}\n          git config --global user.email \"infra-tech@provectus.com\"\n          git config --global user.name \"infra-tech\"\n          git add -A\n          git commit -m \"separate env removed: ${{ github.event.inputs.ENV_NAME }}\" && git push || true\n"
  },
  {
    "path": ".github/workflows/stale.yaml",
    "content": "name: 'Infra: Close stale issues'\non:\n  schedule:\n    - cron: '30 1 * * *'\n\njobs:\n  stale:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/stale@v8\n        with:\n          days-before-issue-stale: 7\n          days-before-issue-close: 3\n          days-before-pr-stale: 7\n          days-before-pr-close: 7\n          stale-issue-message: 'This issue has been automatically marked as stale because no requested feedback has been provided. It will be closed if no further activity occurs. Thank you for your contributions.'\n          stale-pr-message: 'This PR has been automatically marked as stale because no requested changes have been applied. It will be closed if no further activity occurs. Thank you for your contributions.'\n          stale-issue-label: 'status/stale'\n          stale-pr-label: 'status/stale'\n          only-labels: 'status/pending'\n          remove-issue-stale-when-updated: true\n          labels-to-remove-when-unstale: 'status/pending'\n"
  },
  {
    "path": ".github/workflows/terraform-deploy.yml",
    "content": "name: \"Infra: Terraform deploy\"\non:\n  workflow_dispatch:\n    inputs:\n      applyTerraform:\n        description: 'Do you want to apply the infra-repo terraform? Possible values [plan/apply].'\n        required: true\n        default: 'plan'\n      KafkaUIInfraBranch:\n        description: 'Branch name of Kafka-UI-Infra repo, tf will be executed from this branch'\n        required: true\n        default: 'master'\n\njobs:\n  terraform:\n    name: Terraform\n    runs-on: ubuntu-latest\n    steps:\n      - name: Clone infra repo\n        run: |\n          echo \"Cloning repo...\"\n          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 }}\n          echo \"Cd to deployment...\"\n          cd kafka-ui-infra/aws-infrastructure4eks/deployment\n          echo \"TF_DIR=$(pwd)\" >> $GITHUB_ENV\n          echo \"Terraform will be triggered in this dir $TF_DIR\"\n\n      - name: Configure AWS credentials for Kafka-UI account\n        uses: aws-actions/configure-aws-credentials@v3\n        with:\n          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          aws-region: eu-central-1\n\n      - name: Terraform Install\n        uses: hashicorp/setup-terraform@v2\n\n      - name: Terraform init\n        id: init\n        run: cd $TF_DIR && terraform init --backend-config=\"../envs/pro/terraform-backend.tfvars\"\n\n      - name: Terraform validate\n        id: validate\n        run: cd $TF_DIR && terraform validate -no-color\n\n      - name: Terraform plan\n        id: plan\n        run: |\n          cd $TF_DIR\n          export TF_VAR_github_connector_access_token=${{ secrets.SOURCE_CONNECTOR_GITHUB_TOKEN }}\n          export TF_VAR_repo_secret=${{ secrets.KAFKA_UI_INFRA_TOKEN }}\n          terraform plan --var-file=\"../envs/pro/eks.tfvars\"\n\n      - name: Terraform apply\n        id: apply\n        if: ${{ github.event.inputs.applyTerraform == 'apply' }}\n        run: |\n          cd $TF_DIR\n          export TF_VAR_github_connector_access_token=${{ secrets.SOURCE_CONNECTOR_GITHUB_TOKEN }}\n          export TF_VAR_repo_secret=${{ secrets.KAFKA_UI_INFRA_TOKEN }}\n          terraform apply --var-file=\"../envs/pro/eks.tfvars\" -auto-approve\n"
  },
  {
    "path": ".github/workflows/triage_issues.yml",
    "content": "name: \"Infra: Triage: Apply triage label for issues\"\non:\n  issues:\n    types:\n      - opened\njobs:\n  triage_issues:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Label issue\n        uses: andymckay/labeler@master\n        with:\n          add-labels: \"status/triage\"\n          ignore-if-assigned: true\n"
  },
  {
    "path": ".github/workflows/triage_prs.yml",
    "content": "name: \"Infra: Triage: Apply triage label for PRs\"\non:\n  pull_request:\n    types:\n      - opened\njobs:\n  triage_prs:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Label PR\n        uses: andymckay/labeler@master\n        with:\n          add-labels: \"status/triage\"\n          ignore-if-labeled: true\n"
  },
  {
    "path": ".github/workflows/welcome-first-time-contributors.yml",
    "content": "name: Welcome first time contributors\n\non:\n  pull_request_target:\n    types:\n      - opened\n  issues:\n    types:\n      - opened\npermissions:\n  issues: write\n  pull-requests: write\njobs:\n  welcome:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/first-interaction@v1\n        with:\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n          issue-message: |\n            Hello there ${{ github.actor }}! 👋\n\n            Thank you and congratulations 🎉 for opening your very first issue in this project! 💖\n\n            In case you want to claim this issue, please comment down below! We will try to get back to you as soon as we can. 👀\n\n          pr-message: |\n            Hello there ${{ github.actor }}! 👋\n\n            Thank you and congrats 🎉 for opening your first PR on this project! ✨ 💖\n\n            We will try to review it soon!\n"
  },
  {
    "path": ".github/workflows/workflow_linter.yaml",
    "content": "name: \"Infra: Workflow linter\"\non:\n  pull_request:\n    types:\n      - \"opened\"\n      - \"reopened\"\n      - \"synchronize\"\n      - \"edited\"\n    paths:\n      - \".github/workflows/**\"\njobs:\n  build-and-test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 0\n          ref: ${{ github.event.pull_request.head.sha }}\n      - name: Install yamllint\n        run: sudo apt install -y yamllint\n      - name: Validate workflow yaml files\n        run: yamllint .github/workflows/. -d relaxed -f github --no-warnings\n"
  },
  {
    "path": ".gitignore",
    "content": "HELP.md\ntarget/\n!.mvn/wrapper/maven-wrapper.jar\n!**/src/main/**\n!**/src/test/**\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\nbuild/\n\n### VS Code ###\n.vscode/\n/kafka-ui-api/app/node\n\n### SDKMAN ###\n.sdkmanrc\n\n.DS_Store\n*.code-workspace\n\n\n*.tar.gz\n*.tgz\n\n/docker/*.override.yaml\n"
  },
  {
    "path": ".mvn/wrapper/maven-wrapper.properties",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\ndistributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip\nwrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar\n"
  },
  {
    "path": "CODE-OF-CONDUCT.md",
    "content": "\n# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, caste, color, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at email kafkaui@provectus.com.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\n[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].\n\nCommunity Impact Guidelines were inspired by \n[Mozilla's code of conduct enforcement ladder][Mozilla CoC].\n\nFor answers to common questions about this code of conduct, see the FAQ at\n[https://www.contributor-covenant.org/faq][FAQ]. Translations are available \nat [https://www.contributor-covenant.org/translations][translations].\n\n[homepage]: https://www.contributor-covenant.org\n[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html\n[Mozilla CoC]: https://github.com/mozilla/diversity\n[FAQ]: https://www.contributor-covenant.org/faq\n[translations]: https://www.contributor-covenant.org/translations\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "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.\n\nThis guide aims to walk you through the process of working on issues and Pull Requests (PRs).\n\nBear 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.\n\n# General recommendations\n\nPlease note that we have a code of conduct (`CODE-OF-CONDUCT.md`). Make sure that you follow it in all of your interactions with the project.\n\n# Issues\n\n## Choosing an issue\n\nThere are two options to look for the issues to contribute to. <br/>\nThe first is our [\"Up for grabs\"](https://github.com/provectus/kafka-ui/projects/11) board. There the issues are sorted by a required experience level (beginner, intermediate, expert).\n\nThe second option is to search for [\"good first issue\"](https://github.com/provectus/kafka-ui/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)-labeled issues. Some of them might not be displayed on the aforementioned board, or vice versa.\n\nYou also need to consider labels. You can sort the issues by scope labels, such as `scope/backend`, `scope/frontend` or even `scope/k8s`. If any issue covers several specific areas, and you do not have a required expertise for one of them, just do your part of work — others will do the rest.\n\n## Grabbing the issue\n\nThere is a bunch of criteria that make an issue feasible for development. <br/>\nThe 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.\n\nAll bugs should be confirmed as such (i.e. the behavior is unintended).\n\nAny issue should be properly triaged by the maintainers beforehand, which includes:\n1. Having a proper milestone set\n2. Having required labels assigned: accepted label, scope labels, etc.\n\nFormally, if these triage conditions are met, you can start to work on the issue.\n\nWith all these requirements met, feel free to pick the issue you want. Reach out to the maintainers if you have any questions.\n\n## Working on the issue\n\nEvery issue “in-progress” needs to be assigned to a corresponding person.\nTo keep the status of the issue clear to everyone, please keep the card's status updated (\"project\" card to the right of the issue should match the milestone’s name).\n\n## Setting up a local development environment\n\nPlease refer to [this guide](https://docs.kafka-ui.provectus.io/development/contributing).\n\n# Pull Requests\n\n## Branch naming\n\nIn order to keep branch names uniform and easy-to-understand, please use the following conventions for branch naming.\n\nGenerally speaking, it is a good idea to add a group/type prefix to a branch; e.g.,\nif you are working on a specific branch, you could name it `issues/xxx`.\n\nHere is a list of good examples:<br/>\n`issues/123`<br/>\n`feature/feature_name`<br/>\n`bugfix/fix_thing`<br/>\n\n## Code style\n\nJava: There is a file called `checkstyle.xml` in project root under `etc` directory.<br/>\nYou can import it into IntelliJ IDEA via Checkstyle plugin.\n\n## Naming conventions\n\nREST paths should be written in **lowercase** and consist of **plural** nouns only.<br/>\nAlso, multiple words that are placed in a single path segment should be divided by a hyphen (`-`).<br/>\n\nQuery variable names should be formatted in `camelCase`.\n\nModel names should consist of **plural** nouns only and should be formatted in `camelCase` as well.\n\n## Creating a PR\n\nWhen creating a PR please do the following:\n1. In commit messages use these [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword).\n2. Link an issue(-s) via \"linked issues\" block.\n3. Set the PR labels. Ensure that you set only the same set of labels that is present in the issue, and ignore yellow `status/` labels.\n4. 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.\n5. Assign the PR to yourself. A PR assignee is someone whose goal is to get the PR merged.\n6. Add reviewers. As a rule, reviewers' suggestions are pretty good; please use them.\n7. Upon merging the PR, please use a meaningful commit message, task name should be fine in this case.\n\n### Pull Request checklist\n\n1. When composing a build, ensure that any install or build dependencies have been removed before the end of the layer.\n2. Update the `README.md` with the details of changes made to the interface. This includes new environment variables, \nexposed ports, useful file locations, and container parameters.\n\n## Reviewing a PR\n\nWIP\n\n### Pull Request reviewer checklist\n\nWIP\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2020 CloudHut\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "README.md",
    "content": "![UI for Apache Kafka logo](documentation/images/kafka-ui-logo.png) UI for Apache Kafka&nbsp;\n------------------\n#### Versatile, fast and lightweight web UI for managing Apache Kafka® clusters. Built by developers, for developers.\n<br/>\n\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/provectus/kafka-ui/blob/master/LICENSE)\n![UI for Apache Kafka Price Free](documentation/images/free-open-source.svg)\n[![Release version](https://img.shields.io/github/v/release/provectus/kafka-ui)](https://github.com/provectus/kafka-ui/releases)\n[![Chat with us](https://img.shields.io/discord/897805035122077716)](https://discord.gg/4DWzD7pGE5)\n[![Docker pulls](https://img.shields.io/docker/pulls/provectuslabs/kafka-ui)](https://hub.docker.com/r/provectuslabs/kafka-ui)\n\n<p align=\"center\">\n    <a href=\"https://docs.kafka-ui.provectus.io/\">DOCS</a> • \n    <a href=\"https://docs.kafka-ui.provectus.io/configuration/quick-start\">QUICK START</a> • \n    <a href=\"https://discord.gg/4DWzD7pGE5\">COMMUNITY DISCORD</a>\n    <br/>\n    <a href=\"https://aws.amazon.com/marketplace/pp/prodview-ogtt5hfhzkq6a\">AWS Marketplace</a>  •\n    <a href=\"https://www.producthunt.com/products/ui-for-apache-kafka/reviews/new\">ProductHunt</a>\n</p>\n\n<p align=\"center\">\n  <img src=\"https://repobeats.axiom.co/api/embed/2e8a7c2d711af9daddd34f9791143e7554c35d0f.svg\" />\n</p>\n\n#### UI for Apache Kafka is a free, open-source web UI to monitor and manage Apache Kafka clusters.\n\nUI 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.\n\n### DISCLAIMER\n<em>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.\nLooking 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! </em>\n\nSet 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\nthe cloud.\n\n![Interface](documentation/images/Interface.gif)\n\n# Features\n* **Multi-Cluster Management** — monitor and manage all your clusters in one place\n* **Performance Monitoring with Metrics Dashboard** —  track key Kafka metrics with a lightweight dashboard\n* **View Kafka Brokers** — view topic and partition assignments, controller status\n* **View Kafka Topics** — view partition count, replication status, and custom configuration\n* **View Consumer Groups** — view per-partition parked offsets, combined and per-partition lag\n* **Browse Messages** — browse messages with JSON, plain text, and Avro encoding\n* **Dynamic Topic Configuration** — create and configure new topics with dynamic configuration\n* **Configurable Authentification** — [secure](https://docs.kafka-ui.provectus.io/configuration/authentication) your installation with optional Github/Gitlab/Google OAuth 2.0\n* **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!\n* **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\n* **Data masking** - [obfuscate](https://docs.kafka-ui.provectus.io/configuration/data-masking) sensitive data in topic messages\n\n# The Interface\nUI for Apache Kafka wraps major functions of Apache Kafka with an intuitive user interface.\n\n![Interface](documentation/images/Interface.gif)\n\n## Topics\nUI for Apache Kafka makes it easy for you to create topics in your browser by several clicks,\npasting your own parameters, and viewing topics in the list.\n\n![Create Topic](documentation/images/Create_topic_kafka-ui.gif)\n\nIt's possible to jump from connectors view to corresponding topics and from a topic to consumers (back and forth) for more convenient navigation.\nconnectors, overview topic settings.\n\n![Connector_Topic_Consumer](documentation/images/Connector_Topic_Consumer.gif)\n\n### Messages\nLet's say we want to produce messages for our topic. With the UI for Apache Kafka we can send or write data/messages to the Kafka topics without effort by specifying parameters, and viewing messages in the list.\n\n![Produce Message](documentation/images/Create_message_kafka-ui.gif)\n\n## Schema registry\nThere are 3 supported types of schemas: Avro®, JSON Schema, and Protobuf schemas.\n\n![Create Schema Registry](documentation/images/Create_schema.gif)\n\nBefore 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\nwith a few clicks in a user-friendly interface.\n\n![Avro Schema Topic](documentation/images/Schema_Topic.gif)\n\n# Getting Started\n\nTo run UI for Apache Kafka, you can use either a pre-built Docker image or build it (or a jar file) yourself.\n\n## Quick start (Demo run)\n\n```\ndocker run -it -p 8080:8080 -e DYNAMIC_CONFIG_ENABLED=true provectuslabs/kafka-ui\n```\n\nThen access the web UI at [http://localhost:8080](http://localhost:8080)\n\nThe 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)\n\n## Persistent installation\n\n```\nservices:\n  kafka-ui:\n    container_name: kafka-ui\n    image: provectuslabs/kafka-ui:latest\n    ports:\n      - 8080:8080\n    environment:\n      DYNAMIC_CONFIG_ENABLED: 'true'\n    volumes:\n      - ~/kui/config.yml:/etc/kafkaui/dynamic_config.yaml\n```\n\nPlease refer to our [configuration](https://docs.kafka-ui.provectus.io/configuration/quick-start) page to proceed with further app configuration.\n\n## Some useful configuration related links\n\n[Web UI Cluster Configuration Wizard](https://docs.kafka-ui.provectus.io/configuration/configuration-wizard)\n\n[Configuration file explanation](https://docs.kafka-ui.provectus.io/configuration/configuration-file)\n\n[Docker Compose examples](https://docs.kafka-ui.provectus.io/configuration/compose-examples)\n\n[Misc configuration properties](https://docs.kafka-ui.provectus.io/configuration/misc-configuration-properties)\n\n## Helm charts\n\n[Quick start](https://docs.kafka-ui.provectus.io/configuration/helm-charts/quick-start)\n\n## Building from sources\n\n[Quick start](https://docs.kafka-ui.provectus.io/development/building/prerequisites) with building\n\n## Liveliness and readiness probes\nLiveliness and readiness endpoint is at `/actuator/health`.<br/>\nInfo endpoint (build info) is located at `/actuator/info`.\n\n# Configuration options\n\nAll of the environment variables/config properties could be found [here](https://docs.kafka-ui.provectus.io/configuration/misc-configuration-properties).\n\n# Contributing\n\nPlease refer to [contributing guide](https://docs.kafka-ui.provectus.io/development/contributing), we'll guide you from there.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nFollowing versions of the project are currently being supported with security updates.\n\n| Version | Supported          |\n| ------- | ------------------ |\n| 0.7.x   | :white_check_mark: |\n| 0.6.x   | :x:                |\n| 0.5.x   | :x:                |\n| 0.4.x   | :x:                |\n| 0.3.x   | :x:                |\n| 0.2.x   | :x:                |\n| 0.1.x   | :x:                |\n\n## Reporting a Vulnerability\n\nPlease **DO NOT** file a publicly available github issues regarding security vulnerabilities.\nSend us details via email (maintainers.kafka-ui \"at\" provectus.com).\nConsider adding something like \"security vulnerability report\" in the title of an email.\n"
  },
  {
    "path": "documentation/compose/DOCKER_COMPOSE.md",
    "content": "# Descriptions of docker-compose configurations (*.yaml)\n\n1. [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.\n2. [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.\n3. [kafka-clusters-only.yaml](./kafka-clusters-only.yaml) - A configuration for development purposes, everything besides `kafka-ui` itself (to be run locally).\n4. [kafka-ui-ssl.yml](./kafka-ssl.yml) - Connect to Kafka via TLS/SSL\n5. [kafka-cluster-sr-auth.yaml](./kafka-cluster-sr-auth.yaml) - Schema registry with authentication.\n6. [kafka-ui-auth-context.yaml](./kafka-ui-auth-context.yaml) - Basic (username/password) authentication with custom path (URL) (issue 861).\n7. [e2e-tests.yaml](./e2e-tests.yaml) - Configuration with different connectors (github-source, s3, sink-activities, source-activities) and Ksql functionality.\n8. [kafka-ui-jmx-secured.yml](./kafka-ui-jmx-secured.yml) - Kafka’s JMX with SSL and authentication.\n9. [kafka-ui-reverse-proxy.yaml](./nginx-proxy.yaml) - An example for using the app behind a proxy (like nginx).\n10. [kafka-ui-sasl.yaml](./kafka-ui-sasl.yaml) - SASL auth for Kafka.\n11. [kafka-ui-traefik-proxy.yaml](./traefik-proxy.yaml) - Traefik specific proxy configuration.\n12. [oauth-cognito.yaml](./oauth-cognito.yaml) - OAuth2 with Cognito\n13. [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.\n14. [kafka-with-zookeeper.yaml](./kafka-with-zookeeper.yaml) - An example for using kafka with zookeeper"
  },
  {
    "path": "documentation/compose/connectors/github-source.json",
    "content": "{\n  \"name\": \"github-source\",\n  \"config\":\n  {\n    \"connector.class\": \"io.confluent.connect.github.GithubSourceConnector\",\n    \"confluent.topic.bootstrap.servers\": \"kafka0:29092, kafka1:29092\",\n    \"confluent.topic.replication.factor\": \"1\",\n    \"tasks.max\": \"1\",\n    \"github.service.url\": \"https://api.github.com\",\n    \"github.access.token\": \"\",\n    \"github.repositories\": \"provectus/kafka-ui\",\n    \"github.resources\": \"issues,commits,pull_requests\",\n    \"github.since\": \"2019-01-01\",\n    \"topic.name.pattern\": \"github-${resourceName}\",\n    \"key.converter\": \"org.apache.kafka.connect.json.JsonConverter\",\n    \"key.converter.schema.registry.url\": \"http://schemaregistry0:8085\",\n    \"value.converter\": \"org.apache.kafka.connect.json.JsonConverter\",\n    \"value.converter.schema.registry.url\": \"http://schemaregistry0:8085\"\n  }\n}"
  },
  {
    "path": "documentation/compose/connectors/s3-sink.json",
    "content": "{\n  \"name\": \"s3-sink\",\n  \"config\":\n  {\n    \"connector.class\": \"io.confluent.connect.s3.S3SinkConnector\",\n    \"topics\": \"github-issues, github-pull_requests, github-commits\",\n    \"tasks.max\": \"1\",\n    \"s3.region\": \"eu-central-1\",\n    \"s3.bucket.name\": \"kafka-ui-s3-sink-connector\",\n    \"s3.part.size\": \"5242880\",\n    \"flush.size\": \"3\",\n    \"storage.class\": \"io.confluent.connect.s3.storage.S3Storage\",\n    \"format.class\": \"io.confluent.connect.s3.format.json.JsonFormat\",\n    \"schema.generator.class\": \"io.confluent.connect.storage.hive.schema.DefaultSchemaGenerator\",\n    \"partitioner.class\": \"io.confluent.connect.storage.partitioner.DefaultPartitioner\",\n    \"schema.compatibility\": \"BACKWARD\"\n  }\n}"
  },
  {
    "path": "documentation/compose/connectors/sink-activities.json",
    "content": "{\n  \"name\": \"sink_postgres_activities\",\n  \"config\": {\n    \"connector.class\": \"io.confluent.connect.jdbc.JdbcSinkConnector\",\n    \"connection.url\": \"jdbc:postgresql://postgres-db:5432/test\",\n    \"connection.user\": \"dev_user\",\n    \"connection.password\": \"12345\",\n    \"topics\": \"source-activities\",\n    \"table.name.format\": \"sink_activities\",\n    \"key.converter\": \"org.apache.kafka.connect.storage.StringConverter\",\n    \"key.converter.schema.registry.url\": \"http://schemaregistry0:8085\",\n    \"value.converter\": \"io.confluent.connect.avro.AvroConverter\",\n    \"value.converter.schema.registry.url\": \"http://schemaregistry0:8085\",\n    \"auto.create\": \"true\",\n    \"pk.mode\": \"record_value\",\n    \"pk.fields\": \"id\",\n    \"insert.mode\": \"upsert\"\n  }\n}"
  },
  {
    "path": "documentation/compose/connectors/source-activities.json",
    "content": "{\n  \"name\": \"source_postgres_activities\",\n  \"config\": {\n    \"connector.class\": \"io.confluent.connect.jdbc.JdbcSourceConnector\",\n    \"connection.url\": \"jdbc:postgresql://postgres-db:5432/test\",\n    \"connection.user\": \"dev_user\",\n    \"connection.password\": \"12345\",\n    \"topic.prefix\": \"source-\",\n    \"poll.interval.ms\": 3600000,\n    \"table.whitelist\": \"public.activities\",\n    \"mode\": \"bulk\",\n    \"transforms\": \"extractkey\",\n    \"transforms.extractkey.type\": \"org.apache.kafka.connect.transforms.ExtractField$Key\",\n    \"transforms.extractkey.field\": \"id\",\n    \"key.converter\": \"org.apache.kafka.connect.storage.StringConverter\",\n    \"key.converter.schema.registry.url\": \"http://schemaregistry0:8085\",\n    \"value.converter\": \"io.confluent.connect.avro.AvroConverter\",\n    \"value.converter.schema.registry.url\": \"http://schemaregistry0:8085\"\n  }\n}"
  },
  {
    "path": "documentation/compose/connectors/start.sh",
    "content": "#! /bin/bash\nwhile [[ \"$(curl -s -o /dev/null -w ''%{http_code}'' kafka-connect0:8083)\" != \"200\" ]]\n    do sleep 5\ndone\n\necho \"\\n --------------Creating connectors...\"\nfor filename in /connectors/*.json; do\n  curl -X POST -H \"Content-Type: application/json\" -d @$filename http://kafka-connect0:8083/connectors\ndone\n"
  },
  {
    "path": "documentation/compose/data/message.json",
    "content": "{}"
  },
  {
    "path": "documentation/compose/data/proxy.conf",
    "content": "server {\n    listen       80;\n    server_name  localhost;\n\n    location /kafka-ui {\n#        rewrite /kafka-ui/(.*) /$1  break;\n        proxy_pass   http://kafka-ui:8080;\n    }\n}\n"
  },
  {
    "path": "documentation/compose/e2e-tests.yaml",
    "content": "---\nversion: '3.5'\nservices:\n\n  kafka-ui:\n    container_name: kafka-ui\n    image: provectuslabs/kafka-ui:latest\n    ports:\n      - 8080:8080\n    healthcheck:\n      test: wget --no-verbose --tries=1 --spider  http://localhost:8080/actuator/health\n      interval: 30s\n      timeout: 10s\n      retries: 10\n    depends_on:\n      kafka0:\n        condition: service_healthy\n      schemaregistry0:\n        condition: service_healthy\n      kafka-connect0:\n        condition: service_healthy\n    environment:\n      KAFKA_CLUSTERS_0_NAME: local\n      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092\n      KAFKA_CLUSTERS_0_METRICS_PORT: 9997\n      KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schemaregistry0:8085\n      KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME: first\n      KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS: http://kafka-connect0:8083\n      KAFKA_CLUSTERS_0_KSQLDBSERVER: http://ksqldb:8088\n\n  kafka0:\n    image: confluentinc/cp-kafka:7.2.1\n    hostname: kafka0\n    container_name: kafka0\n    healthcheck:\n      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\n      interval: 30s\n      timeout: 10s\n      retries: 10\n    ports:\n      - \"9092:9092\"\n      - \"9997:9997\"\n    environment:\n      KAFKA_BROKER_ID: 1\n      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'\n      KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092'\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0\n      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1\n      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1\n      KAFKA_JMX_PORT: 9997\n      KAFKA_JMX_HOSTNAME: localhost\n      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\n      KAFKA_PROCESS_ROLES: 'broker,controller'\n      KAFKA_NODE_ID: 1\n      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093'\n      KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092'\n      KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'\n      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'\n      KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'\n    volumes:\n      - ./scripts/update_run.sh:/tmp/update_run.sh\n    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'\"\n\n  schemaregistry0:\n    image: confluentinc/cp-schema-registry:7.2.1\n    ports:\n      - 8085:8085\n    depends_on:\n      kafka0:\n        condition: service_healthy\n    healthcheck:\n      test: [ \"CMD\", \"timeout\", \"1\", \"curl\", \"--silent\", \"--fail\", \"http://schemaregistry0:8085/subjects\" ]\n      interval: 30s\n      timeout: 10s\n      retries: 10\n    environment:\n      SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092\n      SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT\n      SCHEMA_REGISTRY_HOST_NAME: schemaregistry0\n      SCHEMA_REGISTRY_LISTENERS: http://schemaregistry0:8085\n\n      SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: \"http\"\n      SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO\n      SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas\n\n  kafka-connect0:\n    build:\n      context: ./kafka-connect\n      args:\n        image: confluentinc/cp-kafka-connect:6.0.1\n    ports:\n      - 8083:8083\n    depends_on:\n      kafka0:\n        condition: service_healthy\n      schemaregistry0:\n        condition: service_healthy\n    healthcheck:\n      test: [ \"CMD\", \"nc\", \"127.0.0.1\", \"8083\" ]\n      interval: 30s\n      timeout: 10s\n      retries: 10\n    environment:\n      CONNECT_BOOTSTRAP_SERVERS: kafka0:29092\n      CONNECT_GROUP_ID: compose-connect-group\n      CONNECT_CONFIG_STORAGE_TOPIC: _connect_configs\n      CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR: 1\n      CONNECT_OFFSET_STORAGE_TOPIC: _connect_offset\n      CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR: 1\n      CONNECT_STATUS_STORAGE_TOPIC: _connect_status\n      CONNECT_STATUS_STORAGE_REPLICATION_FACTOR: 1\n      CONNECT_KEY_CONVERTER: org.apache.kafka.connect.storage.StringConverter\n      CONNECT_KEY_CONVERTER_SCHEMA_REGISTRY_URL: http://schemaregistry0:8085\n      CONNECT_VALUE_CONVERTER: org.apache.kafka.connect.storage.StringConverter\n      CONNECT_VALUE_CONVERTER_SCHEMA_REGISTRY_URL: http://schemaregistry0:8085\n      CONNECT_INTERNAL_KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter\n      CONNECT_INTERNAL_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter\n      CONNECT_REST_ADVERTISED_HOST_NAME: kafka-connect0\n      CONNECT_PLUGIN_PATH: \"/usr/share/java,/usr/share/confluent-hub-components\"\n  #      AWS_ACCESS_KEY_ID: \"\"\n  #      AWS_SECRET_ACCESS_KEY: \"\"\n\n  kafka-init-topics:\n    image: confluentinc/cp-kafka:7.2.1\n    volumes:\n      - ./data/message.json:/data/message.json\n    depends_on:\n      kafka0:\n        condition: service_healthy\n    command: \"bash -c 'echo Waiting for Kafka to be ready... && \\\n               cub kafka-ready -b kafka0:29092 1 30 && \\\n               kafka-topics --create --topic users --partitions 3 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \\\n               kafka-topics --create --topic messages --partitions 2 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \\\n               kafka-console-producer --bootstrap-server kafka0:29092 --topic users < /data/message.json'\"\n\n  postgres-db:\n    build:\n      context: ./postgres\n      args:\n        image: postgres:9.6.22\n    ports:\n      - 5432:5432\n    healthcheck:\n      test: [ \"CMD-SHELL\", \"pg_isready -U dev_user\" ]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    environment:\n      POSTGRES_USER: 'dev_user'\n      POSTGRES_PASSWORD: '12345'\n\n  create-connectors:\n    image: ellerbrock/alpine-bash-curl-ssl\n    depends_on:\n      postgres-db:\n        condition: service_healthy\n      kafka-connect0:\n        condition: service_healthy\n    volumes:\n      - ./connectors:/connectors\n    command: bash -c '/connectors/start.sh'\n\n  ksqldb:\n    image: confluentinc/ksqldb-server:0.18.0\n    healthcheck:\n      test: [ \"CMD\", \"timeout\", \"1\", \"curl\", \"--silent\", \"--fail\", \"http://localhost:8088/info\" ]\n      interval: 30s\n      timeout: 10s\n      retries: 10\n    depends_on:\n      kafka0:\n        condition: service_healthy\n      kafka-connect0:\n        condition: service_healthy\n      schemaregistry0:\n        condition: service_healthy\n    ports:\n      - 8088:8088\n    environment:\n      KSQL_CUB_KAFKA_TIMEOUT: 120\n      KSQL_LISTENERS: http://0.0.0.0:8088\n      KSQL_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092\n      KSQL_KSQL_LOGGING_PROCESSING_STREAM_AUTO_CREATE: \"true\"\n      KSQL_KSQL_LOGGING_PROCESSING_TOPIC_AUTO_CREATE: \"true\"\n      KSQL_KSQL_CONNECT_URL: http://kafka-connect0:8083\n      KSQL_KSQL_SCHEMA_REGISTRY_URL: http://schemaregistry0:8085\n      KSQL_KSQL_SERVICE_ID: my_ksql_1\n      KSQL_KSQL_HIDDEN_TOPICS: '^_.*'\n      KSQL_CACHE_MAX_BYTES_BUFFERING: 0\n"
  },
  {
    "path": "documentation/compose/jaas/client.properties",
    "content": "sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required username=\"admin\" password=\"admin-secret\";\nsecurity.protocol=SASL_PLAINTEXT\nsasl.mechanism=PLAIN"
  },
  {
    "path": "documentation/compose/jaas/kafka_connect.jaas",
    "content": "KafkaConnect {\n  org.apache.kafka.connect.rest.basic.auth.extension.PropertyFileLoginModule required\n  file=\"/conf/kafka_connect.password\";\n};\n"
  },
  {
    "path": "documentation/compose/jaas/kafka_connect.password",
    "content": "admin: admin-secret\n"
  },
  {
    "path": "documentation/compose/jaas/kafka_server.conf",
    "content": "KafkaServer {\n    org.apache.kafka.common.security.plain.PlainLoginModule required\n    username=\"admin\"\n    password=\"admin-secret\"\n    user_admin=\"admin-secret\"\n    user_enzo=\"cisternino\";\n};\n\nKafkaClient {\n    org.apache.kafka.common.security.plain.PlainLoginModule required\n    user_admin=\"admin-secret\";\n};\n\nClient {\n       org.apache.zookeeper.server.auth.DigestLoginModule required\n       username=\"zkuser\"\n       password=\"zkuserpassword\";\n};\n"
  },
  {
    "path": "documentation/compose/jaas/schema_registry.jaas",
    "content": "SchemaRegistryProps {\n  org.eclipse.jetty.jaas.spi.PropertyFileLoginModule required\n  file=\"/conf/schema_registry.password\"\n  debug=\"false\";\n};\n"
  },
  {
    "path": "documentation/compose/jaas/schema_registry.password",
    "content": "admin: OBF:1w8t1tvf1w261w8v1w1c1tvn1w8x,admin"
  },
  {
    "path": "documentation/compose/jaas/zookeeper_jaas.conf",
    "content": "Server {\n       org.apache.zookeeper.server.auth.DigestLoginModule required\n       user_zkuser=\"zkuserpassword\";\n};\n"
  },
  {
    "path": "documentation/compose/jmx/jmxremote.access",
    "content": "root readwrite\n"
  },
  {
    "path": "documentation/compose/jmx/jmxremote.password",
    "content": "root password\n"
  },
  {
    "path": "documentation/compose/jmx-exporter/kafka-broker.yml",
    "content": "rules:\n  - pattern: \".*\"\n"
  },
  {
    "path": "documentation/compose/jmx-exporter/kafka-prepare-and-run",
    "content": "#!/usr/bin/env bash\n\nJAVA_AGENT_FILE=\"/usr/share/jmx_exporter/jmx_prometheus_javaagent.jar\"\nif [ ! -f \"$JAVA_AGENT_FILE\" ]\nthen\n  echo \"Downloading jmx_exporter javaagent\"\n  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\nfi\n\nexec /etc/confluent/docker/run"
  },
  {
    "path": "documentation/compose/kafka-cluster-sr-auth.yaml",
    "content": "---\nversion: '2'\nservices:\n\n  kafka1:\n    image: confluentinc/cp-kafka:7.2.1\n    hostname: kafka1\n    container_name: kafka1\n    ports:\n      - \"9092:9092\"\n      - \"9997:9997\"\n    environment:\n      KAFKA_BROKER_ID: 1\n      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'\n      KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka1:29092,PLAINTEXT_HOST://localhost:9092'\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0\n      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1\n      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1\n      KAFKA_JMX_PORT: 9997\n      KAFKA_JMX_HOSTNAME: localhost\n      KAFKA_PROCESS_ROLES: 'broker,controller'\n      KAFKA_NODE_ID: 1\n      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka1:29093'\n      KAFKA_LISTENERS: 'PLAINTEXT://kafka1:29092,CONTROLLER://kafka1:29093,PLAINTEXT_HOST://0.0.0.0:9092'\n      KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'\n      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'\n      KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'\n    volumes:\n      - ./scripts/update_run.sh:/tmp/update_run.sh\n    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'\"\n\n  schemaregistry1:\n    image: confluentinc/cp-schema-registry:7.2.1\n    ports:\n      - 18085:8085\n    depends_on:\n      - kafka1\n    volumes:\n      - ./jaas:/conf\n    environment:\n      SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka1:29092\n      SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT\n      SCHEMA_REGISTRY_HOST_NAME: schemaregistry1\n      SCHEMA_REGISTRY_LISTENERS: http://schemaregistry1:8085\n\n      # Default credentials: admin/letmein\n      SCHEMA_REGISTRY_AUTHENTICATION_METHOD: BASIC\n      SCHEMA_REGISTRY_AUTHENTICATION_REALM: SchemaRegistryProps\n      SCHEMA_REGISTRY_AUTHENTICATION_ROLES: admin\n      SCHEMA_REGISTRY_OPTS: -Djava.security.auth.login.config=/conf/schema_registry.jaas\n\n      SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: \"http\"\n      SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO\n      SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas\n\n  kafka-init-topics:\n    image: confluentinc/cp-kafka:7.2.1\n    volumes:\n       - ./data/message.json:/data/message.json\n    depends_on:\n      - kafka1\n    command: \"bash -c 'echo Waiting for Kafka to be ready... && \\\n               cub kafka-ready -b kafka1:29092 1 30 && \\\n               kafka-topics --create --topic users --partitions 3 --replication-factor 1 --if-not-exists --bootstrap-server kafka1:29092 && \\\n               kafka-topics --create --topic messages --partitions 2 --replication-factor 1 --if-not-exists --bootstrap-server kafka1:29092 && \\\n               kafka-console-producer --bootstrap-server kafka1:29092 --topic users < /data/message.json'\"\n\n  kafka-ui:\n    container_name: kafka-ui\n    image: provectuslabs/kafka-ui:latest\n    ports:\n      - 8080:8080\n    depends_on:\n      - kafka1\n      - schemaregistry1\n    environment:\n      KAFKA_CLUSTERS_0_NAME: local\n      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka1:29092\n      KAFKA_CLUSTERS_0_METRICS_PORT: 9997\n      KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schemaregistry1:8085\n      KAFKA_CLUSTERS_0_SCHEMAREGISTRYAUTH_USERNAME: admin\n      KAFKA_CLUSTERS_0_SCHEMAREGISTRYAUTH_PASSWORD: letmein\n"
  },
  {
    "path": "documentation/compose/kafka-connect/Dockerfile",
    "content": "ARG image\nFROM ${image}\n\n## Install connectors\nRUN echo \"\\nInstalling all required connectors...\\n\" && \\\nconfluent-hub install --no-prompt confluentinc/kafka-connect-jdbc:latest && \\\nconfluent-hub install --no-prompt confluentinc/kafka-connect-github:latest && \\\nconfluent-hub install --no-prompt confluentinc/kafka-connect-s3:latest"
  },
  {
    "path": "documentation/compose/kafka-ssl-components.yaml",
    "content": "---\nversion: '3.4'\nservices:\n  kafka-ui:\n    container_name: kafka-ui\n    image: provectuslabs/kafka-ui:latest\n    ports:\n      - 8080:8080\n    depends_on:\n      - kafka0\n      - schemaregistry0\n      - kafka-connect0\n      - ksqldb0\n    environment:\n      KAFKA_CLUSTERS_0_NAME: local\n      KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL: SSL\n      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 # SSL LISTENER!\n      KAFKA_CLUSTERS_0_PROPERTIES_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: '' # DISABLE COMMON NAME VERIFICATION\n\n      KAFKA_CLUSTERS_0_SCHEMAREGISTRY: https://schemaregistry0:8085\n      KAFKA_CLUSTERS_0_SCHEMAREGISTRYSSL_KEYSTORELOCATION: /kafka.keystore.jks\n      KAFKA_CLUSTERS_0_SCHEMAREGISTRYSSL_KEYSTOREPASSWORD: \"secret\"\n\n      KAFKA_CLUSTERS_0_KSQLDBSERVER: https://ksqldb0:8088\n      KAFKA_CLUSTERS_0_KSQLDBSERVERSSL_KEYSTORELOCATION: /kafka.keystore.jks\n      KAFKA_CLUSTERS_0_KSQLDBSERVERSSL_KEYSTOREPASSWORD: \"secret\"\n\n      KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME: local\n      KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS: https://kafka-connect0:8083\n      KAFKA_CLUSTERS_0_KAFKACONNECT_0_KEYSTORELOCATION: /kafka.keystore.jks\n      KAFKA_CLUSTERS_0_KAFKACONNECT_0_KEYSTOREPASSWORD: \"secret\"\n\n      KAFKA_CLUSTERS_0_SSL_TRUSTSTORELOCATION: /kafka.truststore.jks\n      KAFKA_CLUSTERS_0_SSL_TRUSTSTOREPASSWORD: \"secret\"\n      DYNAMIC_CONFIG_ENABLED: 'true'  # not necessary for ssl, added for tests\n\n    volumes:\n      - ./ssl/kafka.truststore.jks:/kafka.truststore.jks\n      - ./ssl/kafka.keystore.jks:/kafka.keystore.jks\n\n  kafka0:\n    image: confluentinc/cp-kafka:7.2.1\n    hostname: kafka0\n    container_name: kafka0\n    ports:\n      - \"9092:9092\"\n      - \"9997:9997\"\n    environment:\n      KAFKA_BROKER_ID: 1\n      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,SSL:SSL,PLAINTEXT_HOST:PLAINTEXT'\n      KAFKA_ADVERTISED_LISTENERS: 'SSL://kafka0:29092,PLAINTEXT_HOST://localhost:9092'\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0\n      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1\n      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1\n      KAFKA_JMX_PORT: 9997\n      KAFKA_JMX_HOSTNAME: localhost\n      KAFKA_PROCESS_ROLES: 'broker,controller'\n      KAFKA_NODE_ID: 1\n      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093'\n      KAFKA_LISTENERS: 'SSL://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092'\n      KAFKA_INTER_BROKER_LISTENER_NAME: 'SSL'\n      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'\n      KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'\n      KAFKA_SECURITY_PROTOCOL: SSL\n      KAFKA_SSL_ENABLED_MECHANISMS: PLAIN,SSL\n      KAFKA_SSL_KEYSTORE_FILENAME: kafka.keystore.jks\n      KAFKA_SSL_KEYSTORE_CREDENTIALS: creds\n      KAFKA_SSL_KEY_CREDENTIALS: creds\n      KAFKA_SSL_TRUSTSTORE_FILENAME: kafka.truststore.jks\n      KAFKA_SSL_TRUSTSTORE_CREDENTIALS: creds\n      #KAFKA_SSL_CLIENT_AUTH: 'required'\n      KAFKA_SSL_CLIENT_AUTH: 'requested'\n      KAFKA_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: '' # COMMON NAME VERIFICATION IS DISABLED SERVER-SIDE\n    volumes:\n      - ./scripts/update_run.sh:/tmp/update_run.sh\n      - ./ssl/creds:/etc/kafka/secrets/creds\n      - ./ssl/kafka.truststore.jks:/etc/kafka/secrets/kafka.truststore.jks\n      - ./ssl/kafka.keystore.jks:/etc/kafka/secrets/kafka.keystore.jks\n    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'\"\n\n  schemaregistry0:\n    image: confluentinc/cp-schema-registry:7.2.1\n    depends_on:\n      - kafka0\n    environment:\n      SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: SSL://kafka0:29092\n      SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: SSL\n      SCHEMA_REGISTRY_KAFKASTORE_SSL_TRUSTSTORE_LOCATION: /kafka.truststore.jks\n      SCHEMA_REGISTRY_KAFKASTORE_SSL_TRUSTSTORE_PASSWORD: secret\n      SCHEMA_REGISTRY_KAFKASTORE_SSL_KEYSTORE_LOCATION: /kafka.keystore.jks\n      SCHEMA_REGISTRY_KAFKASTORE_SSL_KEYSTORE_PASSWORD: secret\n      SCHEMA_REGISTRY_KAFKASTORE_SSL_KEY_PASSWORD: secret\n      SCHEMA_REGISTRY_HOST_NAME: schemaregistry0\n      SCHEMA_REGISTRY_LISTENERS: https://schemaregistry0:8085\n      SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: https\n\n      SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: \"https\"\n      SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO\n      SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas\n      SCHEMA_REGISTRY_SSL_CLIENT_AUTHENTICATION: \"REQUIRED\"\n      SCHEMA_REGISTRY_SSL_TRUSTSTORE_LOCATION: /kafka.truststore.jks\n      SCHEMA_REGISTRY_SSL_TRUSTSTORE_PASSWORD: secret\n      SCHEMA_REGISTRY_SSL_KEYSTORE_LOCATION: /kafka.keystore.jks\n      SCHEMA_REGISTRY_SSL_KEYSTORE_PASSWORD: secret\n      SCHEMA_REGISTRY_SSL_KEY_PASSWORD: secret\n    ports:\n      - 8085:8085\n    volumes:\n      - ./ssl/kafka.truststore.jks:/kafka.truststore.jks\n      - ./ssl/kafka.keystore.jks:/kafka.keystore.jks\n\n  kafka-connect0:\n    image: confluentinc/cp-kafka-connect:7.2.1\n    ports:\n      - 8083:8083\n    depends_on:\n      - kafka0\n      - schemaregistry0\n    environment:\n      CONNECT_BOOTSTRAP_SERVERS: kafka0:29092\n      CONNECT_GROUP_ID: compose-connect-group\n      CONNECT_CONFIG_STORAGE_TOPIC: _connect_configs\n      CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR: 1\n      CONNECT_OFFSET_STORAGE_TOPIC: _connect_offset\n      CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR: 1\n      CONNECT_STATUS_STORAGE_TOPIC: _connect_status\n      CONNECT_STATUS_STORAGE_REPLICATION_FACTOR: 1\n      CONNECT_KEY_CONVERTER: org.apache.kafka.connect.storage.StringConverter\n      CONNECT_KEY_CONVERTER_SCHEMA_REGISTRY_URL: https://schemaregistry0:8085\n      CONNECT_VALUE_CONVERTER: org.apache.kafka.connect.storage.StringConverter\n      CONNECT_VALUE_CONVERTER_SCHEMA_REGISTRY_URL: https://schemaregistry0:8085\n      CONNECT_INTERNAL_KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter\n      CONNECT_INTERNAL_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter\n      CONNECT_REST_ADVERTISED_HOST_NAME: kafka-connect0\n      CONNECT_PLUGIN_PATH: \"/usr/share/java,/usr/share/confluent-hub-components\"\n      CONNECT_SECURITY_PROTOCOL: \"SSL\"\n      CONNECT_SSL_KEYSTORE_LOCATION: \"/kafka.keystore.jks\"\n      CONNECT_SSL_KEY_PASSWORD: \"secret\"\n      CONNECT_SSL_KEYSTORE_PASSWORD: \"secret\"\n      CONNECT_SSL_TRUSTSTORE_LOCATION: \"/kafka.truststore.jks\"\n      CONNECT_SSL_TRUSTSTORE_PASSWORD: \"secret\"\n      CONNECT_SSL_CLIENT_AUTH: \"requested\"\n      CONNECT_REST_ADVERTISED_LISTENER: \"https\"\n      CONNECT_LISTENERS: \"https://kafka-connect0:8083\"\n    volumes:\n      - ./ssl/kafka.truststore.jks:/kafka.truststore.jks\n      - ./ssl/kafka.keystore.jks:/kafka.keystore.jks\n\n  ksqldb0:\n    image: confluentinc/ksqldb-server:0.18.0\n    depends_on:\n      - kafka0\n      - kafka-connect0\n      - schemaregistry0\n    ports:\n      - 8088:8088\n    environment:\n      KSQL_CUB_KAFKA_TIMEOUT: 120\n      KSQL_LISTENERS: https://0.0.0.0:8088\n      KSQL_BOOTSTRAP_SERVERS: SSL://kafka0:29092\n      KSQL_SECURITY_PROTOCOL: SSL\n      KSQL_SSL_TRUSTSTORE_LOCATION: /kafka.truststore.jks\n      KSQL_SSL_TRUSTSTORE_PASSWORD: secret\n      KSQL_SSL_KEYSTORE_LOCATION: /kafka.keystore.jks\n      KSQL_SSL_KEYSTORE_PASSWORD: secret\n      KSQL_SSL_KEY_PASSWORD: secret\n      KSQL_SSL_CLIENT_AUTHENTICATION: REQUIRED\n      KSQL_KSQL_LOGGING_PROCESSING_STREAM_AUTO_CREATE: \"true\"\n      KSQL_KSQL_LOGGING_PROCESSING_TOPIC_AUTO_CREATE: \"true\"\n      KSQL_KSQL_CONNECT_URL: https://kafka-connect0:8083\n      KSQL_KSQL_SCHEMA_REGISTRY_URL: https://schemaregistry0:8085\n      KSQL_KSQL_SERVICE_ID: my_ksql_1\n      KSQL_KSQL_HIDDEN_TOPICS: '^_.*'\n      KSQL_CACHE_MAX_BYTES_BUFFERING: 0\n    volumes:\n      - ./ssl/kafka.truststore.jks:/kafka.truststore.jks\n      - ./ssl/kafka.keystore.jks:/kafka.keystore.jks\n"
  },
  {
    "path": "documentation/compose/kafka-ssl.yml",
    "content": "---\nversion: '3.4'\nservices:\n  kafka-ui:\n    container_name: kafka-ui\n    image: provectuslabs/kafka-ui:latest\n    ports:\n      - 8080:8080\n    depends_on:\n      - kafka\n    environment:\n      KAFKA_CLUSTERS_0_NAME: local\n      KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL: SSL\n      KAFKA_CLUSTERS_0_PROPERTIES_SSL_KEYSTORE_LOCATION: /kafka.keystore.jks\n      KAFKA_CLUSTERS_0_PROPERTIES_SSL_KEYSTORE_PASSWORD: \"secret\"\n      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092 # SSL LISTENER!\n      KAFKA_CLUSTERS_0_SSL_TRUSTSTORELOCATION: /kafka.truststore.jks\n      KAFKA_CLUSTERS_0_SSL_TRUSTSTOREPASSWORD: \"secret\"\n      KAFKA_CLUSTERS_0_PROPERTIES_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: '' # DISABLE COMMON NAME VERIFICATION\n    volumes:\n      - ./ssl/kafka.truststore.jks:/kafka.truststore.jks\n      - ./ssl/kafka.keystore.jks:/kafka.keystore.jks\n\n  kafka:\n    image: confluentinc/cp-kafka:7.2.1\n    hostname: kafka\n    container_name: kafka\n    ports:\n      - \"9092:9092\"\n      - \"9997:9997\"\n    environment:\n      KAFKA_BROKER_ID: 1\n      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,SSL:SSL,PLAINTEXT_HOST:PLAINTEXT'\n      KAFKA_ADVERTISED_LISTENERS: 'SSL://kafka:29092,PLAINTEXT_HOST://localhost:9092'\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0\n      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1\n      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1\n      KAFKA_JMX_PORT: 9997\n      KAFKA_JMX_HOSTNAME: localhost\n      KAFKA_PROCESS_ROLES: 'broker,controller'\n      KAFKA_NODE_ID: 1\n      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka:29093'\n      KAFKA_LISTENERS: 'SSL://kafka:29092,CONTROLLER://kafka:29093,PLAINTEXT_HOST://0.0.0.0:9092'\n      KAFKA_INTER_BROKER_LISTENER_NAME: 'SSL'\n      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'\n      KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'\n      KAFKA_SECURITY_PROTOCOL: SSL\n      KAFKA_SSL_ENABLED_MECHANISMS: PLAIN,SSL\n      KAFKA_SSL_KEYSTORE_FILENAME: kafka.keystore.jks\n      KAFKA_SSL_KEYSTORE_CREDENTIALS: creds\n      KAFKA_SSL_KEY_CREDENTIALS: creds\n      KAFKA_SSL_TRUSTSTORE_FILENAME: kafka.truststore.jks\n      KAFKA_SSL_TRUSTSTORE_CREDENTIALS: creds\n      #KAFKA_SSL_CLIENT_AUTH: 'required'\n      KAFKA_SSL_CLIENT_AUTH: 'requested'\n      KAFKA_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: '' # COMMON NAME VERIFICATION IS DISABLED SERVER-SIDE\n    volumes:\n      - ./scripts/update_run.sh:/tmp/update_run.sh\n      - ./ssl/creds:/etc/kafka/secrets/creds\n      - ./ssl/kafka.truststore.jks:/etc/kafka/secrets/kafka.truststore.jks\n      - ./ssl/kafka.keystore.jks:/etc/kafka/secrets/kafka.keystore.jks\n    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'\"\n"
  },
  {
    "path": "documentation/compose/kafka-ui-acl-with-zk.yaml",
    "content": "---\nversion: '2'\nservices:\n\n  kafka-ui:\n    container_name: kafka-ui\n    image: provectuslabs/kafka-ui:latest\n    ports:\n      - 8080:8080\n    depends_on:\n      - zookeeper\n      - kafka\n    environment:\n      KAFKA_CLUSTERS_0_NAME: local\n      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092\n      KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL: SASL_PLAINTEXT\n      KAFKA_CLUSTERS_0_PROPERTIES_SASL_MECHANISM: PLAIN\n      KAFKA_CLUSTERS_0_PROPERTIES_SASL_JAAS_CONFIG: 'org.apache.kafka.common.security.plain.PlainLoginModule required username=\"admin\" password=\"admin-secret\";'\n\n  zookeeper:\n    image: wurstmeister/zookeeper:3.4.6\n    environment:\n      JVMFLAGS: \"-Djava.security.auth.login.config=/etc/zookeeper/zookeeper_jaas.conf\"\n    volumes:\n      - ./jaas/zookeeper_jaas.conf:/etc/zookeeper/zookeeper_jaas.conf\n    ports:\n      - 2181:2181\n\n  kafka:\n    image: confluentinc/cp-kafka:7.2.1\n    hostname: kafka\n    container_name: kafka\n    ports:\n      - \"9092:9092\"\n      - \"9997:9997\"\n    environment:\n      KAFKA_BROKER_ID: 1\n      KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'\n      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,SASL_PLAINTEXT:SASL_PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'\n      KAFKA_ADVERTISED_LISTENERS: 'SASL_PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092'\n      KAFKA_OPTS: \"-Djava.security.auth.login.config=/etc/kafka/jaas/kafka_server.conf\"\n      KAFKA_AUTHORIZER_CLASS_NAME: \"kafka.security.authorizer.AclAuthorizer\"\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0\n      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1\n      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1\n      KAFKA_JMX_PORT: 9997\n      KAFKA_JMX_HOSTNAME: localhost\n      KAFKA_NODE_ID: 1\n      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka:29093'\n      KAFKA_LISTENERS: 'SASL_PLAINTEXT://kafka:29092,CONTROLLER://kafka:29093,PLAINTEXT_HOST://0.0.0.0:9092'\n      KAFKA_INTER_BROKER_LISTENER_NAME: 'SASL_PLAINTEXT'\n      KAFKA_SASL_ENABLED_MECHANISMS: 'PLAIN'\n      KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL: 'PLAIN'\n      KAFKA_SECURITY_PROTOCOL: 'SASL_PLAINTEXT'\n      KAFKA_SUPER_USERS: 'User:admin'\n    volumes:\n      - ./scripts/update_run.sh:/tmp/update_run.sh\n      - ./jaas:/etc/kafka/jaas\n"
  },
  {
    "path": "documentation/compose/kafka-ui-arm64.yaml",
    "content": "# ARM64 supported images for kafka can be found here\n# https://hub.docker.com/r/confluentinc/cp-kafka/tags?page=1&name=arm64\n---\nversion: '2'\nservices:\n  kafka-ui:\n    container_name: kafka-ui\n    image: provectuslabs/kafka-ui:latest\n    ports:\n      - 8080:8080\n    depends_on:\n      - kafka0\n      - schema-registry0\n      - kafka-connect0\n    environment:\n      KAFKA_CLUSTERS_0_NAME: local\n      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092\n      KAFKA_CLUSTERS_0_METRICS_PORT: 9997\n      KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schema-registry0:8085\n      KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME: first\n      KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS: http://kafka-connect0:8083\n      DYNAMIC_CONFIG_ENABLED: 'true'  # not necessary, added for tests\n      KAFKA_CLUSTERS_0_AUDIT_TOPICAUDITENABLED: 'true'\n      KAFKA_CLUSTERS_0_AUDIT_CONSOLEAUDITENABLED: 'true'\n\n  kafka0:\n    image: confluentinc/cp-kafka:7.2.1.arm64\n    hostname: kafka0\n    container_name: kafka0\n    ports:\n      - 9092:9092\n      - 9997:9997\n    environment:\n      KAFKA_BROKER_ID: 1\n      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT\n      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092\n      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0\n      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1\n      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1\n      KAFKA_PROCESS_ROLES: 'broker,controller'\n      KAFKA_NODE_ID: 1\n      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093'\n      KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092'\n      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'\n      KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'\n      KAFKA_JMX_PORT: 9997\n      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\n    volumes:\n      - ./scripts/update_run.sh:/tmp/update_run.sh\n    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'\"\n\n  schema-registry0:\n    image: confluentinc/cp-schema-registry:7.2.1.arm64\n    ports:\n      - 8085:8085\n    depends_on:\n      - kafka0\n    environment:\n      SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092\n      SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT\n      SCHEMA_REGISTRY_HOST_NAME: schema-registry0\n      SCHEMA_REGISTRY_LISTENERS: http://schema-registry0:8085\n\n      SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: \"http\"\n      SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO\n      SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas\n\n  kafka-connect0:\n    image: confluentinc/cp-kafka-connect:7.2.1.arm64\n    ports:\n      - 8083:8083\n    depends_on:\n      - kafka0\n      - schema-registry0\n    environment:\n      CONNECT_BOOTSTRAP_SERVERS: kafka0:29092\n      CONNECT_GROUP_ID: compose-connect-group\n      CONNECT_CONFIG_STORAGE_TOPIC: _connect_configs\n      CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR: 1\n      CONNECT_OFFSET_STORAGE_TOPIC: _connect_offset\n      CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR: 1\n      CONNECT_STATUS_STORAGE_TOPIC: _connect_status\n      CONNECT_STATUS_STORAGE_REPLICATION_FACTOR: 1\n      CONNECT_KEY_CONVERTER: org.apache.kafka.connect.storage.StringConverter\n      CONNECT_KEY_CONVERTER_SCHEMA_REGISTRY_URL: http://schema-registry0:8085\n      CONNECT_VALUE_CONVERTER: org.apache.kafka.connect.storage.StringConverter\n      CONNECT_VALUE_CONVERTER_SCHEMA_REGISTRY_URL: http://schema-registry0:8085\n      CONNECT_INTERNAL_KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter\n      CONNECT_INTERNAL_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter\n      CONNECT_REST_ADVERTISED_HOST_NAME: kafka-connect0\n      CONNECT_PLUGIN_PATH: \"/usr/share/java,/usr/share/confluent-hub-components\"\n\n  kafka-init-topics:\n    image: confluentinc/cp-kafka:7.2.1.arm64\n    volumes:\n       - ./data/message.json:/data/message.json\n    depends_on:\n      - kafka0\n    command: \"bash -c 'echo Waiting for Kafka to be ready... && \\\n               cub kafka-ready -b kafka0:29092 1 30 && \\\n               kafka-topics --create --topic second.users --partitions 3 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \\\n               kafka-topics --create --topic second.messages --partitions 2 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \\\n               kafka-topics --create --topic first.messages --partitions 2 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \\\n               kafka-console-producer --bootstrap-server kafka0:29092 --topic second.users < /data/message.json'\"\n"
  },
  {
    "path": "documentation/compose/kafka-ui-auth-context.yaml",
    "content": "---\nversion: '2'\nservices:\n\n  kafka-ui:\n    container_name: kafka-ui\n    image: provectuslabs/kafka-ui:latest\n    ports:\n      - 8080:8080\n    depends_on:\n      - kafka\n    environment:\n      KAFKA_CLUSTERS_0_NAME: local\n      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092\n      KAFKA_CLUSTERS_0_METRICS_PORT: 9997\n      SERVER_SERVLET_CONTEXT_PATH: /kafkaui\n      AUTH_TYPE: \"LOGIN_FORM\"\n      SPRING_SECURITY_USER_NAME: admin\n      SPRING_SECURITY_USER_PASSWORD: pass\n\n  kafka:\n    image: confluentinc/cp-kafka:7.2.1\n    hostname: kafka\n    container_name: kafka\n    ports:\n      - \"9092:9092\"\n      - \"9997:9997\"\n    environment:\n      KAFKA_BROKER_ID: 1\n      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'\n      KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092'\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0\n      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1\n      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1\n      KAFKA_JMX_PORT: 9997\n      KAFKA_JMX_HOSTNAME: localhost\n      KAFKA_PROCESS_ROLES: 'broker,controller'\n      KAFKA_NODE_ID: 1\n      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka:29093'\n      KAFKA_LISTENERS: 'PLAINTEXT://kafka:29092,CONTROLLER://kafka:29093,PLAINTEXT_HOST://0.0.0.0:9092'\n      KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'\n      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'\n      KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'\n    volumes:\n      - ./scripts/update_run.sh:/tmp/update_run.sh\n    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'\""
  },
  {
    "path": "documentation/compose/kafka-ui-connectors-auth.yaml",
    "content": "---\nversion: \"2\"\nservices:\n  kafka-ui:\n    container_name: kafka-ui\n    image: provectuslabs/kafka-ui:latest\n    ports:\n      - 8080:8080\n    depends_on:\n      - kafka0\n      - schemaregistry0\n      - kafka-connect0\n    environment:\n      KAFKA_CLUSTERS_0_NAME: local\n      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092\n      KAFKA_CLUSTERS_0_METRICS_PORT: 9997\n      KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schemaregistry0:8085\n      KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME: first\n      KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS: http://kafka-connect0:8083\n      KAFKA_CLUSTERS_0_KAFKACONNECT_0_USERNAME: admin\n      KAFKA_CLUSTERS_0_KAFKACONNECT_0_PASSWORD: admin-secret\n\n  kafka0:\n    image: confluentinc/cp-kafka:7.2.1\n    hostname: kafka0\n    container_name: kafka0\n    ports:\n      - \"9092:9092\"\n      - \"9997:9997\"\n    environment:\n      KAFKA_BROKER_ID: 1\n      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: \"CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT\"\n      KAFKA_ADVERTISED_LISTENERS: \"PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092\"\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0\n      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1\n      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1\n      KAFKA_JMX_PORT: 9997\n      KAFKA_JMX_HOSTNAME: localhost\n      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\n      KAFKA_PROCESS_ROLES: \"broker,controller\"\n      KAFKA_NODE_ID: 1\n      KAFKA_CONTROLLER_QUORUM_VOTERS: \"1@kafka0:29093\"\n      KAFKA_LISTENERS: \"PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092\"\n      KAFKA_INTER_BROKER_LISTENER_NAME: \"PLAINTEXT\"\n      KAFKA_CONTROLLER_LISTENER_NAMES: \"CONTROLLER\"\n      KAFKA_LOG_DIRS: \"/tmp/kraft-combined-logs\"\n    volumes:\n      - ./scripts/update_run.sh:/tmp/update_run.sh\n    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'''\n\n  schemaregistry0:\n    image: confluentinc/cp-schema-registry:7.2.1\n    ports:\n      - 8085:8085\n    depends_on:\n      - kafka0\n    environment:\n      SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092\n      SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT\n      SCHEMA_REGISTRY_HOST_NAME: schemaregistry0\n      SCHEMA_REGISTRY_LISTENERS: http://schemaregistry0:8085\n\n      SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: \"http\"\n      SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO\n      SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas\n\n  kafka-connect0:\n    build:\n      context: ./kafka-connect\n      args:\n        image: confluentinc/cp-kafka-connect:7.2.1\n    ports:\n      - 8083:8083\n    depends_on:\n      - kafka0\n      - schemaregistry0\n    volumes:\n      - ./jaas:/conf\n    environment:\n      CONNECT_BOOTSTRAP_SERVERS: kafka0:29092\n      CONNECT_GROUP_ID: compose-connect-group\n      CONNECT_CONFIG_STORAGE_TOPIC: _connect_configs\n      CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR: 1\n      CONNECT_OFFSET_STORAGE_TOPIC: _connect_offset\n      CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR: 1\n      CONNECT_STATUS_STORAGE_TOPIC: _connect_status\n      CONNECT_STATUS_STORAGE_REPLICATION_FACTOR: 1\n      CONNECT_KEY_CONVERTER: org.apache.kafka.connect.storage.StringConverter\n      CONNECT_KEY_CONVERTER_SCHEMA_REGISTRY_URL: http://schemaregistry0:8085\n      CONNECT_VALUE_CONVERTER: org.apache.kafka.connect.storage.StringConverter\n      CONNECT_VALUE_CONVERTER_SCHEMA_REGISTRY_URL: http://schemaregistry0:8085\n      CONNECT_INTERNAL_KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter\n      CONNECT_INTERNAL_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter\n      CONNECT_REST_ADVERTISED_HOST_NAME: kafka-connect0\n      CONNECT_REST_PORT: 8083\n      CONNECT_PLUGIN_PATH: \"/usr/share/java,/usr/share/confluent-hub-components\"\n      CONNECT_REST_EXTENSION_CLASSES: \"org.apache.kafka.connect.rest.basic.auth.extension.BasicAuthSecurityRestExtension\"\n      KAFKA_OPTS: \"-Djava.security.auth.login.config=/conf/kafka_connect.jaas\"\n\n  #      AWS_ACCESS_KEY_ID: \"\"\n  #      AWS_SECRET_ACCESS_KEY: \"\"\n\n  kafka-init-topics:\n    image: confluentinc/cp-kafka:7.2.1\n    volumes:\n      - ./data/message.json:/data/message.json\n    depends_on:\n      - kafka0\n    command: \"bash -c 'echo Waiting for Kafka to be ready... && \\\n      cub kafka-ready -b kafka0:29092 1 30 && \\\n      kafka-topics --create --topic users --partitions 3 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \\\n      kafka-topics --create --topic messages --partitions 2 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \\\n      kafka-console-producer --bootstrap-server kafka0:29092 --topic users < /data/message.json'\"\n"
  },
  {
    "path": "documentation/compose/kafka-ui-jmx-secured.yml",
    "content": "---\nversion: '2'\nservices:\n\n  kafka-ui:\n    container_name: kafka-ui\n    image: provectuslabs/kafka-ui:latest\n    ports:\n      - 8080:8080\n    depends_on:\n      - kafka0\n    environment:\n      KAFKA_CLUSTERS_0_NAME: local\n      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092\n      KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schemaregistry0:8085\n      KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME: first\n      KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS: http://kafka-connect0:8083\n      KAFKA_CLUSTERS_0_METRICS_PORT: 9997\n      KAFKA_CLUSTERS_0_METRICS_USERNAME: root\n      KAFKA_CLUSTERS_0_METRICS_PASSWORD: password\n      KAFKA_CLUSTERS_0_METRICS_KEYSTORE_LOCATION: /jmx/clientkeystore\n      KAFKA_CLUSTERS_0_METRICS_KEYSTORE_PASSWORD: '12345678'\n      KAFKA_CLUSTERS_0_SSL_TRUSTSTORE_LOCATION: /jmx/clienttruststore\n      KAFKA_CLUSTERS_0_SSL_TRUSTSTORE_PASSWORD: '12345678'\n    volumes:\n      - ./jmx/clienttruststore:/jmx/clienttruststore\n      - ./jmx/clientkeystore:/jmx/clientkeystore\n\n  kafka0:\n    image: confluentinc/cp-kafka:7.2.1\n    hostname: kafka0\n    container_name: kafka0\n    ports:\n      - 9092:9092\n      - 9997:9997\n    environment:\n      KAFKA_BROKER_ID: 1\n      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'\n      KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092'\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0\n      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1\n      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1\n      KAFKA_JMX_PORT: 9997\n      KAFKA_PROCESS_ROLES: 'broker,controller'\n      KAFKA_NODE_ID: 1\n      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093'\n      KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092'\n      KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'\n      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'\n      KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'\n      # CHMOD 700 FOR JMXREMOTE.* FILES\n      KAFKA_JMX_OPTS: >-\n        -Dcom.sun.management.jmxremote\n        -Dcom.sun.management.jmxremote.authenticate=true\n        -Dcom.sun.management.jmxremote.ssl=true\n        -Dcom.sun.management.jmxremote.registry.ssl=true\n        -Dcom.sun.management.jmxremote.ssl.need.client.auth=true\n        -Djavax.net.ssl.keyStore=/jmx/serverkeystore\n        -Djavax.net.ssl.keyStorePassword=12345678\n        -Djavax.net.ssl.trustStore=/jmx/servertruststore\n        -Djavax.net.ssl.trustStorePassword=12345678\n        -Dcom.sun.management.jmxremote.password.file=/jmx/jmxremote.password\n        -Dcom.sun.management.jmxremote.access.file=/jmx/jmxremote.access\n        -Dcom.sun.management.jmxremote.rmi.port=9997\n        -Djava.rmi.server.hostname=kafka0\n    volumes:\n      - ./jmx/serverkeystore:/jmx/serverkeystore\n      - ./jmx/servertruststore:/jmx/servertruststore\n      - ./jmx/jmxremote.password:/jmx/jmxremote.password\n      - ./jmx/jmxremote.access:/jmx/jmxremote.access\n      - ./scripts/update_run.sh:/tmp/update_run.sh\n    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'\"\n"
  },
  {
    "path": "documentation/compose/kafka-ui-sasl.yaml",
    "content": "---\nversion: '2'\nservices:\n\n  kafka-ui:\n    container_name: kafka-ui\n    image: provectuslabs/kafka-ui:latest\n    ports:\n      - 8080:8080\n    depends_on:\n      - kafka\n    environment:\n      KAFKA_CLUSTERS_0_NAME: local\n      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092\n      KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL: SASL_PLAINTEXT\n      KAFKA_CLUSTERS_0_PROPERTIES_SASL_MECHANISM: PLAIN\n      KAFKA_CLUSTERS_0_PROPERTIES_SASL_JAAS_CONFIG: 'org.apache.kafka.common.security.plain.PlainLoginModule required username=\"admin\" password=\"admin-secret\";'\n      DYNAMIC_CONFIG_ENABLED: true # not necessary for sasl auth, added for tests\n\n  kafka:\n    image: confluentinc/cp-kafka:7.2.1\n    hostname: kafka\n    container_name: kafka\n    ports:\n      - \"9092:9092\"\n      - \"9997:9997\"\n    environment:\n      KAFKA_BROKER_ID: 1\n      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,SASL_PLAINTEXT:SASL_PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'\n      KAFKA_ADVERTISED_LISTENERS: 'SASL_PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092'\n      KAFKA_OPTS: \"-Djava.security.auth.login.config=/etc/kafka/jaas/kafka_server.conf\"\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0\n      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1\n      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1\n      KAFKA_JMX_PORT: 9997\n      KAFKA_JMX_HOSTNAME: localhost\n      KAFKA_PROCESS_ROLES: 'broker,controller'\n      KAFKA_NODE_ID: 1\n      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka:29093'\n      KAFKA_LISTENERS: 'SASL_PLAINTEXT://kafka:29092,CONTROLLER://kafka:29093,PLAINTEXT_HOST://0.0.0.0:9092'\n      KAFKA_INTER_BROKER_LISTENER_NAME: 'SASL_PLAINTEXT'\n      KAFKA_SASL_ENABLED_MECHANISMS: 'PLAIN'\n      KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL: 'PLAIN'\n      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'\n      KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'\n      KAFKA_SECURITY_PROTOCOL: 'SASL_PLAINTEXT'\n      KAFKA_SUPER_USERS: 'User:admin,User:enzo'\n    volumes:\n      - ./scripts/update_run.sh:/tmp/update_run.sh\n      - ./jaas:/etc/kafka/jaas\n    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'\"\n"
  },
  {
    "path": "documentation/compose/kafka-ui-serdes.yaml",
    "content": "---\nversion: '2'\nservices:\n\n    kafka-ui:\n        container_name: kafka-ui\n        image: provectuslabs/kafka-ui:latest\n        ports:\n            - 8080:8080\n        depends_on:\n            - kafka0\n            - schemaregistry0\n        environment:\n            kafka.clusters.0.name: SerdeExampleCluster\n            kafka.clusters.0.bootstrapServers: kafka0:29092\n            kafka.clusters.0.schemaRegistry: http://schemaregistry0:8085\n\n            # optional SSL settings for cluster (will be used by SchemaRegistry serde, if set)\n            #kafka.clusters.0.ssl.keystoreLocation: /kafka.keystore.jks\n            #kafka.clusters.0.ssl.keystorePassword: \"secret\"\n            #kafka.clusters.0.ssl.truststoreLocation: /kafka.truststore.jks\n            #kafka.clusters.0.ssl.truststorePassword: \"secret\"\n\n            # optional auth properties for SR\n            #kafka.clusters.0.schemaRegistryAuth.username: \"use\"\n            #kafka.clusters.0.schemaRegistryAuth.password: \"pswrd\"\n\n            kafka.clusters.0.defaultKeySerde: Int32  #optional\n            kafka.clusters.0.defaultValueSerde: String #optional\n\n            kafka.clusters.0.serde.0.name: ProtobufFile\n            kafka.clusters.0.serde.0.topicKeysPattern: \"topic1\"\n            kafka.clusters.0.serde.0.topicValuesPattern: \"topic1\"\n            kafka.clusters.0.serde.0.properties.protobufFilesDir: /protofiles/\n            kafka.clusters.0.serde.0.properties.protobufMessageNameForKey: test.MyKey # default type for keys\n            kafka.clusters.0.serde.0.properties.protobufMessageName: test.MyValue # default type for values\n            kafka.clusters.0.serde.0.properties.protobufMessageNameForKeyByTopic.topic1: test.MySpecificTopicKey # keys type for topic \"topic1\"\n            kafka.clusters.0.serde.0.properties.protobufMessageNameByTopic.topic1: test.MySpecificTopicValue # values type for topic \"topic1\"\n\n            kafka.clusters.0.serde.1.name: String\n            #kafka.clusters.0.serde.1.properties.encoding: \"UTF-16\" #optional, default is UTF-8\n            kafka.clusters.0.serde.1.topicValuesPattern: \"json-events|text-events\"\n\n            kafka.clusters.0.serde.2.name: AsciiString\n            kafka.clusters.0.serde.2.className: com.provectus.kafka.ui.serdes.builtin.StringSerde\n            kafka.clusters.0.serde.2.properties.encoding: \"ASCII\"\n\n            kafka.clusters.0.serde.3.name: SchemaRegistry # will be configured automatically using cluster SR\n            kafka.clusters.0.serde.3.topicValuesPattern: \"sr-topic.*\"\n\n            kafka.clusters.0.serde.4.name: AnotherSchemaRegistry\n            kafka.clusters.0.serde.4.className: com.provectus.kafka.ui.serdes.builtin.sr.SchemaRegistrySerde\n            kafka.clusters.0.serde.4.properties.url: http://schemaregistry0:8085\n            kafka.clusters.0.serde.4.properties.keySchemaNameTemplate: \"%s-key\"\n            kafka.clusters.0.serde.4.properties.schemaNameTemplate: \"%s-value\"\n            #kafka.clusters.0.serde.4.topicValuesPattern: \"sr2-topic.*\"\n            # optional auth and ssl properties for SR (overrides cluster-level):\n            #kafka.clusters.0.serde.4.properties.username: \"user\"\n            #kafka.clusters.0.serde.4.properties.password: \"passw\"\n            #kafka.clusters.0.serde.4.properties.keystoreLocation:  /kafka.keystore.jks\n            #kafka.clusters.0.serde.4.properties.keystorePassword: \"secret\"\n            #kafka.clusters.0.serde.4.properties.truststoreLocation: /kafka.truststore.jks\n            #kafka.clusters.0.serde.4.properties.truststorePassword: \"secret\"\n\n            kafka.clusters.0.serde.5.name: UInt64\n            kafka.clusters.0.serde.5.topicKeysPattern: \"topic-with-uint64keys\"\n        volumes:\n            - ./proto:/protofiles\n\n    kafka0:\n        image: confluentinc/cp-kafka:7.2.1\n        hostname: kafka0\n        container_name: kafka0\n        ports:\n            - \"9092:9092\"\n            - \"9997:9997\"\n        environment:\n            KAFKA_BROKER_ID: 1\n            KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'\n            KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092'\n            KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n            KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0\n            KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1\n            KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1\n            KAFKA_JMX_PORT: 9997\n            KAFKA_JMX_HOSTNAME: localhost\n            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\n            KAFKA_PROCESS_ROLES: 'broker,controller'\n            KAFKA_NODE_ID: 1\n            KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093'\n            KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092'\n            KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'\n            KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'\n            KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'\n        volumes:\n            - ./scripts/update_run.sh:/tmp/update_run.sh\n        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'\"\n\n    schemaregistry0:\n        image: confluentinc/cp-schema-registry:7.2.1\n        ports:\n            - 8085:8085\n        depends_on:\n            - kafka0\n        environment:\n            SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092\n            SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT\n            SCHEMA_REGISTRY_HOST_NAME: schemaregistry0\n            SCHEMA_REGISTRY_LISTENERS: http://schemaregistry0:8085\n\n            SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: \"http\"\n            SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO\n            SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas\n"
  },
  {
    "path": "documentation/compose/kafka-ui-with-jmx-exporter.yaml",
    "content": "---\nversion: '2'\nservices:\n\n  kafka0:\n    image: confluentinc/cp-kafka:7.2.1\n    hostname: kafka0\n    container_name: kafka0\n    ports:\n      - \"9092:9092\"\n      - \"11001:11001\"\n    environment:\n      KAFKA_BROKER_ID: 1\n      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'\n      KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092'\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0\n      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1\n      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1\n      KAFKA_PROCESS_ROLES: 'broker,controller'\n      KAFKA_NODE_ID: 1\n      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093'\n      KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092'\n      KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'\n      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'\n      KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'\n      KAFKA_OPTS: -javaagent:/usr/share/jmx_exporter/jmx_prometheus_javaagent.jar=11001:/usr/share/jmx_exporter/kafka-broker.yml\n    volumes:\n      - ./jmx-exporter:/usr/share/jmx_exporter/\n      - ./scripts/update_run.sh:/tmp/update_run.sh\n    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'\"\n\n  kafka-ui:\n    container_name: kafka-ui\n    image: provectuslabs/kafka-ui:latest\n    ports:\n      - 8080:8080\n    depends_on:\n      - kafka0\n    environment:\n      KAFKA_CLUSTERS_0_NAME: local\n      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092\n      KAFKA_CLUSTERS_0_METRICS_PORT: 11001\n      KAFKA_CLUSTERS_0_METRICS_TYPE: PROMETHEUS\n"
  },
  {
    "path": "documentation/compose/kafka-ui.yaml",
    "content": "---\nversion: '2'\nservices:\n\n  kafka-ui:\n    container_name: kafka-ui\n    image: provectuslabs/kafka-ui:latest\n    ports:\n      - 8080:8080\n    depends_on:\n      - kafka0\n      - kafka1\n      - schemaregistry0\n      - schemaregistry1\n      - kafka-connect0\n    environment:\n      KAFKA_CLUSTERS_0_NAME: local\n      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092\n      KAFKA_CLUSTERS_0_METRICS_PORT: 9997\n      KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schemaregistry0:8085\n      KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME: first\n      KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS: http://kafka-connect0:8083\n      KAFKA_CLUSTERS_1_NAME: secondLocal\n      KAFKA_CLUSTERS_1_BOOTSTRAPSERVERS: kafka1:29092\n      KAFKA_CLUSTERS_1_METRICS_PORT: 9998\n      KAFKA_CLUSTERS_1_SCHEMAREGISTRY: http://schemaregistry1:8085\n      DYNAMIC_CONFIG_ENABLED: 'true'\n\n  kafka0:\n    image: confluentinc/cp-kafka:7.2.1\n    hostname: kafka0\n    container_name: kafka0\n    ports:\n      - \"9092:9092\"\n      - \"9997:9997\"\n    environment:\n      KAFKA_BROKER_ID: 1\n      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'\n      KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092'\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0\n      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1\n      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1\n      KAFKA_JMX_PORT: 9997\n      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\n      KAFKA_PROCESS_ROLES: 'broker,controller'\n      KAFKA_NODE_ID: 1\n      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093'\n      KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092'\n      KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'\n      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'\n      KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'\n    volumes:\n      - ./scripts/update_run.sh:/tmp/update_run.sh\n    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'\"\n\n  kafka1:\n    image: confluentinc/cp-kafka:7.2.1\n    hostname: kafka1\n    container_name: kafka1\n    ports:\n      - \"9093:9092\"\n      - \"9998:9998\"\n    environment:\n      KAFKA_BROKER_ID: 1\n      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'\n      KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka1:29092,PLAINTEXT_HOST://localhost:9092'\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0\n      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1\n      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1\n      KAFKA_JMX_PORT: 9998\n      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\n      KAFKA_PROCESS_ROLES: 'broker,controller'\n      KAFKA_NODE_ID: 1\n      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka1:29093'\n      KAFKA_LISTENERS: 'PLAINTEXT://kafka1:29092,CONTROLLER://kafka1:29093,PLAINTEXT_HOST://0.0.0.0:9092'\n      KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'\n      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'\n      KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'\n    volumes:\n      - ./scripts/update_run.sh:/tmp/update_run.sh\n    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'\"\n\n  schemaregistry0:\n    image: confluentinc/cp-schema-registry:7.2.1\n    ports:\n      - 8085:8085\n    depends_on:\n      - kafka0\n    environment:\n      SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092\n      SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT\n      SCHEMA_REGISTRY_HOST_NAME: schemaregistry0\n      SCHEMA_REGISTRY_LISTENERS: http://schemaregistry0:8085\n\n      SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: \"http\"\n      SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO\n      SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas\n\n  schemaregistry1:\n    image: confluentinc/cp-schema-registry:7.2.1\n    ports:\n      - 18085:8085\n    depends_on:\n      - kafka1\n    environment:\n      SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka1:29092\n      SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT\n      SCHEMA_REGISTRY_HOST_NAME: schemaregistry1\n      SCHEMA_REGISTRY_LISTENERS: http://schemaregistry1:8085\n\n      SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: \"http\"\n      SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO\n      SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas\n\n  kafka-connect0:\n    image: confluentinc/cp-kafka-connect:7.2.1\n    ports:\n      - 8083:8083\n    depends_on:\n      - kafka0\n      - schemaregistry0\n    environment:\n      CONNECT_BOOTSTRAP_SERVERS: kafka0:29092\n      CONNECT_GROUP_ID: compose-connect-group\n      CONNECT_CONFIG_STORAGE_TOPIC: _connect_configs\n      CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR: 1\n      CONNECT_OFFSET_STORAGE_TOPIC: _connect_offset\n      CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR: 1\n      CONNECT_STATUS_STORAGE_TOPIC: _connect_status\n      CONNECT_STATUS_STORAGE_REPLICATION_FACTOR: 1\n      CONNECT_KEY_CONVERTER: org.apache.kafka.connect.storage.StringConverter\n      CONNECT_KEY_CONVERTER_SCHEMA_REGISTRY_URL: http://schemaregistry0:8085\n      CONNECT_VALUE_CONVERTER: org.apache.kafka.connect.storage.StringConverter\n      CONNECT_VALUE_CONVERTER_SCHEMA_REGISTRY_URL: http://schemaregistry0:8085\n      CONNECT_INTERNAL_KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter\n      CONNECT_INTERNAL_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter\n      CONNECT_REST_ADVERTISED_HOST_NAME: kafka-connect0\n      CONNECT_PLUGIN_PATH: \"/usr/share/java,/usr/share/confluent-hub-components\"\n\n  kafka-init-topics:\n    image: confluentinc/cp-kafka:7.2.1\n    volumes:\n       - ./data/message.json:/data/message.json\n    depends_on:\n      - kafka1\n    command: \"bash -c 'echo Waiting for Kafka to be ready... && \\\n               cub kafka-ready -b kafka1:29092 1 30 && \\\n               kafka-topics --create --topic second.users --partitions 3 --replication-factor 1 --if-not-exists --bootstrap-server kafka1:29092 && \\\n               kafka-topics --create --topic second.messages --partitions 2 --replication-factor 1 --if-not-exists --bootstrap-server kafka1:29092 && \\\n               kafka-topics --create --topic first.messages --partitions 2 --replication-factor 1 --if-not-exists --bootstrap-server kafka0:29092 && \\\n               kafka-console-producer --bootstrap-server kafka1:29092 -topic second.users < /data/message.json'\"\n"
  },
  {
    "path": "documentation/compose/kafka-with-zookeeper.yaml",
    "content": "---\nversion: '2'\nservices:\n\n  zookeeper:\n    image: confluentinc/cp-zookeeper:7.2.1\n    hostname: zookeeper\n    container_name: zookeeper\n    ports:\n      - \"2181:2181\"\n    environment:\n      ZOOKEEPER_CLIENT_PORT: 2181\n      ZOOKEEPER_TICK_TIME: 2000\n\n  kafka:\n    image: confluentinc/cp-server:7.2.1\n    hostname: kafka\n    container_name: kafka\n    depends_on:\n      - zookeeper\n    ports:\n      - \"9092:9092\"\n      - \"9997:9997\"\n    environment:\n      KAFKA_BROKER_ID: 1\n      KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'\n      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT\n      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0\n      KAFKA_CONFLUENT_LICENSE_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_CONFLUENT_BALANCER_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1\n      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1\n      KAFKA_JMX_PORT: 9997\n      KAFKA_JMX_HOSTNAME: kafka\n\n  kafka-init-topics:\n    image: confluentinc/cp-kafka:7.2.1\n    volumes:\n       - ./data/message.json:/data/message.json\n    depends_on:\n      - kafka\n    command: \"bash -c 'echo Waiting for Kafka to be ready... && \\\n               cub kafka-ready -b kafka:29092 1 30 && \\\n               kafka-topics --create --topic users --partitions 3 --replication-factor 1 --if-not-exists --bootstrap-server kafka:29092 && \\\n               kafka-topics --create --topic messages --partitions 2 --replication-factor 1 --if-not-exists --bootstrap-server kafka:29092 && \\\n               kafka-console-producer --bootstrap-server kafka:29092 --topic users < /data/message.json'\"\n"
  },
  {
    "path": "documentation/compose/ldap.yaml",
    "content": "---\nversion: '2'\nservices:\n\n  kafka-ui:\n    container_name: kafka-ui\n    image: provectuslabs/kafka-ui:latest\n    ports:\n      - 8080:8080\n    depends_on:\n      - kafka0\n      - schemaregistry0\n    environment:\n      KAFKA_CLUSTERS_0_NAME: local\n      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092\n      KAFKA_CLUSTERS_0_METRICS_PORT: 9997\n      KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schemaregistry0:8085\n\n      AUTH_TYPE: \"LDAP\"\n      SPRING_LDAP_URLS: \"ldap://ldap:10389\"\n      SPRING_LDAP_BASE: \"cn={0},ou=people,dc=planetexpress,dc=com\"\n      SPRING_LDAP_ADMIN_USER: \"cn=admin,dc=planetexpress,dc=com\"\n      SPRING_LDAP_ADMIN_PASSWORD: \"GoodNewsEveryone\"\n      SPRING_LDAP_USER_FILTER_SEARCH_BASE: \"dc=planetexpress,dc=com\"\n      SPRING_LDAP_USER_FILTER_SEARCH_FILTER: \"(&(uid={0})(objectClass=inetOrgPerson))\"\n      SPRING_LDAP_GROUP_FILTER_SEARCH_BASE: \"ou=people,dc=planetexpress,dc=com\"\n#     OAUTH2.LDAP.ACTIVEDIRECTORY: true\n#     OAUTH2.LDAP.AСTIVEDIRECTORY.DOMAIN: \"memelord.lol\"\n\n  ldap:\n    image: rroemhild/test-openldap:latest\n    hostname: \"ldap\"\n    ports:\n      - 10389:10389\n\n  kafka0:\n    image: confluentinc/cp-kafka:7.2.1\n    hostname: kafka0\n    container_name: kafka0\n    ports:\n      - \"9092:9092\"\n      - \"9997:9997\"\n    environment:\n      KAFKA_BROKER_ID: 1\n      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'\n      KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092'\n      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1\n      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0\n      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1\n      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1\n      KAFKA_JMX_PORT: 9997\n      KAFKA_JMX_HOSTNAME: localhost\n      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\n      KAFKA_PROCESS_ROLES: 'broker,controller'\n      KAFKA_NODE_ID: 1\n      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093'\n      KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092'\n      KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'\n      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'\n      KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'\n    volumes:\n      - ./scripts/update_run.sh:/tmp/update_run.sh\n    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'\"\n\n  schemaregistry0:\n    image: confluentinc/cp-schema-registry:7.2.1\n    ports:\n      - 8085:8085\n    depends_on:\n      - kafka0\n    environment:\n      SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092\n      SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT\n      SCHEMA_REGISTRY_HOST_NAME: schemaregistry0\n      SCHEMA_REGISTRY_LISTENERS: http://schemaregistry0:8085\n\n      SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: \"http\"\n      SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO\n      SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas\n"
  },
  {
    "path": "documentation/compose/nginx-proxy.yaml",
    "content": "---\nversion: '2'\nservices:\n  nginx:\n    image: nginx:latest\n    volumes:\n      - ./data/proxy.conf:/etc/nginx/conf.d/default.conf\n    ports:\n      - 8080:80\n\n  kafka-ui:\n    container_name: kafka-ui\n    image: provectuslabs/kafka-ui:latest\n    ports:\n      - 8082:8080\n    environment:\n      KAFKA_CLUSTERS_0_NAME: local\n      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092\n      SERVER_SERVLET_CONTEXT_PATH: /kafka-ui\n"
  },
  {
    "path": "documentation/compose/postgres/Dockerfile",
    "content": "ARG image\n\nFROM ${image}\n\nMAINTAINER Provectus Team\n\nADD data.sql /docker-entrypoint-initdb.d\n\nEXPOSE 5432"
  },
  {
    "path": "documentation/compose/postgres/data.sql",
    "content": "CREATE DATABASE test WITH OWNER = dev_user;\n\\connect test\n\nCREATE TABLE activities\n(\n    id        INTEGER PRIMARY KEY,\n    msg       varchar(24),\n    action    varchar(128),\n    browser   varchar(24),\n    device    json,\n    createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\ninsert into activities(id, action, msg, browser, device)\nvalues (1, 'LOGIN', 'Success', 'Chrome', '{\n  \"name\": \"Chrome\",\n  \"major\": \"67\",\n  \"version\": \"67.0.3396.99\"\n}'),\n       (2, 'LOGIN', 'Failed', 'Apple WebKit', '{\n         \"name\": \"WebKit\",\n         \"major\": \"605\",\n         \"version\": \"605.1.15\"\n       }');"
  },
  {
    "path": "documentation/compose/proto/key-types.proto",
    "content": "syntax = \"proto3\";\npackage test;\n\nimport \"google/protobuf/wrappers.proto\";\n\nmessage MyKey {\n    string myKeyF1 = 1;\n    google.protobuf.UInt64Value uint_64_wrapper = 2;\n}\n\nmessage MySpecificTopicKey {\n    string special_field1 = 1;\n    string special_field2 = 2;\n    google.protobuf.FloatValue float_wrapper = 3;\n}\n"
  },
  {
    "path": "documentation/compose/proto/values.proto",
    "content": "syntax = \"proto3\";\npackage test;\n\nmessage MySpecificTopicValue {\n    string f1 = 1;\n    string f2 = 2;\n}\n\nmessage MyValue {\n  int32 version = 1;\n  string payload = 2;\n  map<int32, string> intToStringMap = 3;\n  map<string, MyValue> strToObjMap  = 4;\n}\n"
  },
  {
    "path": "documentation/compose/scripts/clusterID",
    "content": "zlFiTJelTOuhnklFwLWixw"
  },
  {
    "path": "documentation/compose/scripts/create_cluster_id.sh",
    "content": "kafka-storage random-uuid > /workspace/kafka-ui/documentation/compose/clusterID"
  },
  {
    "path": "documentation/compose/scripts/update_run.sh",
    "content": "# This script is required to run kafka cluster (without zookeeper)\n#!/bin/sh\n\n# Docker workaround: Remove check for KAFKA_ZOOKEEPER_CONNECT parameter\nsed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure\n\n# Docker workaround: Ignore cub zk-ready\nsed -i 's/cub zk-ready/echo ignore zk-ready/' /etc/confluent/docker/ensure\n\n# KRaft required step: Format the storage directory with a new cluster ID\necho \"kafka-storage format --ignore-formatted -t $(kafka-storage random-uuid) -c /etc/kafka/kafka.properties\" >> /etc/confluent/docker/ensure"
  },
  {
    "path": "documentation/compose/scripts/update_run_cluster.sh",
    "content": "# This script is required to run kafka cluster (without zookeeper)\n#!/bin/sh\n\n# Docker workaround: Remove check for KAFKA_ZOOKEEPER_CONNECT parameter\nsed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure\n\n# Docker workaround: Ignore cub zk-ready\nsed -i 's/cub zk-ready/echo ignore zk-ready/' /etc/confluent/docker/ensure\n\n# KRaft required step: Format the storage directory with a new cluster ID\necho \"kafka-storage format --ignore-formatted -t $(cat /tmp/clusterID) -c /etc/kafka/kafka.properties\" >> /etc/confluent/docker/ensure"
  },
  {
    "path": "documentation/compose/ssl/creds",
    "content": "secret"
  },
  {
    "path": "documentation/compose/ssl/generate_certs.sh",
    "content": "#!/usr/bin/env bash\n\nset -eu\n\nKEYSTORE_FILENAME=\"kafka.keystore.jks\"\nVALIDITY_IN_DAYS=3650\nDEFAULT_TRUSTSTORE_FILENAME=\"kafka.truststore.jks\"\nTRUSTSTORE_WORKING_DIRECTORY=\"truststore\"\nKEYSTORE_WORKING_DIRECTORY=\"keystore\"\nCA_CERT_FILE=\"ca-cert\"\nKEYSTORE_SIGN_REQUEST=\"cert-file\"\nKEYSTORE_SIGN_REQUEST_SRL=\"ca-cert.srl\"\nKEYSTORE_SIGNED_CERT=\"cert-signed\"\n\nexport COUNTRY=US\nexport STATE=IL\nexport ORGANIZATION_UNIT=SE\nexport CITY=Chicago\nexport PASSWORD=secret\n\nCOUNTRY=$COUNTRY\nSTATE=$STATE\nOU=$ORGANIZATION_UNIT\nCN=kafka0 # COMMON NAME VERIFICATION GOES BRR\nLOCATION=$CITY\nPASS=$PASSWORD\n\nfunction file_exists_and_exit() {\n  echo \"'$1' cannot exist. Move or delete it before\"\n  echo \"re-running this script.\"\n  exit 1\n}\n\nif [ -e \"$KEYSTORE_WORKING_DIRECTORY\" ]; then\n  file_exists_and_exit $KEYSTORE_WORKING_DIRECTORY\nfi\n\nif [ -e \"$CA_CERT_FILE\" ]; then\n  file_exists_and_exit $CA_CERT_FILE\nfi\n\nif [ -e \"$KEYSTORE_SIGN_REQUEST\" ]; then\n  file_exists_and_exit $KEYSTORE_SIGN_REQUEST\nfi\n\nif [ -e \"$KEYSTORE_SIGN_REQUEST_SRL\" ]; then\n  file_exists_and_exit $KEYSTORE_SIGN_REQUEST_SRL\nfi\n\nif [ -e \"$KEYSTORE_SIGNED_CERT\" ]; then\n  file_exists_and_exit $KEYSTORE_SIGNED_CERT\nfi\n\necho \"Welcome to the Kafka SSL keystore and trust store generator script.\"\n\ntrust_store_file=\"\"\ntrust_store_private_key_file=\"\"\n\n  if [ -e \"$TRUSTSTORE_WORKING_DIRECTORY\" ]; then\n    file_exists_and_exit $TRUSTSTORE_WORKING_DIRECTORY\n  fi\n\n  mkdir $TRUSTSTORE_WORKING_DIRECTORY\n  echo\n  echo \"OK, we'll generate a trust store and associated private key.\"\n  echo\n  echo \"First, the private key.\"\n  echo\n\n  openssl req -new -x509 -keyout $TRUSTSTORE_WORKING_DIRECTORY/ca-key \\\n    -out $TRUSTSTORE_WORKING_DIRECTORY/ca-cert -days $VALIDITY_IN_DAYS -nodes \\\n    -subj \"/C=$COUNTRY/ST=$STATE/L=$LOCATION/O=$OU/CN=$CN\"\n\n  trust_store_private_key_file=\"$TRUSTSTORE_WORKING_DIRECTORY/ca-key\"\n\n  echo\n  echo \"Two files were created:\"\n  echo \" - $TRUSTSTORE_WORKING_DIRECTORY/ca-key -- the private key used later to\"\n  echo \"   sign certificates\"\n  echo \" - $TRUSTSTORE_WORKING_DIRECTORY/ca-cert -- the certificate that will be\"\n  echo \"   stored in the trust store in a moment and serve as the certificate\"\n  echo \"   authority (CA). Once this certificate has been stored in the trust\"\n  echo \"   store, it will be deleted. It can be retrieved from the trust store via:\"\n  echo \"   $ keytool -keystore <trust-store-file> -export -alias CARoot -rfc\"\n\n  echo\n  echo \"Now the trust store will be generated from the certificate.\"\n  echo\n\n  keytool -keystore $TRUSTSTORE_WORKING_DIRECTORY/$DEFAULT_TRUSTSTORE_FILENAME \\\n    -alias CARoot -import -file $TRUSTSTORE_WORKING_DIRECTORY/ca-cert \\\n    -noprompt -dname \"C=$COUNTRY, ST=$STATE, L=$LOCATION, O=$OU, CN=$CN\" -keypass $PASS -storepass $PASS -storetype JKS\n\n  trust_store_file=\"$TRUSTSTORE_WORKING_DIRECTORY/$DEFAULT_TRUSTSTORE_FILENAME\"\n\n  echo\n  echo \"$TRUSTSTORE_WORKING_DIRECTORY/$DEFAULT_TRUSTSTORE_FILENAME was created.\"\n\n  # don't need the cert because it's in the trust store.\n  rm $TRUSTSTORE_WORKING_DIRECTORY/$CA_CERT_FILE\n\necho\necho \"Continuing with:\"\necho \" - trust store file:        $trust_store_file\"\necho \" - trust store private key: $trust_store_private_key_file\"\n\nmkdir $KEYSTORE_WORKING_DIRECTORY\n\necho\necho \"Now, a keystore will be generated. Each broker and logical client needs its own\"\necho \"keystore. This script will create only one keystore. Run this script multiple\"\necho \"times for multiple keystores.\"\necho\necho \"     NOTE: currently in Kafka, the Common Name (CN) does not need to be the FQDN of\"\necho \"           this host. However, at some point, this may change. As such, make the CN\"\necho \"           the FQDN. Some operating systems call the CN prompt 'first / last name'\"\n\n# To learn more about CNs and FQDNs, read:\n# https://docs.oracle.com/javase/7/docs/api/javax/net/ssl/X509ExtendedTrustManager.html\n\nkeytool -keystore $KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME \\\n  -alias localhost -validity $VALIDITY_IN_DAYS -genkey -keyalg RSA \\\n   -noprompt -dname \"C=$COUNTRY, ST=$STATE, L=$LOCATION, O=$OU, CN=$CN\" -keypass $PASS -storepass $PASS -storetype JKS\n\necho\necho \"'$KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME' now contains a key pair and a\"\necho \"self-signed certificate. Again, this keystore can only be used for one broker or\"\necho \"one logical client. Other brokers or clients need to generate their own keystores.\"\n\necho\necho \"Fetching the certificate from the trust store and storing in $CA_CERT_FILE.\"\necho\n\nkeytool -keystore $trust_store_file -export -alias CARoot -rfc -file $CA_CERT_FILE -keypass $PASS -storepass $PASS\n\necho\necho \"Now a certificate signing request will be made to the keystore.\"\necho\nkeytool -keystore $KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME -alias localhost \\\n  -certreq -file $KEYSTORE_SIGN_REQUEST -keypass $PASS -storepass $PASS\n\necho\necho \"Now the trust store's private key (CA) will sign the keystore's certificate.\"\necho\nopenssl x509 -req -CA $CA_CERT_FILE -CAkey $trust_store_private_key_file \\\n  -in $KEYSTORE_SIGN_REQUEST -out $KEYSTORE_SIGNED_CERT \\\n  -days $VALIDITY_IN_DAYS -CAcreateserial \\\n  -extensions kafka -extfile san.cnf\n# creates $KEYSTORE_SIGN_REQUEST_SRL which is never used or needed.\n\necho\necho \"Now the CA will be imported into the keystore.\"\necho\nkeytool -keystore $KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME -alias CARoot \\\n  -import -file $CA_CERT_FILE -keypass $PASS -storepass $PASS -noprompt\nrm $CA_CERT_FILE # delete the trust store cert because it's stored in the trust store.\n\necho\necho \"Now the keystore's signed certificate will be imported back into the keystore.\"\necho\nkeytool -keystore $KEYSTORE_WORKING_DIRECTORY/$KEYSTORE_FILENAME -alias localhost -import \\\n  -file $KEYSTORE_SIGNED_CERT -keypass $PASS -storepass $PASS\n\necho\necho \"All done!\"\necho\necho \"Deleting intermediate files. They are:\"\necho \" - '$KEYSTORE_SIGN_REQUEST_SRL': CA serial number\"\necho \" - '$KEYSTORE_SIGN_REQUEST': the keystore's certificate signing request\"\necho \"   (that was fulfilled)\"\necho \" - '$KEYSTORE_SIGNED_CERT': the keystore's certificate, signed by the CA, and stored back\"\necho \"    into the keystore\"\n\n  rm $KEYSTORE_SIGN_REQUEST_SRL\n  rm $KEYSTORE_SIGN_REQUEST\n  rm $KEYSTORE_SIGNED_CERT\n"
  },
  {
    "path": "documentation/compose/ssl/san.cnf",
    "content": "[kafka]\nsubjectAltName = DNS:kafka0,DNS:schemaregistry0,DNS:kafka-connect0,DNS:ksqldb0\n"
  },
  {
    "path": "documentation/compose/traefik/kafkaui.yaml",
    "content": "http:\n  routers:\n    kafkaui:\n      rule: \"PathPrefix(`/kafka-ui/`)\"\n      entrypoints: web\n      service: kafkaui\n  services:\n    kafkaui:\n      loadBalancer:\n        servers:\n          - url: http://kafka-ui:8080"
  },
  {
    "path": "documentation/compose/traefik-proxy.yaml",
    "content": "---\nversion: '3.8'\nservices:\n  traefik:\n    restart: always\n    image: traefik:v2.4\n    container_name: traefik\n    command:\n      - --api.insecure=true\n      - --providers.file.directory=/etc/traefik\n      - --providers.file.watch=true\n      - --entrypoints.web.address=:80\n      - --log.level=debug\n    ports:\n      - 80:80\n    volumes:\n      - ./traefik:/etc/traefik\n\n  kafka-ui:\n    container_name: kafka-ui\n    image: provectuslabs/kafka-ui:latest\n    ports:\n      - 8082:8080\n    environment:\n      KAFKA_CLUSTERS_0_NAME: local\n      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092\n      SERVER_SERVLET_CONTEXT_PATH: /kafka-ui\n"
  },
  {
    "path": "etc/checkstyle/apache-header.txt",
    "content": "Licensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License."
  },
  {
    "path": "etc/checkstyle/checkstyle-e2e.xml",
    "content": "<?xml version=\"1.0\"?>\n<!DOCTYPE module PUBLIC\n        \"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN\"\n        \"https://checkstyle.org/dtds/configuration_1_3.dtd\">\n\n<!--\n    Checkstyle configuration that checks the Google coding conventions from Google Java Style\n    that can be found at https://google.github.io/styleguide/javaguide.html\n\n    Checkstyle is very configurable. Be sure to read the documentation at\n    http://checkstyle.org (or in your downloaded distribution).\n\n    To completely disable a check, just comment it out or delete it from the file.\n    To suppress certain violations please review suppression filters.\n\n    Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov.\n -->\n\n<module name = \"Checker\">\n    <property name=\"charset\" value=\"UTF-8\"/>\n\n    <property name=\"severity\" value=\"warning\"/>\n\n    <property name=\"fileExtensions\" value=\"java, properties, xml\"/>\n    <!-- Excludes all 'module-info.java' files              -->\n    <!-- See https://checkstyle.org/config_filefilters.html -->\n    <module name=\"BeforeExecutionExclusionFileFilter\">\n        <property name=\"fileNamePattern\" value=\"module\\-info\\.java$\"/>\n    </module>\n    <!-- https://checkstyle.org/config_filters.html#SuppressionFilter -->\n    <module name=\"SuppressionFilter\">\n        <property name=\"file\" value=\"${org.checkstyle.google.suppressionfilter.config}\"\n                  default=\"checkstyle-suppressions.xml\" />\n        <property name=\"optional\" value=\"true\"/>\n    </module>\n\n    <!-- Checks for whitespace                               -->\n    <!-- See http://checkstyle.org/config_whitespace.html -->\n    <module name=\"FileTabCharacter\">\n        <property name=\"eachLine\" value=\"true\"/>\n    </module>\n\n    <module name=\"LineLength\">\n        <property name=\"fileExtensions\" value=\"java\"/>\n        <property name=\"max\" value=\"120\"/>\n        <property name=\"ignorePattern\" value=\"^package.*|^import.*|a href|href|http://|https://|ftp://\"/>\n    </module>\n\n    <module name=\"TreeWalker\">\n        <module name=\"OuterTypeFilename\"/>\n        <module name=\"IllegalTokenText\">\n            <property name=\"tokens\" value=\"STRING_LITERAL, CHAR_LITERAL\"/>\n            <property name=\"format\"\n                      value=\"\\\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\\\(0(10|11|12|14|15|42|47)|134)\"/>\n            <property name=\"message\"\n                      value=\"Consider using special escape sequence instead of octal value or Unicode escaped value.\"/>\n        </module>\n        <module name=\"AvoidEscapedUnicodeCharacters\">\n            <property name=\"allowEscapesForControlCharacters\" value=\"true\"/>\n            <property name=\"allowByTailComment\" value=\"true\"/>\n            <property name=\"allowNonPrintableEscapes\" value=\"true\"/>\n        </module>\n        <module name=\"AvoidStarImport\"/>\n        <module name=\"OneTopLevelClass\"/>\n        <module name=\"NoLineWrap\">\n            <property name=\"tokens\" value=\"PACKAGE_DEF, IMPORT, STATIC_IMPORT\"/>\n        </module>\n        <module name=\"EmptyBlock\">\n            <property name=\"option\" value=\"TEXT\"/>\n            <property name=\"tokens\"\n                      value=\"LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH\"/>\n        </module>\n        <module name=\"NeedBraces\">\n            <property name=\"tokens\"\n                      value=\"LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF, LITERAL_WHILE\"/>\n        </module>\n        <module name=\"LeftCurly\">\n            <property name=\"tokens\"\n                      value=\"ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_CONSTANT_DEF, ENUM_DEF,\n                    INTERFACE_DEF, LAMBDA, LITERAL_CASE, LITERAL_CATCH, LITERAL_DEFAULT,\n                    LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF,\n                    LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, METHOD_DEF,\n                    OBJBLOCK, STATIC_INIT\"/>\n        </module>\n        <module name=\"RightCurly\">\n            <property name=\"id\" value=\"RightCurlySame\"/>\n            <property name=\"tokens\"\n                      value=\"LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE,\n                    LITERAL_DO\"/>\n        </module>\n        <module name=\"RightCurly\">\n            <property name=\"id\" value=\"RightCurlyAlone\"/>\n            <property name=\"option\" value=\"alone\"/>\n            <property name=\"tokens\"\n                      value=\"CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,\n                    INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF\"/>\n        </module>\n        <module name=\"SuppressionXpathSingleFilter\">\n            <!-- suppresion is required till https://github.com/checkstyle/checkstyle/issues/7541 -->\n            <property name=\"id\" value=\"RightCurlyAlone\"/>\n            <property name=\"query\" value=\"//RCURLY[parent::SLIST[count(./*)=1]\n                                                 or preceding-sibling::*[last()][self::LCURLY]]\"/>\n        </module>\n        <module name=\"WhitespaceAfter\">\n            <property name=\"tokens\"\n                      value=\"COMMA, SEMI, TYPECAST, LITERAL_IF, LITERAL_ELSE,\n                    LITERAL_WHILE, LITERAL_DO, LITERAL_FOR, DO_WHILE\"/>\n        </module>\n        <module name=\"WhitespaceAround\">\n            <property name=\"allowEmptyConstructors\" value=\"true\"/>\n            <property name=\"allowEmptyLambdas\" value=\"true\"/>\n            <property name=\"allowEmptyMethods\" value=\"true\"/>\n            <property name=\"allowEmptyTypes\" value=\"true\"/>\n            <property name=\"allowEmptyLoops\" value=\"true\"/>\n            <property name=\"tokens\"\n                      value=\"ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR,\n                    BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAMBDA, LAND,\n                    LCURLY, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY,\n                    LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, LITERAL_SWITCH, LITERAL_SYNCHRONIZED,\n                     LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN,\n                     NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR,\n                     SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT, TYPE_EXTENSION_AND\"/>\n            <message key=\"ws.notFollowed\"\n                     value=\"WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)\"/>\n            <message key=\"ws.notPreceded\"\n                     value=\"WhitespaceAround: ''{0}'' is not preceded with whitespace.\"/>\n        </module>\n        <module name=\"OneStatementPerLine\"/>\n<!--        <module name=\"MultipleVariableDeclarations\"/>-->\n        <module name=\"ArrayTypeStyle\"/>\n        <module name=\"MissingSwitchDefault\"/>\n        <module name=\"FallThrough\"/>\n        <module name=\"UpperEll\"/>\n        <module name=\"ModifierOrder\"/>\n        <module name=\"EmptyLineSeparator\">\n            <property name=\"tokens\"\n                      value=\"PACKAGE_DEF, IMPORT, STATIC_IMPORT, CLASS_DEF, INTERFACE_DEF, ENUM_DEF,\n                    STATIC_INIT, INSTANCE_INIT, METHOD_DEF, CTOR_DEF, VARIABLE_DEF\"/>\n            <property name=\"allowNoEmptyLineBetweenFields\" value=\"true\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <property name=\"id\" value=\"SeparatorWrapDot\"/>\n            <property name=\"tokens\" value=\"DOT\"/>\n            <property name=\"option\" value=\"nl\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <property name=\"id\" value=\"SeparatorWrapComma\"/>\n            <property name=\"tokens\" value=\"COMMA\"/>\n            <property name=\"option\" value=\"EOL\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/258 -->\n            <property name=\"id\" value=\"SeparatorWrapEllipsis\"/>\n            <property name=\"tokens\" value=\"ELLIPSIS\"/>\n            <property name=\"option\" value=\"EOL\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/259 -->\n            <property name=\"id\" value=\"SeparatorWrapArrayDeclarator\"/>\n            <property name=\"tokens\" value=\"ARRAY_DECLARATOR\"/>\n            <property name=\"option\" value=\"EOL\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <property name=\"id\" value=\"SeparatorWrapMethodRef\"/>\n            <property name=\"tokens\" value=\"METHOD_REF\"/>\n            <property name=\"option\" value=\"nl\"/>\n        </module>\n        <module name=\"PackageName\">\n            <property name=\"format\" value=\"^[a-z]+(\\.[a-z][a-z0-9]*)*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Package name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"TypeName\">\n            <property name=\"tokens\" value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"MemberName\">\n            <property name=\"format\" value=\"^[a-z][a-z0-9][a-zA-Z0-9]*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Member name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"ParameterName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Parameter name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"LambdaParameterName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Lambda parameter name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"CatchParameterName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Catch parameter name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"LocalVariableName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Local variable name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"ClassTypeParameterName\">\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Class type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"MethodTypeParameterName\">\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Method type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"InterfaceTypeParameterName\">\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Interface type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"NoFinalizer\"/>\n        <module name=\"GenericWhitespace\">\n            <message key=\"ws.followed\"\n                     value=\"GenericWhitespace ''{0}'' is followed by whitespace.\"/>\n            <message key=\"ws.preceded\"\n                     value=\"GenericWhitespace ''{0}'' is preceded with whitespace.\"/>\n            <message key=\"ws.illegalFollow\"\n                     value=\"GenericWhitespace ''{0}'' should followed by whitespace.\"/>\n            <message key=\"ws.notPreceded\"\n                     value=\"GenericWhitespace ''{0}'' is not preceded with whitespace.\"/>\n        </module>\n        <module name=\"Indentation\">\n            <property name=\"basicOffset\" value=\"2\"/>\n            <property name=\"braceAdjustment\" value=\"0\"/>\n            <property name=\"caseIndent\" value=\"2\"/>\n            <property name=\"throwsIndent\" value=\"4\"/>\n            <property name=\"lineWrappingIndentation\" value=\"4\"/>\n            <property name=\"arrayInitIndent\" value=\"2\"/>\n        </module>\n        <module name=\"AbbreviationAsWordInName\">\n            <property name=\"ignoreFinal\" value=\"false\"/>\n            <property name=\"allowedAbbreviationLength\" value=\"1\"/>\n            <property name=\"tokens\"\n                      value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF,\n                    PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF\"/>\n        </module>\n        <module name=\"OverloadMethodsDeclarationOrder\"/>\n<!--        <module name=\"VariableDeclarationUsageDistance\"/>-->\n        <module name=\"CustomImportOrder\">\n            <property name=\"sortImportsInGroupAlphabetically\" value=\"true\"/>\n            <property name=\"separateLineBetweenGroups\" value=\"true\"/>\n            <property name=\"customImportOrderRules\" value=\"STATIC###THIRD_PARTY_PACKAGE\"/>\n            <property name=\"tokens\" value=\"IMPORT, STATIC_IMPORT, PACKAGE_DEF\"/>\n        </module>\n        <module name=\"MethodParamPad\">\n            <property name=\"tokens\"\n                      value=\"CTOR_DEF, LITERAL_NEW, METHOD_CALL, METHOD_DEF,\n                    SUPER_CTOR_CALL, ENUM_CONSTANT_DEF\"/>\n        </module>\n        <module name=\"NoWhitespaceBefore\">\n            <property name=\"tokens\"\n                      value=\"COMMA, SEMI, POST_INC, POST_DEC, DOT, ELLIPSIS,\n                    LABELED_STAT, METHOD_REF\"/>\n            <property name=\"allowLineBreaks\" value=\"true\"/>\n        </module>\n        <module name=\"ParenPad\">\n            <property name=\"tokens\"\n                      value=\"ANNOTATION, ANNOTATION_FIELD_DEF, CTOR_CALL, CTOR_DEF, DOT, ENUM_CONSTANT_DEF,\n                    EXPR, LITERAL_CATCH, LITERAL_DO, LITERAL_FOR, LITERAL_IF, LITERAL_NEW,\n                    LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_WHILE, METHOD_CALL,\n                    METHOD_DEF, QUESTION, RESOURCE_SPECIFICATION, SUPER_CTOR_CALL, LAMBDA\"/>\n        </module>\n        <module name=\"OperatorWrap\">\n            <property name=\"option\" value=\"NL\"/>\n            <property name=\"tokens\"\n                      value=\"BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,\n                    LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF \"/>\n        </module>\n        <module name=\"AnnotationLocation\">\n            <property name=\"id\" value=\"AnnotationLocationMostCases\"/>\n            <property name=\"tokens\"\n                      value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF\"/>\n        </module>\n        <module name=\"AnnotationLocation\">\n            <property name=\"id\" value=\"AnnotationLocationVariables\"/>\n            <property name=\"tokens\" value=\"VARIABLE_DEF\"/>\n            <property name=\"allowSamelineMultipleAnnotations\" value=\"true\"/>\n        </module>\n        <module name=\"NonEmptyAtclauseDescription\"/>\n        <module name=\"InvalidJavadocPosition\"/>\n        <module name=\"JavadocTagContinuationIndentation\"/>\n        <module name=\"SummaryJavadoc\">\n            <property name=\"forbiddenSummaryFragments\"\n                      value=\"^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )\"/>\n        </module>\n        <module name=\"JavadocParagraph\"/>\n        <module name=\"AtclauseOrder\">\n            <property name=\"tagOrder\" value=\"@param, @return, @throws, @deprecated\"/>\n            <property name=\"target\"\n                      value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF\"/>\n        </module>\n        <module name=\"JavadocMethod\">\n            <property name=\"accessModifiers\" value=\"public\"/>\n            <property name=\"allowMissingParamTags\" value=\"true\"/>\n            <property name=\"allowMissingReturnTag\" value=\"true\"/>\n            <property name=\"allowedAnnotations\" value=\"Override, Test\"/>\n            <property name=\"tokens\" value=\"METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF\"/>\n        </module>\n<!--        <module name=\"MissingJavadocMethod\">-->\n<!--            <property name=\"scope\" value=\"public\"/>-->\n<!--            <property name=\"minLineCount\" value=\"2\"/>-->\n<!--            <property name=\"allowedAnnotations\" value=\"Override, Test\"/>-->\n<!--            <property name=\"tokens\" value=\"METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF\"/>-->\n<!--        </module>-->\n        <module name=\"MethodName\">\n            <property name=\"format\" value=\"^[a-z][a-z0-9][a-zA-Z0-9_]*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Method name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"SingleLineJavadoc\">\n            <property name=\"ignoreInlineTags\" value=\"false\"/>\n        </module>\n        <module name=\"EmptyCatchBlock\">\n            <property name=\"exceptionVariableName\" value=\"ignored\"/>\n        </module>\n        <module name=\"CommentsIndentation\">\n            <property name=\"tokens\" value=\"SINGLE_LINE_COMMENT, BLOCK_COMMENT_BEGIN\"/>\n        </module>\n        <!-- https://checkstyle.org/config_filters.html#SuppressionXpathFilter -->\n        <module name=\"SuppressionXpathFilter\">\n            <property name=\"file\" value=\"${org.checkstyle.google.suppressionxpathfilter.config}\"\n                      default=\"checkstyle-xpath-suppressions.xml\" />\n            <property name=\"optional\" value=\"true\"/>\n        </module>\n    </module>\n</module>\n"
  },
  {
    "path": "etc/checkstyle/checkstyle.xml",
    "content": "<?xml version=\"1.0\"?>\n<!DOCTYPE module PUBLIC\n        \"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN\"\n        \"https://checkstyle.org/dtds/configuration_1_3.dtd\">\n\n<!--\n    Checkstyle configuration that checks the Google coding conventions from Google Java Style\n    that can be found at https://google.github.io/styleguide/javaguide.html\n\n    Checkstyle is very configurable. Be sure to read the documentation at\n    http://checkstyle.org (or in your downloaded distribution).\n\n    To completely disable a check, just comment it out or delete it from the file.\n    To suppress certain violations please review suppression filters.\n\n    Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov.\n -->\n\n<module name = \"Checker\">\n    <property name=\"charset\" value=\"UTF-8\"/>\n\n    <property name=\"severity\" value=\"warning\"/>\n\n    <property name=\"fileExtensions\" value=\"java, properties, xml\"/>\n    <!-- Excludes all 'module-info.java' files              -->\n    <!-- See https://checkstyle.org/config_filefilters.html -->\n    <module name=\"BeforeExecutionExclusionFileFilter\">\n        <property name=\"fileNamePattern\" value=\"module\\-info\\.java$\"/>\n    </module>\n    <!-- https://checkstyle.org/config_filters.html#SuppressionFilter -->\n    <module name=\"SuppressionFilter\">\n        <property name=\"file\" value=\"${org.checkstyle.google.suppressionfilter.config}\"\n                  default=\"checkstyle-suppressions.xml\" />\n        <property name=\"optional\" value=\"true\"/>\n    </module>\n\n    <!-- Checks for whitespace                               -->\n    <!-- See http://checkstyle.org/config_whitespace.html -->\n    <module name=\"FileTabCharacter\">\n        <property name=\"eachLine\" value=\"true\"/>\n    </module>\n\n    <module name=\"LineLength\">\n        <property name=\"fileExtensions\" value=\"java\"/>\n        <property name=\"max\" value=\"120\"/>\n        <property name=\"ignorePattern\" value=\"^package.*|^import.*|a href|href|http://|https://|ftp://\"/>\n    </module>\n\n    <module name=\"TreeWalker\">\n        <module name=\"OuterTypeFilename\"/>\n        <module name=\"IllegalTokenText\">\n            <property name=\"tokens\" value=\"STRING_LITERAL, CHAR_LITERAL\"/>\n            <property name=\"format\"\n                      value=\"\\\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\\\(0(10|11|12|14|15|42|47)|134)\"/>\n            <property name=\"message\"\n                      value=\"Consider using special escape sequence instead of octal value or Unicode escaped value.\"/>\n        </module>\n        <module name=\"AvoidEscapedUnicodeCharacters\">\n            <property name=\"allowEscapesForControlCharacters\" value=\"true\"/>\n            <property name=\"allowByTailComment\" value=\"true\"/>\n            <property name=\"allowNonPrintableEscapes\" value=\"true\"/>\n        </module>\n        <module name=\"AvoidStarImport\"/>\n        <module name=\"OneTopLevelClass\"/>\n        <module name=\"NoLineWrap\">\n            <property name=\"tokens\" value=\"PACKAGE_DEF, IMPORT, STATIC_IMPORT\"/>\n        </module>\n        <module name=\"EmptyBlock\">\n            <property name=\"option\" value=\"TEXT\"/>\n            <property name=\"tokens\"\n                      value=\"LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH\"/>\n        </module>\n        <module name=\"NeedBraces\">\n            <property name=\"tokens\"\n                      value=\"LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF, LITERAL_WHILE\"/>\n        </module>\n        <module name=\"LeftCurly\">\n            <property name=\"tokens\"\n                      value=\"ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_CONSTANT_DEF, ENUM_DEF,\n                    INTERFACE_DEF, LAMBDA, LITERAL_CASE, LITERAL_CATCH, LITERAL_DEFAULT,\n                    LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF,\n                    LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, METHOD_DEF,\n                    OBJBLOCK, STATIC_INIT\"/>\n        </module>\n        <module name=\"RightCurly\">\n            <property name=\"id\" value=\"RightCurlySame\"/>\n            <property name=\"tokens\"\n                      value=\"LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE,\n                    LITERAL_DO\"/>\n        </module>\n        <module name=\"RightCurly\">\n            <property name=\"id\" value=\"RightCurlyAlone\"/>\n            <property name=\"option\" value=\"alone\"/>\n            <property name=\"tokens\"\n                      value=\"CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,\n                    INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF\"/>\n        </module>\n        <module name=\"SuppressionXpathSingleFilter\">\n            <!-- suppresion is required till https://github.com/checkstyle/checkstyle/issues/7541 -->\n            <property name=\"id\" value=\"RightCurlyAlone\"/>\n            <property name=\"query\" value=\"//RCURLY[parent::SLIST[count(./*)=1]\n                                                 or preceding-sibling::*[last()][self::LCURLY]]\"/>\n        </module>\n        <module name=\"WhitespaceAfter\">\n            <property name=\"tokens\"\n                      value=\"COMMA, SEMI, TYPECAST, LITERAL_IF, LITERAL_ELSE,\n                    LITERAL_WHILE, LITERAL_DO, LITERAL_FOR, DO_WHILE\"/>\n        </module>\n        <module name=\"WhitespaceAround\">\n            <property name=\"allowEmptyConstructors\" value=\"true\"/>\n            <property name=\"allowEmptyLambdas\" value=\"true\"/>\n            <property name=\"allowEmptyMethods\" value=\"true\"/>\n            <property name=\"allowEmptyTypes\" value=\"true\"/>\n            <property name=\"allowEmptyLoops\" value=\"true\"/>\n            <property name=\"tokens\"\n                      value=\"ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR,\n                    BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAMBDA, LAND,\n                    LCURLY, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY,\n                    LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, LITERAL_SWITCH, LITERAL_SYNCHRONIZED,\n                     LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN,\n                     NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR,\n                     SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT, TYPE_EXTENSION_AND\"/>\n            <message key=\"ws.notFollowed\"\n                     value=\"WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)\"/>\n            <message key=\"ws.notPreceded\"\n                     value=\"WhitespaceAround: ''{0}'' is not preceded with whitespace.\"/>\n        </module>\n        <module name=\"OneStatementPerLine\"/>\n        <module name=\"MultipleVariableDeclarations\"/>\n        <module name=\"ArrayTypeStyle\"/>\n        <module name=\"MissingSwitchDefault\"/>\n        <module name=\"FallThrough\"/>\n        <module name=\"UpperEll\"/>\n        <module name=\"ModifierOrder\"/>\n        <module name=\"EmptyLineSeparator\">\n            <property name=\"tokens\"\n                      value=\"PACKAGE_DEF, IMPORT, STATIC_IMPORT, CLASS_DEF, INTERFACE_DEF, ENUM_DEF,\n                    STATIC_INIT, INSTANCE_INIT, METHOD_DEF, CTOR_DEF, VARIABLE_DEF\"/>\n            <property name=\"allowNoEmptyLineBetweenFields\" value=\"true\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <property name=\"id\" value=\"SeparatorWrapDot\"/>\n            <property name=\"tokens\" value=\"DOT\"/>\n            <property name=\"option\" value=\"nl\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <property name=\"id\" value=\"SeparatorWrapComma\"/>\n            <property name=\"tokens\" value=\"COMMA\"/>\n            <property name=\"option\" value=\"EOL\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/258 -->\n            <property name=\"id\" value=\"SeparatorWrapEllipsis\"/>\n            <property name=\"tokens\" value=\"ELLIPSIS\"/>\n            <property name=\"option\" value=\"EOL\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/259 -->\n            <property name=\"id\" value=\"SeparatorWrapArrayDeclarator\"/>\n            <property name=\"tokens\" value=\"ARRAY_DECLARATOR\"/>\n            <property name=\"option\" value=\"EOL\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <property name=\"id\" value=\"SeparatorWrapMethodRef\"/>\n            <property name=\"tokens\" value=\"METHOD_REF\"/>\n            <property name=\"option\" value=\"nl\"/>\n        </module>\n        <module name=\"PackageName\">\n            <property name=\"format\" value=\"^[a-z]+(\\.[a-z][a-z0-9]*)*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Package name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"TypeName\">\n            <property name=\"tokens\" value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"MemberName\">\n            <property name=\"format\" value=\"^[a-z][a-z0-9][a-zA-Z0-9]*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Member name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"ParameterName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Parameter name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"LambdaParameterName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Lambda parameter name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"CatchParameterName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Catch parameter name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"LocalVariableName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Local variable name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"ClassTypeParameterName\">\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Class type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"MethodTypeParameterName\">\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Method type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"InterfaceTypeParameterName\">\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Interface type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"NoFinalizer\"/>\n        <module name=\"GenericWhitespace\">\n            <message key=\"ws.followed\"\n                     value=\"GenericWhitespace ''{0}'' is followed by whitespace.\"/>\n            <message key=\"ws.preceded\"\n                     value=\"GenericWhitespace ''{0}'' is preceded with whitespace.\"/>\n            <message key=\"ws.illegalFollow\"\n                     value=\"GenericWhitespace ''{0}'' should followed by whitespace.\"/>\n            <message key=\"ws.notPreceded\"\n                     value=\"GenericWhitespace ''{0}'' is not preceded with whitespace.\"/>\n        </module>\n        <module name=\"Indentation\">\n            <property name=\"basicOffset\" value=\"2\"/>\n            <property name=\"braceAdjustment\" value=\"0\"/>\n            <property name=\"caseIndent\" value=\"2\"/>\n            <property name=\"throwsIndent\" value=\"4\"/>\n            <property name=\"lineWrappingIndentation\" value=\"4\"/>\n            <property name=\"arrayInitIndent\" value=\"2\"/>\n        </module>\n        <module name=\"AbbreviationAsWordInName\">\n            <property name=\"ignoreFinal\" value=\"false\"/>\n            <property name=\"allowedAbbreviationLength\" value=\"1\"/>\n            <property name=\"tokens\"\n                      value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF,\n                    PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF\"/>\n        </module>\n        <module name=\"OverloadMethodsDeclarationOrder\"/>\n        <module name=\"VariableDeclarationUsageDistance\"/>\n        <module name=\"CustomImportOrder\">\n            <property name=\"sortImportsInGroupAlphabetically\" value=\"true\"/>\n            <property name=\"separateLineBetweenGroups\" value=\"true\"/>\n            <property name=\"customImportOrderRules\" value=\"STATIC###THIRD_PARTY_PACKAGE\"/>\n            <property name=\"tokens\" value=\"IMPORT, STATIC_IMPORT, PACKAGE_DEF\"/>\n        </module>\n        <module name=\"MethodParamPad\">\n            <property name=\"tokens\"\n                      value=\"CTOR_DEF, LITERAL_NEW, METHOD_CALL, METHOD_DEF,\n                    SUPER_CTOR_CALL, ENUM_CONSTANT_DEF\"/>\n        </module>\n        <module name=\"NoWhitespaceBefore\">\n            <property name=\"tokens\"\n                      value=\"COMMA, SEMI, POST_INC, POST_DEC, DOT, ELLIPSIS,\n                    LABELED_STAT, METHOD_REF\"/>\n            <property name=\"allowLineBreaks\" value=\"true\"/>\n        </module>\n        <module name=\"ParenPad\">\n            <property name=\"tokens\"\n                      value=\"ANNOTATION, ANNOTATION_FIELD_DEF, CTOR_CALL, CTOR_DEF, DOT, ENUM_CONSTANT_DEF,\n                    EXPR, LITERAL_CATCH, LITERAL_DO, LITERAL_FOR, LITERAL_IF, LITERAL_NEW,\n                    LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_WHILE, METHOD_CALL,\n                    METHOD_DEF, QUESTION, RESOURCE_SPECIFICATION, SUPER_CTOR_CALL, LAMBDA\"/>\n        </module>\n        <module name=\"OperatorWrap\">\n            <property name=\"option\" value=\"NL\"/>\n            <property name=\"tokens\"\n                      value=\"BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,\n                    LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF \"/>\n        </module>\n        <module name=\"AnnotationLocation\">\n            <property name=\"id\" value=\"AnnotationLocationMostCases\"/>\n            <property name=\"tokens\"\n                      value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF\"/>\n        </module>\n        <module name=\"AnnotationLocation\">\n            <property name=\"id\" value=\"AnnotationLocationVariables\"/>\n            <property name=\"tokens\" value=\"VARIABLE_DEF\"/>\n            <property name=\"allowSamelineMultipleAnnotations\" value=\"true\"/>\n        </module>\n        <module name=\"NonEmptyAtclauseDescription\"/>\n        <module name=\"InvalidJavadocPosition\"/>\n        <module name=\"JavadocTagContinuationIndentation\"/>\n        <module name=\"SummaryJavadoc\">\n            <property name=\"forbiddenSummaryFragments\"\n                      value=\"^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )\"/>\n        </module>\n        <module name=\"JavadocParagraph\"/>\n        <module name=\"AtclauseOrder\">\n            <property name=\"tagOrder\" value=\"@param, @return, @throws, @deprecated\"/>\n            <property name=\"target\"\n                      value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF\"/>\n        </module>\n        <module name=\"JavadocMethod\">\n            <property name=\"accessModifiers\" value=\"public\"/>\n            <property name=\"allowMissingParamTags\" value=\"true\"/>\n            <property name=\"allowMissingReturnTag\" value=\"true\"/>\n            <property name=\"allowedAnnotations\" value=\"Override, Test\"/>\n            <property name=\"tokens\" value=\"METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF\"/>\n        </module>\n<!--        <module name=\"MissingJavadocMethod\">-->\n<!--            <property name=\"scope\" value=\"public\"/>-->\n<!--            <property name=\"minLineCount\" value=\"2\"/>-->\n<!--            <property name=\"allowedAnnotations\" value=\"Override, Test\"/>-->\n<!--            <property name=\"tokens\" value=\"METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF\"/>-->\n<!--        </module>-->\n        <module name=\"MethodName\">\n            <property name=\"format\" value=\"^[a-z][a-z0-9][a-zA-Z0-9_]*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Method name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"SingleLineJavadoc\">\n            <property name=\"ignoreInlineTags\" value=\"false\"/>\n        </module>\n        <module name=\"EmptyCatchBlock\">\n            <property name=\"exceptionVariableName\" value=\"ignored\"/>\n        </module>\n        <module name=\"CommentsIndentation\">\n            <property name=\"tokens\" value=\"SINGLE_LINE_COMMENT, BLOCK_COMMENT_BEGIN\"/>\n        </module>\n        <!-- https://checkstyle.org/config_filters.html#SuppressionXpathFilter -->\n        <module name=\"SuppressionXpathFilter\">\n            <property name=\"file\" value=\"${org.checkstyle.google.suppressionxpathfilter.config}\"\n                      default=\"checkstyle-xpath-suppressions.xml\" />\n            <property name=\"optional\" value=\"true\"/>\n        </module>\n    </module>\n</module>\n"
  },
  {
    "path": "kafka-ui-api/Dockerfile",
    "content": "#FROM azul/zulu-openjdk-alpine:17-jre-headless\nFROM azul/zulu-openjdk-alpine@sha256:a36679ac0d28cb835e2a8c00e1e0d95509c6c51c5081c7782b85edb1f37a771a\n\nRUN apk add --no-cache \\\n    # snappy codec\n    gcompat \\\n    # configuring timezones\n    tzdata\nRUN addgroup -S kafkaui && adduser -S kafkaui -G kafkaui\n\n# creating folder for dynamic config usage (certificates uploads, etc)\nRUN mkdir /etc/kafkaui/\nRUN chown kafkaui /etc/kafkaui\n\nUSER kafkaui\n\nARG JAR_FILE\nCOPY \"/target/${JAR_FILE}\" \"/kafka-ui-api.jar\"\n\nENV JAVA_OPTS=\n\nEXPOSE 8080\n\n# see JmxSslSocketFactory docs to understand why add-opens is needed\nCMD java --add-opens java.rmi/javax.rmi.ssl=ALL-UNNAMED  $JAVA_OPTS -jar kafka-ui-api.jar\n"
  },
  {
    "path": "kafka-ui-api/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>kafka-ui</artifactId>\n        <groupId>com.provectus</groupId>\n        <version>0.0.1-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>kafka-ui-api</artifactId>\n\n    <properties>\n        <jacoco.version>0.8.10</jacoco.version>\n        <sonar.java.coveragePlugin>jacoco</sonar.java.coveragePlugin>\n        <sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>\n        <sonar.jacoco.reportPath>${project.basedir}/target/jacoco.exec</sonar.jacoco.reportPath>\n        <sonar.coverage.jacoco.xmlReportPaths>${project.basedir}/target/site/jacoco/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths>\n        <sonar.language>java</sonar.language>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-webflux</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-security</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-actuator</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-oauth2-client</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.provectus</groupId>\n            <artifactId>kafka-ui-contract</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>com.provectus</groupId>\n            <artifactId>kafka-ui-serde-api</artifactId>\n            <version>${kafka-ui-serde-api.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.kafka</groupId>\n            <artifactId>kafka-clients</artifactId>\n            <version>${kafka-clients.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-lang3</artifactId>\n            <version>3.12.0</version>\n        </dependency>\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mapstruct</groupId>\n            <artifactId>mapstruct</artifactId>\n            <version>${org.mapstruct.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>io.confluent</groupId>\n            <artifactId>kafka-schema-registry-client</artifactId>\n            <version>${confluent.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>io.confluent</groupId>\n            <artifactId>kafka-avro-serializer</artifactId>\n            <version>${confluent.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>io.confluent</groupId>\n            <artifactId>kafka-json-schema-serializer</artifactId>\n            <version>${confluent.version}</version>\n            <exclusions>\n                <exclusion>\n                    <groupId>commons-collections</groupId>\n                    <artifactId>commons-collections</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n        <dependency>\n            <groupId>io.confluent</groupId>\n            <artifactId>kafka-protobuf-serializer</artifactId>\n            <version>${confluent.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>software.amazon.msk</groupId>\n            <artifactId>aws-msk-iam-auth</artifactId>\n            <version>1.1.7</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.apache.avro</groupId>\n            <artifactId>avro</artifactId>\n            <version>${avro.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-logging</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>io.projectreactor.addons</groupId>\n            <artifactId>reactor-extra</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.json</groupId>\n            <artifactId>json</artifactId>\n            <version>${org.json.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>io.micrometer</groupId>\n            <artifactId>micrometer-registry-prometheus</artifactId>\n            <scope>runtime</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>io.projectreactor</groupId>\n            <artifactId>reactor-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n            <version>${apache.commons.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-collections4</artifactId>\n            <version>4.4</version>\n        </dependency>\n        <dependency>\n            <groupId>org.testcontainers</groupId>\n            <artifactId>testcontainers</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.testcontainers</groupId>\n            <artifactId>kafka</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.testcontainers</groupId>\n            <artifactId>junit-jupiter</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-engine</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-core</artifactId>\n            <version>${mockito.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-junit-jupiter</artifactId>\n            <version>${mockito.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>net.bytebuddy</groupId>\n            <artifactId>byte-buddy</artifactId>\n            <version>${byte-buddy.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.assertj</groupId>\n            <artifactId>assertj-core</artifactId>\n            <version>${assertj.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.github.java-json-tools</groupId>\n            <artifactId>json-schema-validator</artifactId>\n            <version>2.2.14</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.squareup.okhttp3</groupId>\n            <artifactId>mockwebserver</artifactId>\n            <version>${okhttp3.mockwebserver.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.squareup.okhttp3</groupId>\n            <artifactId>okhttp</artifactId>\n            <version>${okhttp3.mockwebserver.version}</version>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-actuator</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.antlr</groupId>\n            <artifactId>antlr4-runtime</artifactId>\n            <version>${antlr4-maven-plugin.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.opendatadiscovery</groupId>\n            <artifactId>oddrn-generator-java</artifactId>\n            <version>${odd-oddrn-generator.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.opendatadiscovery</groupId>\n            <artifactId>ingestion-contract-client</artifactId>\n            <exclusions>\n                <exclusion>\n                    <groupId>org.springframework.boot</groupId>\n                    <artifactId>spring-boot-starter-webflux</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>io.projectreactor</groupId>\n                    <artifactId>reactor-core</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>io.projectreactor.ipc</groupId>\n                    <artifactId>reactor-netty</artifactId>\n                </exclusion>\n            </exclusions>\n            <version>${odd-oddrn-client.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.security</groupId>\n            <artifactId>spring-security-ldap</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.codehaus.groovy</groupId>\n            <artifactId>groovy-jsr223</artifactId>\n            <version>${groovy.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.codehaus.groovy</groupId>\n            <artifactId>groovy-json</artifactId>\n            <version>${groovy.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.datasketches</groupId>\n            <artifactId>datasketches-java</artifactId>\n            <version>${datasketches-java.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-devtools</artifactId>\n            <optional>true</optional>\n        </dependency>\n\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n                <version>${spring-boot.version}</version>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>repackage</goal>\n                            <goal>build-info</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <configuration>\n                    <annotationProcessorPaths>\n                        <path>\n                            <groupId>org.mapstruct</groupId>\n                            <artifactId>mapstruct-processor</artifactId>\n                            <version>${org.mapstruct.version}</version>\n                        </path>\n                        <path>\n                            <groupId>org.projectlombok</groupId>\n                            <artifactId>lombok</artifactId>\n                            <version>${org.projectlombok.version}</version>\n                        </path>\n                        <path>\n                            <groupId>org.projectlombok</groupId>\n                            <artifactId>lombok-mapstruct-binding</artifactId>\n                            <version>0.2.0</version>\n                        </path>\n                        <path>\n                            <groupId>org.springframework.boot</groupId>\n                            <artifactId>spring-boot-configuration-processor</artifactId>\n                            <version>${spring-boot.version}</version>\n                        </path>\n                    </annotationProcessorPaths>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <configuration>\n                    <argLine>@{argLine} --illegal-access=permit</argLine>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-checkstyle-plugin</artifactId>\n                <version>3.3.0</version>\n                <dependencies>\n                    <dependency>\n                        <groupId>com.puppycrawl.tools</groupId>\n                        <artifactId>checkstyle</artifactId>\n                        <version>10.3.1</version>\n                    </dependency>\n                </dependencies>\n                <executions>\n                    <execution>\n                        <id>checkstyle</id>\n                        <phase>validate</phase>\n                        <goals>\n                            <goal>check</goal>\n                        </goals>\n                        <configuration>\n                            <violationSeverity>warning</violationSeverity>\n                            <failOnViolation>true</failOnViolation>\n                            <failsOnError>true</failsOnError>\n                            <includeTestSourceDirectory>true</includeTestSourceDirectory>\n                            <configLocation>file:${basedir}/../etc/checkstyle/checkstyle.xml</configLocation>\n                            <headerLocation>file:${basedir}/../etc/checkstyle/apache-header.txt</headerLocation>\n                        </configuration>\n                    </execution>\n                </executions>\n\n            </plugin>\n            <plugin>\n                <groupId>org.antlr</groupId>\n                <artifactId>antlr4-maven-plugin</artifactId>\n                <version>${antlr4-maven-plugin.version}</version>\n                <configuration>\n                    <visitor>false</visitor>\n                </configuration>\n                <executions>\n                    <execution>\n                        <phase>generate-sources</phase>\n                        <goals>\n                            <goal>antlr4</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.jacoco</groupId>\n                <artifactId>jacoco-maven-plugin</artifactId>\n                <version>${jacoco.version}</version>\n                <executions>\n                    <execution>\n                        <id>prepare-agent</id>\n                        <goals>\n                            <goal>prepare-agent</goal>\n                        </goals>\n                    </execution>\n                    <execution>\n                        <id>report</id>\n                        <goals>\n                            <goal>report</goal>\n                        </goals>\n                        <configuration>\n                            <formats>\n                                <format>XML</format>\n                            </formats>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n\n    <profiles>\n        <profile>\n            <id>prod</id>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>pl.project13.maven</groupId>\n                        <artifactId>git-commit-id-plugin</artifactId>\n                        <version>4.9.10</version>\n                        <executions>\n                            <execution>\n                                <id>get-the-git-infos</id>\n                                <goals>\n                                    <goal>revision</goal>\n                                </goals>\n                                <phase>initialize</phase>\n                            </execution>\n                        </executions>\n                        <configuration>\n                            <generateGitPropertiesFile>true</generateGitPropertiesFile>\n                            <generateGitPropertiesFilename>${project.build.outputDirectory}/git.properties</generateGitPropertiesFilename>\n                            <includeOnlyProperties>\n                                <includeOnlyProperty>^git.build.(time|version)$</includeOnlyProperty>\n                                <includeOnlyProperty>^git.commit.id.(abbrev|full)$</includeOnlyProperty>\n                            </includeOnlyProperties>\n                            <commitIdGenerationMode>full</commitIdGenerationMode>\n                        </configuration>\n                    </plugin>\n                    <plugin>\n                        <artifactId>maven-resources-plugin</artifactId>\n                        <executions>\n                            <execution>\n                                <id>copy-resources</id>\n                                <phase>process-classes</phase>\n                                <goals>\n                                    <goal>copy-resources</goal>\n                                </goals>\n                                <configuration>\n                                    <outputDirectory>${basedir}/target/classes/static</outputDirectory>\n                                    <resources>\n                                        <resource>\n                                            <directory>../kafka-ui-react-app/build</directory>\n                                        </resource>\n                                    </resources>\n                                </configuration>\n                            </execution>\n                        </executions>\n                    </plugin>\n                    <plugin>\n                        <groupId>com.github.eirslett</groupId>\n                        <artifactId>frontend-maven-plugin</artifactId>\n                        <version>${frontend-maven-plugin.version}</version>\n                        <configuration>\n                            <skip>${skipUIBuild}</skip>\n                            <workingDirectory>../kafka-ui-react-app</workingDirectory>\n                            <environmentVariables>\n                                <VITE_TAG>${project.version}</VITE_TAG>\n                                <VITE_COMMIT>${git.commit.id.abbrev}</VITE_COMMIT>\n                            </environmentVariables>\n                        </configuration>\n                        <executions>\n                            <execution>\n                                <id>install node and pnpm</id>\n                                <goals>\n                                    <goal>install-node-and-pnpm</goal>\n                                </goals>\n                                <configuration>\n                                    <nodeVersion>${node.version}</nodeVersion>\n                                    <pnpmVersion>${pnpm.version}</pnpmVersion>\n                                </configuration>\n                            </execution>\n                            <execution>\n                                <id>pnpm install</id>\n                                <goals>\n                                    <goal>pnpm</goal>\n                                </goals>\n                                <configuration>\n                                    <arguments>install</arguments>\n                                </configuration>\n                            </execution>\n                            <execution>\n                                <id>pnpm build</id>\n                                <goals>\n                                    <goal>pnpm</goal>\n                                </goals>\n                                <configuration>\n                                    <arguments>build</arguments>\n                                </configuration>\n                            </execution>\n                        </executions>\n                    </plugin>\n                    <plugin>\n                        <groupId>io.fabric8</groupId>\n                        <artifactId>docker-maven-plugin</artifactId>\n                        <version>${fabric8-maven-plugin.version}</version>\n                        <configuration>\n                            <verbose>true</verbose>\n                            <images>\n                                <image>\n                                    <name>provectuslabs/kafka-ui:${git.revision}</name>\n                                    <build>\n                                        <contextDir>${project.basedir}</contextDir>\n                                        <args>\n                                            <JAR_FILE>${project.build.finalName}.jar</JAR_FILE>\n                                        </args>\n                                    </build>\n                                </image>\n                            </images>\n                        </configuration>\n                        <executions>\n                            <execution>\n                                <id>default</id>\n                                <phase>package</phase>\n                                <goals>\n                                    <goal>build</goal>\n                                </goals>\n                            </execution>\n                        </executions>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n    </profiles>\n\n</project>\n"
  },
  {
    "path": "kafka-ui-api/src/main/antlr4/ksql/KsqlGrammar.g4",
    "content": "grammar KsqlGrammar;\n\ntokens {\n    DELIMITER\n}\n\n@lexer::members {\n    public static final int COMMENTS = 2;\n    public static final int WHITESPACE = 3;\n    public static final int DIRECTIVES = 4;\n}\n\nstatements\n    : (singleStatement)* EOF\n    ;\n\ntestStatement\n    : (singleStatement | assertStatement ';' | runScript ';') EOF?\n    ;\n\nsingleStatement\n    : statement ';'\n    ;\n\nsingleExpression\n    : expression EOF\n    ;\n\nstatement\n    : query                                                                 #queryStatement\n    | (LIST | SHOW) PROPERTIES                                              #listProperties\n    | (LIST | SHOW) ALL? TOPICS EXTENDED?                                   #listTopics\n    | (LIST | SHOW) STREAMS EXTENDED?                                       #listStreams\n    | (LIST | SHOW) TABLES EXTENDED?                                        #listTables\n    | (LIST | SHOW) FUNCTIONS                                               #listFunctions\n    | (LIST | SHOW) (SOURCE | SINK)? CONNECTORS                             #listConnectors\n    | (LIST | SHOW) CONNECTOR PLUGINS                                       #listConnectorPlugins\n    | (LIST | SHOW) TYPES                                                   #listTypes\n    | (LIST | SHOW) VARIABLES                                               #listVariables\n    | DESCRIBE sourceName EXTENDED?                                         #showColumns\n    | DESCRIBE STREAMS EXTENDED?                                            #describeStreams\n    | DESCRIBE FUNCTION identifier                                          #describeFunction\n    | DESCRIBE CONNECTOR identifier                                         #describeConnector\n    | PRINT (identifier| STRING) printClause                                #printTopic\n    | (LIST | SHOW) QUERIES EXTENDED?                                       #listQueries\n    | TERMINATE identifier                                                  #terminateQuery\n    | TERMINATE ALL                                                         #terminateQuery\n    | SET STRING EQ STRING                                                  #setProperty\n    | UNSET STRING                                                          #unsetProperty\n    | DEFINE variableName EQ variableValue                                  #defineVariable\n    | UNDEFINE variableName                                                 #undefineVariable\n    | CREATE (OR REPLACE)? (SOURCE)? STREAM (IF NOT EXISTS)? sourceName\n                (tableElements)?\n                (WITH tableProperties)?                                     #createStream\n    | CREATE (OR REPLACE)? STREAM (IF NOT EXISTS)? sourceName\n            (WITH tableProperties)? AS query                                #createStreamAs\n    | CREATE (OR REPLACE)? (SOURCE)? TABLE (IF NOT EXISTS)? sourceName\n                    (tableElements)?\n                    (WITH tableProperties)?                                 #createTable\n    | CREATE (OR REPLACE)? TABLE (IF NOT EXISTS)? sourceName\n            (WITH tableProperties)? AS query                                #createTableAs\n    | CREATE (SINK | SOURCE) CONNECTOR (IF NOT EXISTS)? identifier\n             WITH tableProperties                                           #createConnector\n    | INSERT INTO sourceName (WITH tableProperties)? query                  #insertInto\n    | INSERT INTO sourceName (columns)? VALUES values                       #insertValues\n    | DROP STREAM (IF EXISTS)? sourceName (DELETE TOPIC)?                   #dropStream\n    | DROP TABLE (IF EXISTS)? sourceName (DELETE TOPIC)?                    #dropTable\n    | DROP CONNECTOR (IF EXISTS)? identifier                                #dropConnector\n    | EXPLAIN  (statement | identifier)                                     #explain\n    | CREATE TYPE (IF NOT EXISTS)? identifier AS type                       #registerType\n    | DROP TYPE (IF EXISTS)? identifier                                     #dropType\n    | ALTER (STREAM | TABLE) sourceName alterOption (',' alterOption)*      #alterSource\n    ;\n\nassertStatement\n    : ASSERT VALUES sourceName (columns)? VALUES values                     #assertValues\n    | ASSERT NULL VALUES sourceName (columns)? KEY values                   #assertTombstone\n    | ASSERT STREAM sourceName (tableElements)? (WITH tableProperties)?     #assertStream\n    | ASSERT TABLE sourceName (tableElements)? (WITH tableProperties)?      #assertTable\n    ;\n\nrunScript\n    : RUN SCRIPT STRING\n    ;\n\nquery\n    : SELECT selectItem (',' selectItem)*\n      FROM from=relation\n      (WINDOW  windowExpression)?\n      (WHERE where=booleanExpression)?\n      (GROUP BY groupBy)?\n      (PARTITION BY partitionBy)?\n      (HAVING having=booleanExpression)?\n      (EMIT resultMaterialization)?\n      limitClause?\n    ;\n\nresultMaterialization\n    : CHANGES\n    | FINAL\n    ;\n\nalterOption\n    : ADD (COLUMN)? identifier type\n    ;\n\ntableElements\n    : '(' tableElement (',' tableElement)* ')'\n    ;\n\ntableElement\n    : identifier type columnConstraints?\n    ;\n\ncolumnConstraints\n    : ((PRIMARY)? KEY)\n    | HEADERS\n    | HEADER '(' STRING ')'\n    ;\n\ntableProperties\n    : '(' tableProperty (',' tableProperty)* ')'\n    ;\n\ntableProperty\n    : (identifier | STRING) EQ literal\n    ;\n\nprintClause\n      : (FROM BEGINNING)? intervalClause? limitClause?\n      ;\n\nintervalClause\n    : (INTERVAL | SAMPLE) number\n    ;\n\nlimitClause\n    : LIMIT number\n    ;\n\nretentionClause\n    : RETENTION number windowUnit\n    ;\n\ngracePeriodClause\n    : GRACE PERIOD number windowUnit\n    ;\n\nwindowExpression\n    : (IDENTIFIER)?\n     ( tumblingWindowExpression | hoppingWindowExpression | sessionWindowExpression )\n    ;\n\ntumblingWindowExpression\n    : TUMBLING '(' SIZE number windowUnit (',' retentionClause)? (',' gracePeriodClause)?')'\n    ;\n\nhoppingWindowExpression\n    : HOPPING '(' SIZE number windowUnit ',' ADVANCE BY number windowUnit (',' retentionClause)? (',' gracePeriodClause)?')'\n    ;\n\nsessionWindowExpression\n    : SESSION '(' number windowUnit (',' retentionClause)? (',' gracePeriodClause)?')'\n    ;\n\nwindowUnit\n    : DAY\n    | HOUR\n    | MINUTE\n    | SECOND\n    | MILLISECOND\n    | DAYS\n    | HOURS\n    | MINUTES\n    | SECONDS\n    | MILLISECONDS\n    ;\n\ngroupBy\n    : valueExpression (',' valueExpression)*\n    | '(' (valueExpression (',' valueExpression)*)? ')'\n    ;\n\npartitionBy\n    : valueExpression (',' valueExpression)*\n    | '(' (valueExpression (',' valueExpression)*)? ')'\n    ;\n\nvalues\n    : '(' (valueExpression (',' valueExpression)*)? ')'\n    ;\n\nselectItem\n    : expression (AS? identifier)?  #selectSingle\n    | identifier '.' ASTERISK       #selectAll\n    | ASTERISK                      #selectAll\n    ;\n\nrelation\n    : left=aliasedRelation joinedSource+  #joinRelation\n    | aliasedRelation                     #relationDefault\n    ;\n\njoinedSource\n    : joinType JOIN aliasedRelation joinWindow? joinCriteria\n    ;\n\njoinType\n    : INNER? #innerJoin\n    | FULL OUTER? #outerJoin\n    | LEFT OUTER? #leftJoin\n    ;\n\njoinWindow\n    : WITHIN withinExpression\n    ;\n\nwithinExpression\n    : '(' joinWindowSize ',' joinWindowSize ')' (gracePeriodClause)?  # joinWindowWithBeforeAndAfter\n    | joinWindowSize (gracePeriodClause)?                             # singleJoinWindow\n    ;\n\njoinWindowSize\n    : number windowUnit\n    ;\n\njoinCriteria\n    : ON booleanExpression\n    ;\n\naliasedRelation\n    : relationPrimary (AS? sourceName)?\n    ;\n\ncolumns\n    : '(' identifier (',' identifier)* ')'\n    ;\n\nrelationPrimary\n    : sourceName                                                  #tableName\n    ;\n\nexpression\n    : booleanExpression\n    ;\n\nbooleanExpression\n    : predicated                                                   #booleanDefault\n    | NOT booleanExpression                                        #logicalNot\n    | left=booleanExpression operator=AND right=booleanExpression  #logicalBinary\n    | left=booleanExpression operator=OR right=booleanExpression   #logicalBinary\n    ;\n\npredicated\n    : valueExpression predicate[$valueExpression.ctx]?\n    ;\n\npredicate[ParserRuleContext value]\n    : comparisonOperator right=valueExpression                            #comparison\n    | NOT? BETWEEN lower=valueExpression AND upper=valueExpression        #between\n    | NOT? IN '(' expression (',' expression)* ')'                        #inList\n    | NOT? LIKE pattern=valueExpression\t(ESCAPE escape=STRING)?   \t\t    #like\n    | IS NOT? NULL                                                        #nullPredicate\n    | IS NOT? DISTINCT FROM right=valueExpression                         #distinctFrom\n    ;\n\nvalueExpression\n    : primaryExpression                                                                 #valueExpressionDefault\n    | valueExpression AT timeZoneSpecifier                                              #atTimeZone\n    | operator=(MINUS | PLUS) valueExpression                                           #arithmeticUnary\n    | left=valueExpression operator=(ASTERISK | SLASH | PERCENT) right=valueExpression  #arithmeticBinary\n    | left=valueExpression operator=(PLUS | MINUS) right=valueExpression                #arithmeticBinary\n    | left=valueExpression CONCAT right=valueExpression                                 #concatenation\n    ;\n\nprimaryExpression\n    : literal                                                                             #literalExpression\n    | identifier STRING                                                                   #typeConstructor\n    | CASE valueExpression whenClause+ (ELSE elseExpression=expression)? END              #simpleCase\n    | CASE whenClause+ (ELSE elseExpression=expression)? END                              #searchedCase\n    | CAST '(' expression AS type ')'                                                     #cast\n    | ARRAY '[' (expression (',' expression)*)? ']'                                       #arrayConstructor\n    | MAP '(' (expression ASSIGN expression (',' expression ASSIGN expression)*)? ')'     #mapConstructor\n    | STRUCT '(' (identifier ASSIGN expression (',' identifier ASSIGN expression)*)? ')'  #structConstructor\n    | identifier '(' ASTERISK ')'                              \t\t                        #functionCall\n    | identifier '(' (functionArgument (',' functionArgument)* (',' lambdaFunction)*)? ')' #functionCall\n    | value=primaryExpression '[' index=valueExpression ']'                               #subscript\n    | identifier                                                                          #columnReference\n    | identifier '.' identifier                                                           #qualifiedColumnReference\n    | base=primaryExpression STRUCT_FIELD_REF fieldName=identifier                        #dereference\n    | '(' expression ')'                                                                  #parenthesizedExpression\n    ;\n\nfunctionArgument\n    : expression\n    | windowUnit\n    ;\n\ntimeZoneSpecifier\n    : TIME ZONE STRING    #timeZoneString\n    ;\n\ncomparisonOperator\n    : EQ | NEQ | LT | LTE | GT | GTE\n    ;\n\nbooleanValue\n    : TRUE | FALSE\n    ;\n\ntype\n    : type ARRAY\n    | ARRAY '<' type '>'\n    | MAP '<' type ',' type '>'\n    | STRUCT '<' (identifier type (',' identifier type)*)? '>'\n    | DECIMAL '(' number ',' number ')'\n    | baseType ('(' typeParameter (',' typeParameter)* ')')?\n    ;\n\ntypeParameter\n    : INTEGER_VALUE | 'STRING'\n    ;\n\nbaseType\n    : identifier\n    ;\n\nwhenClause\n    : WHEN condition=expression THEN result=expression\n    ;\n\nidentifier\n    : VARIABLE               #variableIdentifier\n    | IDENTIFIER             #unquotedIdentifier\n    | QUOTED_IDENTIFIER      #quotedIdentifierAlternative\n    | nonReserved            #unquotedIdentifier\n    | BACKQUOTED_IDENTIFIER  #backQuotedIdentifier\n    | DIGIT_IDENTIFIER       #digitIdentifier\n    ;\n\nlambdaFunction\n    :  identifier '=>' expression                            #lambda\n    | '(' identifier (',' identifier)*  ')' '=>' expression  #lambda\n    ;\n\nvariableName\n    : IDENTIFIER\n    ;\n\nvariableValue\n    : STRING\n    ;\n\nsourceName\n    : identifier\n    ;\n\nnumber\n    : MINUS? DECIMAL_VALUE         #decimalLiteral\n    | MINUS? FLOATING_POINT_VALUE  #floatLiteral\n    | MINUS? INTEGER_VALUE         #integerLiteral\n    ;\n\nliteral\n    : NULL                                                                           #nullLiteral\n    | number                                                                         #numericLiteral\n    | booleanValue                                                                   #booleanLiteral\n    | STRING                                                                         #stringLiteral\n    | VARIABLE                                                                       #variableLiteral\n    ;\n\nnonReserved\n    : SHOW | TABLES | COLUMNS | COLUMN | PARTITIONS | FUNCTIONS | FUNCTION | SESSION\n    | STRUCT | MAP | ARRAY | PARTITION\n    | INTEGER | DATE | TIME | TIMESTAMP | INTERVAL | ZONE | 'STRING'\n    | YEAR | MONTH | DAY | HOUR | MINUTE | SECOND\n    | EXPLAIN | ANALYZE | TYPE | TYPES\n    | SET | RESET\n    | IF\n    | SOURCE | SINK\n    | PRIMARY | KEY\n    | EMIT\n    | CHANGES\n    | FINAL\n    | ESCAPE\n    | REPLACE\n    | ASSERT\n    | ALTER\n    | ADD\n    ;\n\nEMIT: 'EMIT';\nCHANGES: 'CHANGES';\nFINAL: 'FINAL';\nSELECT: 'SELECT';\nFROM: 'FROM';\nAS: 'AS';\nALL: 'ALL';\nDISTINCT: 'DISTINCT';\nWHERE: 'WHERE';\nWITHIN: 'WITHIN';\nWINDOW: 'WINDOW';\nGROUP: 'GROUP';\nBY: 'BY';\nHAVING: 'HAVING';\nLIMIT: 'LIMIT';\nAT: 'AT';\nOR: 'OR';\nAND: 'AND';\nIN: 'IN';\nNOT: 'NOT';\nEXISTS: 'EXISTS';\nBETWEEN: 'BETWEEN';\nLIKE: 'LIKE';\nESCAPE: 'ESCAPE';\nIS: 'IS';\nNULL: 'NULL';\nTRUE: 'TRUE';\nFALSE: 'FALSE';\nINTEGER: 'INTEGER';\nDATE: 'DATE';\nTIME: 'TIME';\nTIMESTAMP: 'TIMESTAMP';\nINTERVAL: 'INTERVAL';\nYEAR: 'YEAR';\nMONTH: 'MONTH';\nDAY: 'DAY';\nHOUR: 'HOUR';\nMINUTE: 'MINUTE';\nSECOND: 'SECOND';\nMILLISECOND: 'MILLISECOND';\nYEARS: 'YEARS';\nMONTHS: 'MONTHS';\nDAYS: 'DAYS';\nHOURS: 'HOURS';\nMINUTES: 'MINUTES';\nSECONDS: 'SECONDS';\nMILLISECONDS: 'MILLISECONDS';\nZONE: 'ZONE';\nTUMBLING: 'TUMBLING';\nHOPPING: 'HOPPING';\nSIZE: 'SIZE';\nADVANCE: 'ADVANCE';\nRETENTION: 'RETENTION';\nGRACE: 'GRACE';\nPERIOD: 'PERIOD';\nCASE: 'CASE';\nWHEN: 'WHEN';\nTHEN: 'THEN';\nELSE: 'ELSE';\nEND: 'END';\nJOIN: 'JOIN';\nFULL: 'FULL';\nOUTER: 'OUTER';\nINNER: 'INNER';\nLEFT: 'LEFT';\nRIGHT: 'RIGHT';\nON: 'ON';\nPARTITION: 'PARTITION';\nSTRUCT: 'STRUCT';\nWITH: 'WITH';\nVALUES: 'VALUES';\nCREATE: 'CREATE';\nTABLE: 'TABLE';\nTOPIC: 'TOPIC';\nSTREAM: 'STREAM';\nSTREAMS: 'STREAMS';\nINSERT: 'INSERT';\nDELETE: 'DELETE';\nINTO: 'INTO';\nDESCRIBE: 'DESCRIBE';\nEXTENDED: 'EXTENDED';\nPRINT: 'PRINT';\nEXPLAIN: 'EXPLAIN';\nANALYZE: 'ANALYZE';\nTYPE: 'TYPE';\nTYPES: 'TYPES';\nCAST: 'CAST';\nSHOW: 'SHOW';\nLIST: 'LIST';\nTABLES: 'TABLES';\nTOPICS: 'TOPICS';\nQUERY: 'QUERY';\nQUERIES: 'QUERIES';\nTERMINATE: 'TERMINATE';\nLOAD: 'LOAD';\nCOLUMNS: 'COLUMNS';\nCOLUMN: 'COLUMN';\nPARTITIONS: 'PARTITIONS';\nFUNCTIONS: 'FUNCTIONS';\nFUNCTION: 'FUNCTION';\nDROP: 'DROP';\nTO: 'TO';\nRENAME: 'RENAME';\nARRAY: 'ARRAY';\nMAP: 'MAP';\nSET: 'SET';\nDEFINE: 'DEFINE';\nUNDEFINE: 'UNDEFINE';\nRESET: 'RESET';\nSESSION: 'SESSION';\nSAMPLE: 'SAMPLE';\nEXPORT: 'EXPORT';\nCATALOG: 'CATALOG';\nPROPERTIES: 'PROPERTIES';\nBEGINNING: 'BEGINNING';\nUNSET: 'UNSET';\nRUN: 'RUN';\nSCRIPT: 'SCRIPT';\nDECIMAL: 'DECIMAL';\nKEY: 'KEY';\nCONNECTOR: 'CONNECTOR';\nCONNECTORS: 'CONNECTORS';\nSINK: 'SINK';\nSOURCE: 'SOURCE';\nNAMESPACE: 'NAMESPACE';\nMATERIALIZED: 'MATERIALIZED';\nVIEW: 'VIEW';\nPRIMARY: 'PRIMARY';\nREPLACE: 'REPLACE';\nASSERT: 'ASSERT';\nADD: 'ADD';\nALTER: 'ALTER';\nVARIABLES: 'VARIABLES';\nPLUGINS: 'PLUGINS';\nHEADERS: 'HEADERS';\nHEADER: 'HEADER';\n\nIF: 'IF';\n\nEQ  : '=';\nNEQ : '<>' | '!=';\nLT  : '<';\nLTE : '<=';\nGT  : '>';\nGTE : '>=';\n\nPLUS: '+';\nMINUS: '-';\nASTERISK: '*';\nSLASH: '/';\nPERCENT: '%';\nCONCAT: '||';\n\nASSIGN: ':=';\nSTRUCT_FIELD_REF: '->';\n\nLAMBDA_EXPRESSION: '=>';\n\nSTRING\n    : '\\'' ( ~'\\'' | '\\'\\'' )* '\\''\n    ;\n\nINTEGER_VALUE\n    : DIGIT+\n    ;\n\nDECIMAL_VALUE\n    : DIGIT+ '.' DIGIT*\n    | '.' DIGIT+\n    ;\n\nFLOATING_POINT_VALUE\n    : DIGIT+ ('.' DIGIT*)? EXPONENT\n    | '.' DIGIT+ EXPONENT\n    ;\n\nIDENTIFIER\n    : (LETTER | '_') (LETTER | DIGIT | '_' | '@' )*\n    ;\n\nDIGIT_IDENTIFIER\n    : DIGIT (LETTER | DIGIT | '_' | '@' )+\n    ;\n\nQUOTED_IDENTIFIER\n    : '\"' ( ~'\"' | '\"\"' )* '\"'\n    ;\n\nBACKQUOTED_IDENTIFIER\n    : '`' ( ~'`' | '``' )* '`'\n    ;\n\nVARIABLE\n    : '${' IDENTIFIER '}'\n    ;\n\nfragment EXPONENT\n    : 'E' [+-]? DIGIT+\n    ;\n\nfragment DIGIT\n    : [0-9]\n    ;\n\nfragment LETTER\n    : [A-Z]\n    ;\n\nSIMPLE_COMMENT\n    : '--' ~'@' ~[\\r\\n]* '\\r'? '\\n'? -> channel(2) // channel(COMMENTS)\n    ;\n\nDIRECTIVE_COMMENT\n    : '--@' ~[\\r\\n]* '\\r'? '\\n'? -> channel(4) // channel(DIRECTIVES)\n    ;\n\nBRACKETED_COMMENT\n    : '/*' .*? '*/' -> channel(2) // channel(COMMENTS)\n    ;\n\nWS\n    : [ \\r\\n\\t]+ -> channel(3) // channel(WHITESPACE)\n    ;\n\n// Catch-all for anything we can't recognize.\n// We use this to be able to ignore and recover all the text\n// when splitting statements with DelimiterLexer\nUNRECOGNIZED\n    : .\n    ;\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/KafkaUiApplication.java",
    "content": "package com.provectus.kafka.ui;\n\nimport com.provectus.kafka.ui.util.DynamicConfigOperations;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration;\nimport org.springframework.boot.builder.SpringApplicationBuilder;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.springframework.scheduling.annotation.EnableAsync;\nimport org.springframework.scheduling.annotation.EnableScheduling;\n\n@SpringBootApplication(exclude = LdapAutoConfiguration.class)\n@EnableScheduling\n@EnableAsync\npublic class KafkaUiApplication {\n\n  public static void main(String[] args) {\n    startApplication(args);\n  }\n\n  public static ConfigurableApplicationContext startApplication(String[] args) {\n    return new SpringApplicationBuilder(KafkaUiApplication.class)\n        .initializers(DynamicConfigOperations.dynamicConfigPropertiesInitializer())\n        .build()\n        .run(args);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/client/RetryingKafkaConnectClient.java",
    "content": "package com.provectus.kafka.ui.client;\n\nimport static com.provectus.kafka.ui.config.ClustersProperties.ConnectCluster;\n\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.connect.ApiClient;\nimport com.provectus.kafka.ui.connect.api.KafkaConnectClientApi;\nimport com.provectus.kafka.ui.connect.model.Connector;\nimport com.provectus.kafka.ui.connect.model.ConnectorPlugin;\nimport com.provectus.kafka.ui.connect.model.ConnectorPluginConfigValidationResponse;\nimport com.provectus.kafka.ui.connect.model.ConnectorStatus;\nimport com.provectus.kafka.ui.connect.model.ConnectorTask;\nimport com.provectus.kafka.ui.connect.model.ConnectorTopics;\nimport com.provectus.kafka.ui.connect.model.NewConnector;\nimport com.provectus.kafka.ui.connect.model.TaskStatus;\nimport com.provectus.kafka.ui.exception.KafkaConnectConflictReponseException;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.util.WebClientConfigurator;\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.Map;\nimport javax.annotation.Nullable;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.util.unit.DataSize;\nimport org.springframework.web.client.RestClientException;\nimport org.springframework.web.reactive.function.client.WebClient;\nimport org.springframework.web.reactive.function.client.WebClientResponseException;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\n\n@Slf4j\npublic class RetryingKafkaConnectClient extends KafkaConnectClientApi {\n  private static final int MAX_RETRIES = 5;\n  private static final Duration RETRIES_DELAY = Duration.ofMillis(200);\n\n  public RetryingKafkaConnectClient(ConnectCluster config,\n                                    @Nullable ClustersProperties.TruststoreConfig truststoreConfig,\n                                    DataSize maxBuffSize) {\n    super(new RetryingApiClient(config, truststoreConfig, maxBuffSize));\n  }\n\n  private static Retry conflictCodeRetry() {\n    return Retry\n        .fixedDelay(MAX_RETRIES, RETRIES_DELAY)\n        .filter(e -> e instanceof WebClientResponseException.Conflict)\n        .onRetryExhaustedThrow((spec, signal) ->\n            new KafkaConnectConflictReponseException(\n                (WebClientResponseException.Conflict) signal.failure()));\n  }\n\n  private static <T> Mono<T> withRetryOnConflict(Mono<T> publisher) {\n    return publisher.retryWhen(conflictCodeRetry());\n  }\n\n  private static <T> Flux<T> withRetryOnConflict(Flux<T> publisher) {\n    return publisher.retryWhen(conflictCodeRetry());\n  }\n\n  private static <T> Mono<T> withBadRequestErrorHandling(Mono<T> publisher) {\n    return publisher\n        .onErrorResume(WebClientResponseException.BadRequest.class, e ->\n            Mono.error(new ValidationException(\"Invalid configuration\")))\n        .onErrorResume(WebClientResponseException.InternalServerError.class, e ->\n            Mono.error(new ValidationException(\"Invalid configuration\")));\n  }\n\n  @Override\n  public Mono<Connector> createConnector(NewConnector newConnector) throws RestClientException {\n    return withBadRequestErrorHandling(\n        super.createConnector(newConnector)\n    );\n  }\n\n  @Override\n  public Mono<Connector> setConnectorConfig(String connectorName, Map<String, Object> requestBody)\n      throws RestClientException {\n    return withBadRequestErrorHandling(\n        super.setConnectorConfig(connectorName, requestBody)\n    );\n  }\n\n  @Override\n  public Mono<ResponseEntity<Connector>> createConnectorWithHttpInfo(NewConnector newConnector)\n      throws WebClientResponseException {\n    return withRetryOnConflict(super.createConnectorWithHttpInfo(newConnector));\n  }\n\n  @Override\n  public Mono<Void> deleteConnector(String connectorName) throws WebClientResponseException {\n    return withRetryOnConflict(super.deleteConnector(connectorName));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> deleteConnectorWithHttpInfo(String connectorName)\n      throws WebClientResponseException {\n    return withRetryOnConflict(super.deleteConnectorWithHttpInfo(connectorName));\n  }\n\n\n  @Override\n  public Mono<Connector> getConnector(String connectorName) throws WebClientResponseException {\n    return withRetryOnConflict(super.getConnector(connectorName));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Connector>> getConnectorWithHttpInfo(String connectorName)\n      throws WebClientResponseException {\n    return withRetryOnConflict(super.getConnectorWithHttpInfo(connectorName));\n  }\n\n  @Override\n  public Mono<Map<String, Object>> getConnectorConfig(String connectorName) throws WebClientResponseException {\n    return withRetryOnConflict(super.getConnectorConfig(connectorName));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Map<String, Object>>> getConnectorConfigWithHttpInfo(String connectorName)\n      throws WebClientResponseException {\n    return withRetryOnConflict(super.getConnectorConfigWithHttpInfo(connectorName));\n  }\n\n  @Override\n  public Flux<ConnectorPlugin> getConnectorPlugins() throws WebClientResponseException {\n    return withRetryOnConflict(super.getConnectorPlugins());\n  }\n\n  @Override\n  public Mono<ResponseEntity<List<ConnectorPlugin>>> getConnectorPluginsWithHttpInfo()\n      throws WebClientResponseException {\n    return withRetryOnConflict(super.getConnectorPluginsWithHttpInfo());\n  }\n\n  @Override\n  public Mono<ConnectorStatus> getConnectorStatus(String connectorName) throws WebClientResponseException {\n    return withRetryOnConflict(super.getConnectorStatus(connectorName));\n  }\n\n  @Override\n  public Mono<ResponseEntity<ConnectorStatus>> getConnectorStatusWithHttpInfo(String connectorName)\n      throws WebClientResponseException {\n    return withRetryOnConflict(super.getConnectorStatusWithHttpInfo(connectorName));\n  }\n\n  @Override\n  public Mono<TaskStatus> getConnectorTaskStatus(String connectorName, Integer taskId)\n      throws WebClientResponseException {\n    return withRetryOnConflict(super.getConnectorTaskStatus(connectorName, taskId));\n  }\n\n  @Override\n  public Mono<ResponseEntity<TaskStatus>> getConnectorTaskStatusWithHttpInfo(String connectorName, Integer taskId)\n      throws WebClientResponseException {\n    return withRetryOnConflict(super.getConnectorTaskStatusWithHttpInfo(connectorName, taskId));\n  }\n\n  @Override\n  public Flux<ConnectorTask> getConnectorTasks(String connectorName) throws WebClientResponseException {\n    return withRetryOnConflict(super.getConnectorTasks(connectorName));\n  }\n\n  @Override\n  public Mono<ResponseEntity<List<ConnectorTask>>> getConnectorTasksWithHttpInfo(String connectorName)\n      throws WebClientResponseException {\n    return withRetryOnConflict(super.getConnectorTasksWithHttpInfo(connectorName));\n  }\n\n  @Override\n  public Mono<Map<String, ConnectorTopics>> getConnectorTopics(String connectorName) throws WebClientResponseException {\n    return withRetryOnConflict(super.getConnectorTopics(connectorName));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Map<String, ConnectorTopics>>> getConnectorTopicsWithHttpInfo(String connectorName)\n      throws WebClientResponseException {\n    return withRetryOnConflict(super.getConnectorTopicsWithHttpInfo(connectorName));\n  }\n\n  @Override\n  public Flux<String> getConnectors(String search) throws WebClientResponseException {\n    return withRetryOnConflict(super.getConnectors(search));\n  }\n\n  @Override\n  public Mono<ResponseEntity<List<String>>> getConnectorsWithHttpInfo(String search) throws WebClientResponseException {\n    return withRetryOnConflict(super.getConnectorsWithHttpInfo(search));\n  }\n\n  @Override\n  public Mono<Void> pauseConnector(String connectorName) throws WebClientResponseException {\n    return withRetryOnConflict(super.pauseConnector(connectorName));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> pauseConnectorWithHttpInfo(String connectorName) throws WebClientResponseException {\n    return withRetryOnConflict(super.pauseConnectorWithHttpInfo(connectorName));\n  }\n\n  @Override\n  public Mono<Void> restartConnector(String connectorName, Boolean includeTasks, Boolean onlyFailed)\n      throws WebClientResponseException {\n    return withRetryOnConflict(super.restartConnector(connectorName, includeTasks, onlyFailed));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> restartConnectorWithHttpInfo(String connectorName, Boolean includeTasks,\n                                                                 Boolean onlyFailed) throws WebClientResponseException {\n    return withRetryOnConflict(super.restartConnectorWithHttpInfo(connectorName, includeTasks, onlyFailed));\n  }\n\n  @Override\n  public Mono<Void> restartConnectorTask(String connectorName, Integer taskId) throws WebClientResponseException {\n    return withRetryOnConflict(super.restartConnectorTask(connectorName, taskId));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> restartConnectorTaskWithHttpInfo(String connectorName, Integer taskId)\n      throws WebClientResponseException {\n    return withRetryOnConflict(super.restartConnectorTaskWithHttpInfo(connectorName, taskId));\n  }\n\n  @Override\n  public Mono<Void> resumeConnector(String connectorName) throws WebClientResponseException {\n    return super.resumeConnector(connectorName);\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> resumeConnectorWithHttpInfo(String connectorName)\n      throws WebClientResponseException {\n    return withRetryOnConflict(super.resumeConnectorWithHttpInfo(connectorName));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Connector>> setConnectorConfigWithHttpInfo(String connectorName,\n                                                                        Map<String, Object> requestBody)\n      throws WebClientResponseException {\n    return withRetryOnConflict(super.setConnectorConfigWithHttpInfo(connectorName, requestBody));\n  }\n\n  @Override\n  public Mono<ConnectorPluginConfigValidationResponse> validateConnectorPluginConfig(String pluginName,\n                                                                                     Map<String, Object> requestBody)\n      throws WebClientResponseException {\n    return withRetryOnConflict(super.validateConnectorPluginConfig(pluginName, requestBody));\n  }\n\n  @Override\n  public Mono<ResponseEntity<ConnectorPluginConfigValidationResponse>> validateConnectorPluginConfigWithHttpInfo(\n      String pluginName, Map<String, Object> requestBody) throws WebClientResponseException {\n    return withRetryOnConflict(super.validateConnectorPluginConfigWithHttpInfo(pluginName, requestBody));\n  }\n\n  private static class RetryingApiClient extends ApiClient {\n\n    public RetryingApiClient(ConnectCluster config,\n                             ClustersProperties.TruststoreConfig truststoreConfig,\n                             DataSize maxBuffSize) {\n      super(buildWebClient(maxBuffSize, config, truststoreConfig), null, null);\n      setBasePath(config.getAddress());\n      setUsername(config.getUsername());\n      setPassword(config.getPassword());\n    }\n\n    public static WebClient buildWebClient(DataSize maxBuffSize,\n                                           ConnectCluster config,\n                                           ClustersProperties.TruststoreConfig truststoreConfig) {\n      return new WebClientConfigurator()\n          .configureSsl(\n              truststoreConfig,\n              new ClustersProperties.KeystoreConfig(\n                  config.getKeystoreLocation(),\n                  config.getKeystorePassword()\n              )\n          )\n          .configureBasicAuth(\n              config.getUsername(),\n              config.getPassword()\n          )\n          .configureBufferSize(maxBuffSize)\n          .build();\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ClustersProperties.java",
    "content": "package com.provectus.kafka.ui.config;\n\nimport com.provectus.kafka.ui.model.MetricsConfig;\nimport jakarta.annotation.PostConstruct;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport javax.annotation.Nullable;\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\nimport lombok.ToString;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.util.StringUtils;\n\n@Configuration\n@ConfigurationProperties(\"kafka\")\n@Data\npublic class ClustersProperties {\n\n  List<Cluster> clusters = new ArrayList<>();\n\n  String internalTopicPrefix;\n\n  Integer adminClientTimeout;\n\n  PollingProperties polling = new PollingProperties();\n\n  @Data\n  public static class Cluster {\n    String name;\n    String bootstrapServers;\n    String schemaRegistry;\n    SchemaRegistryAuth schemaRegistryAuth;\n    KeystoreConfig schemaRegistrySsl;\n    String ksqldbServer;\n    KsqldbServerAuth ksqldbServerAuth;\n    KeystoreConfig ksqldbServerSsl;\n    List<ConnectCluster> kafkaConnect;\n    MetricsConfigData metrics;\n    Map<String, Object> properties;\n    boolean readOnly = false;\n    List<SerdeConfig> serde;\n    String defaultKeySerde;\n    String defaultValueSerde;\n    List<Masking> masking;\n    Long pollingThrottleRate;\n    TruststoreConfig ssl;\n    AuditProperties audit;\n  }\n\n  @Data\n  public static class PollingProperties {\n    Integer pollTimeoutMs;\n    Integer maxPageSize;\n    Integer defaultPageSize;\n  }\n\n  @Data\n  @ToString(exclude = \"password\")\n  public static class MetricsConfigData {\n    String type;\n    Integer port;\n    Boolean ssl;\n    String username;\n    String password;\n    String keystoreLocation;\n    String keystorePassword;\n  }\n\n  @Data\n  @NoArgsConstructor\n  @AllArgsConstructor\n  @Builder(toBuilder = true)\n  @ToString(exclude = {\"password\", \"keystorePassword\"})\n  public static class ConnectCluster {\n    String name;\n    String address;\n    String username;\n    String password;\n    String keystoreLocation;\n    String keystorePassword;\n  }\n\n  @Data\n  @ToString(exclude = {\"password\"})\n  public static class SchemaRegistryAuth {\n    String username;\n    String password;\n  }\n\n  @Data\n  @ToString(exclude = {\"truststorePassword\"})\n  public static class TruststoreConfig {\n    String truststoreLocation;\n    String truststorePassword;\n  }\n\n  @Data\n  public static class SerdeConfig {\n    String name;\n    String className;\n    String filePath;\n    Map<String, Object> properties;\n    String topicKeysPattern;\n    String topicValuesPattern;\n  }\n\n  @Data\n  @ToString(exclude = \"password\")\n  public static class KsqldbServerAuth {\n    String username;\n    String password;\n  }\n\n  @Data\n  @NoArgsConstructor\n  @AllArgsConstructor\n  @ToString(exclude = {\"keystorePassword\"})\n  public static class KeystoreConfig {\n    String keystoreLocation;\n    String keystorePassword;\n  }\n\n  @Data\n  public static class Masking {\n    Type type;\n    List<String> fields;\n    String fieldsNamePattern;\n    List<String> maskingCharsReplacement; //used when type=MASK\n    String replacement; //used when type=REPLACE\n    String topicKeysPattern;\n    String topicValuesPattern;\n\n    public enum Type {\n      REMOVE, MASK, REPLACE\n    }\n  }\n\n  @Data\n  @NoArgsConstructor\n  @AllArgsConstructor\n  public static class AuditProperties {\n    String topic;\n    Integer auditTopicsPartitions;\n    Boolean topicAuditEnabled;\n    Boolean consoleAuditEnabled;\n    LogLevel level;\n    Map<String, String> auditTopicProperties;\n\n    public enum LogLevel {\n      ALL,\n      ALTER_ONLY //default\n    }\n  }\n\n  @PostConstruct\n  public void validateAndSetDefaults() {\n    if (clusters != null) {\n      validateClusterNames();\n      flattenClusterProperties();\n      setMetricsDefaults();\n    }\n  }\n\n  private void setMetricsDefaults() {\n    for (Cluster cluster : clusters) {\n      if (cluster.getMetrics() != null && !StringUtils.hasText(cluster.getMetrics().getType())) {\n        cluster.getMetrics().setType(MetricsConfig.JMX_METRICS_TYPE);\n      }\n    }\n  }\n\n  private void flattenClusterProperties() {\n    for (Cluster cluster : clusters) {\n      cluster.setProperties(flattenClusterProperties(null, cluster.getProperties()));\n    }\n  }\n\n  private Map<String, Object> flattenClusterProperties(@Nullable String prefix,\n                                                       @Nullable Map<String, Object> propertiesMap) {\n    Map<String, Object> flattened = new HashMap<>();\n    if (propertiesMap != null) {\n      propertiesMap.forEach((k, v) -> {\n        String key = prefix == null ? k : prefix + \".\" + k;\n        if (v instanceof Map<?, ?>) {\n          flattened.putAll(flattenClusterProperties(key, (Map<String, Object>) v));\n        } else {\n          flattened.put(key, v);\n        }\n      });\n    }\n    return flattened;\n  }\n\n  private void validateClusterNames() {\n    // if only one cluster provided it is ok not to set name\n    if (clusters.size() == 1 && !StringUtils.hasText(clusters.get(0).getName())) {\n      clusters.get(0).setName(\"Default\");\n      return;\n    }\n\n    Set<String> clusterNames = new HashSet<>();\n    for (Cluster clusterProperties : clusters) {\n      if (!StringUtils.hasText(clusterProperties.getName())) {\n        throw new IllegalStateException(\n            \"Application config isn't valid. \"\n                + \"Cluster names should be provided in case of multiple clusters present\");\n      }\n      if (!clusterNames.add(clusterProperties.getName())) {\n        throw new IllegalStateException(\n            \"Application config isn't valid. Two clusters can't have the same name\");\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/Config.java",
    "content": "package com.provectus.kafka.ui.config;\n\nimport java.util.Collections;\nimport java.util.Map;\nimport lombok.AllArgsConstructor;\nimport org.openapitools.jackson.nullable.JsonNullableModule;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.boot.autoconfigure.web.ServerProperties;\nimport org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.server.reactive.ContextPathCompositeHandler;\nimport org.springframework.http.server.reactive.HttpHandler;\nimport org.springframework.jmx.export.MBeanExporter;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.server.adapter.WebHttpHandlerBuilder;\n\n@Configuration\n@AllArgsConstructor\npublic class Config {\n\n  private final ApplicationContext applicationContext;\n\n  private final ServerProperties serverProperties;\n\n  @Bean\n  public HttpHandler httpHandler(ObjectProvider<WebFluxProperties> propsProvider) {\n\n    final String basePath = serverProperties.getServlet().getContextPath();\n\n    HttpHandler httpHandler = WebHttpHandlerBuilder\n        .applicationContext(this.applicationContext).build();\n\n    if (StringUtils.hasText(basePath)) {\n      Map<String, HttpHandler> handlersMap =\n          Collections.singletonMap(basePath, httpHandler);\n      return new ContextPathCompositeHandler(handlersMap);\n    }\n    return httpHandler;\n  }\n\n  @Bean\n  public MBeanExporter exporter() {\n    final var exporter = new MBeanExporter();\n    exporter.setAutodetect(true);\n    exporter.setExcludedBeans(\"pool\");\n    return exporter;\n  }\n\n  @Bean\n  // will be used by webflux json mapping\n  public JsonNullableModule jsonNullableModule() {\n    return new JsonNullableModule();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CorsGlobalConfiguration.java",
    "content": "package com.provectus.kafka.ui.config;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.server.reactive.ServerHttpRequest;\nimport org.springframework.http.server.reactive.ServerHttpResponse;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilter;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\n\n@Configuration\npublic class CorsGlobalConfiguration {\n\n  @Bean\n  public WebFilter corsFilter() {\n    return (final ServerWebExchange ctx, final WebFilterChain chain) -> {\n      final ServerHttpRequest request = ctx.getRequest();\n\n      final ServerHttpResponse response = ctx.getResponse();\n      final HttpHeaders headers = response.getHeaders();\n      headers.add(\"Access-Control-Allow-Origin\", \"*\");\n      headers.add(\"Access-Control-Allow-Methods\", \"GET, PUT, POST, DELETE, OPTIONS\");\n      headers.add(\"Access-Control-Max-Age\", \"3600\");\n      headers.add(\"Access-Control-Allow-Headers\", \"Content-Type\");\n\n      if (request.getMethod() == HttpMethod.OPTIONS) {\n        response.setStatusCode(HttpStatus.OK);\n        return Mono.empty();\n      }\n\n      return chain.filter(ctx);\n    };\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CustomWebFilter.java",
    "content": "package com.provectus.kafka.ui.config;\n\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilter;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\n\n@Component\npublic class CustomWebFilter implements WebFilter {\n\n  @Override\n  public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {\n\n    final String basePath = exchange.getRequest().getPath().contextPath().value();\n\n    final String path = exchange.getRequest().getPath().pathWithinApplication().value();\n\n    if (path.startsWith(\"/ui\") || path.equals(\"\") || path.equals(\"/\")) {\n      return chain.filter(\n          exchange.mutate().request(\n              exchange.getRequest().mutate()\n                  .path(basePath + \"/index.html\")\n                  .contextPath(basePath)\n                  .build()\n          ).build()\n      );\n    }\n\n    return chain.filter(exchange);\n  }\n}"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ReadOnlyModeFilter.java",
    "content": "package com.provectus.kafka.ui.config;\n\nimport com.provectus.kafka.ui.exception.ClusterNotFoundException;\nimport com.provectus.kafka.ui.exception.ReadOnlyModeException;\nimport com.provectus.kafka.ui.service.ClustersStorage;\nimport java.net.URLDecoder;\nimport java.nio.charset.StandardCharsets;\nimport java.util.regex.Pattern;\nimport lombok.RequiredArgsConstructor;\nimport org.jetbrains.annotations.NotNull;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilter;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\n\n@Order\n@Component\n@RequiredArgsConstructor\npublic class ReadOnlyModeFilter implements WebFilter {\n  private static final Pattern CLUSTER_NAME_REGEX =\n      Pattern.compile(\"/api/clusters/(?<clusterName>[^/]++)\");\n\n  private final ClustersStorage clustersStorage;\n\n  @NotNull\n  @Override\n  public Mono<Void> filter(ServerWebExchange exchange, @NotNull WebFilterChain chain) {\n    var isSafeMethod = exchange.getRequest().getMethod() == HttpMethod.GET;\n    if (isSafeMethod) {\n      return chain.filter(exchange);\n    }\n\n    var path = exchange.getRequest().getPath().pathWithinApplication().value();\n    var decodedPath = URLDecoder.decode(path, StandardCharsets.UTF_8);\n    var matcher = CLUSTER_NAME_REGEX.matcher(decodedPath);\n    if (!matcher.find()) {\n      return chain.filter(exchange);\n    }\n    var clusterName = matcher.group(\"clusterName\");\n    var kafkaCluster = clustersStorage.getClusterByName(clusterName)\n        .orElseThrow(\n            () -> new ClusterNotFoundException(\n                String.format(\"No cluster for name '%s'\", clusterName)));\n\n    if (!kafkaCluster.isReadOnly()) {\n      return chain.filter(exchange);\n    }\n\n    return Mono.error(ReadOnlyModeException::new);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/WebclientProperties.java",
    "content": "package com.provectus.kafka.ui.config;\n\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport javax.annotation.PostConstruct;\nimport lombok.Data;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.util.unit.DataSize;\n\n@Configuration\n@ConfigurationProperties(\"webclient\")\n@Data\npublic class WebclientProperties {\n\n  String maxInMemoryBufferSize;\n\n  @PostConstruct\n  public void validate() {\n    validateAndSetDefaultBufferSize();\n  }\n\n  private void validateAndSetDefaultBufferSize() {\n    if (maxInMemoryBufferSize != null) {\n      try {\n        DataSize.parse(maxInMemoryBufferSize);\n      } catch (Exception e) {\n        throw new ValidationException(\"Invalid format for webclient.maxInMemoryBufferSize\");\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/AbstractAuthSecurityConfig.java",
    "content": "package com.provectus.kafka.ui.config.auth;\n\nabstract class AbstractAuthSecurityConfig {\n\n  protected AbstractAuthSecurityConfig() {\n\n  }\n\n  protected static final String[] AUTH_WHITELIST = {\n      \"/css/**\",\n      \"/js/**\",\n      \"/media/**\",\n      \"/resources/**\",\n      \"/actuator/health/**\",\n      \"/actuator/info\",\n      \"/actuator/prometheus\",\n      \"/auth\",\n      \"/login\",\n      \"/logout\",\n      \"/oauth2/**\",\n      \"/static/**\"\n  };\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/AuthenticatedUser.java",
    "content": "package com.provectus.kafka.ui.config.auth;\n\nimport java.util.Collection;\n\npublic record AuthenticatedUser(String principal, Collection<String> groups) {\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/BasicAuthSecurityConfig.java",
    "content": "package com.provectus.kafka.ui.config.auth;\n\nimport com.provectus.kafka.ui.util.EmptyRedirectStrategy;\nimport java.net.URI;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;\nimport org.springframework.security.config.web.server.ServerHttpSecurity;\nimport org.springframework.security.web.server.SecurityWebFilterChain;\nimport org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;\nimport org.springframework.security.web.server.authentication.logout.RedirectServerLogoutSuccessHandler;\nimport org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;\n\n@Configuration\n@EnableWebFluxSecurity\n@ConditionalOnProperty(value = \"auth.type\", havingValue = \"LOGIN_FORM\")\n@Slf4j\npublic class BasicAuthSecurityConfig extends AbstractAuthSecurityConfig {\n\n  public static final String LOGIN_URL = \"/auth\";\n  public static final String LOGOUT_URL = \"/auth?logout\";\n\n  @Bean\n  public SecurityWebFilterChain configure(ServerHttpSecurity http) {\n    log.info(\"Configuring LOGIN_FORM authentication.\");\n\n    final var authHandler = new RedirectServerAuthenticationSuccessHandler();\n    authHandler.setRedirectStrategy(new EmptyRedirectStrategy());\n\n    final var logoutSuccessHandler = new RedirectServerLogoutSuccessHandler();\n    logoutSuccessHandler.setLogoutSuccessUrl(URI.create(LOGOUT_URL));\n\n\n    return http.authorizeExchange(spec -> spec\n            .pathMatchers(AUTH_WHITELIST)\n            .permitAll()\n            .anyExchange()\n            .authenticated()\n        )\n        .formLogin(spec -> spec.loginPage(LOGIN_URL).authenticationSuccessHandler(authHandler))\n        .logout(spec -> spec\n            .logoutSuccessHandler(logoutSuccessHandler)\n            .requiresLogout(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, \"/logout\")))\n        .csrf(ServerHttpSecurity.CsrfSpec::disable)\n        .build();\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/DisabledAuthSecurityConfig.java",
    "content": "package com.provectus.kafka.ui.config.auth;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.env.Environment;\nimport org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;\nimport org.springframework.security.config.web.server.ServerHttpSecurity;\nimport org.springframework.security.web.server.SecurityWebFilterChain;\n\n@Configuration\n@EnableWebFluxSecurity\n@ConditionalOnProperty(value = \"auth.type\", havingValue = \"DISABLED\")\n@Slf4j\npublic class DisabledAuthSecurityConfig extends AbstractAuthSecurityConfig {\n\n  @Bean\n  public SecurityWebFilterChain configure(ServerHttpSecurity http, Environment env, ApplicationContext context) {\n    if (env.getProperty(\"auth.enabled\") != null) {\n      log.error(\"A deprecated property (auth.enabled) is present. \"\n          + \"Please replace it with 'auth.type' (possible values are: 'LOGIN_FORM', 'DISABLED', 'OAUTH2', 'LDAP') \"\n          + \"and restart the application.\");\n      SpringApplication.exit(context, () -> 1);\n      System.exit(1);\n    }\n    log.warn(\"Authentication is disabled. Access will be unrestricted.\");\n\n    return http.authorizeExchange(spec -> spec\n            .anyExchange()\n            .permitAll()\n        )\n        .csrf(ServerHttpSecurity.CsrfSpec::disable)\n        .build();\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapProperties.java",
    "content": "package com.provectus.kafka.ui.config.auth;\n\nimport lombok.Data;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\n\n@ConfigurationProperties(\"spring.ldap\")\n@Data\npublic class LdapProperties {\n\n  private String urls;\n  private String base;\n  private String adminUser;\n  private String adminPassword;\n  private String userFilterSearchBase;\n  private String userFilterSearchFilter;\n  private String groupFilterSearchBase;\n  private String groupFilterSearchFilter;\n  private String groupRoleAttribute;\n\n  @Value(\"${oauth2.ldap.activeDirectory:false}\")\n  private boolean isActiveDirectory;\n  @Value(\"${oauth2.ldap.aсtiveDirectory.domain:@null}\")\n  private String activeDirectoryDomain;\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapSecurityConfig.java",
    "content": "package com.provectus.kafka.ui.config.auth;\n\nimport static com.provectus.kafka.ui.config.auth.AbstractAuthSecurityConfig.AUTH_WHITELIST;\n\nimport com.provectus.kafka.ui.service.rbac.AccessControlService;\nimport com.provectus.kafka.ui.service.rbac.extractor.RbacLdapAuthoritiesExtractor;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Optional;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.context.annotation.Import;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.ldap.core.DirContextOperations;\nimport org.springframework.ldap.core.support.BaseLdapPathContextSource;\nimport org.springframework.ldap.core.support.LdapContextSource;\nimport org.springframework.security.authentication.AuthenticationManager;\nimport org.springframework.security.authentication.ProviderManager;\nimport org.springframework.security.authentication.ReactiveAuthenticationManager;\nimport org.springframework.security.authentication.ReactiveAuthenticationManagerAdapter;\nimport org.springframework.security.config.Customizer;\nimport org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;\nimport org.springframework.security.config.web.server.ServerHttpSecurity;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.userdetails.UserDetails;\nimport org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider;\nimport org.springframework.security.ldap.authentication.BindAuthenticator;\nimport org.springframework.security.ldap.authentication.LdapAuthenticationProvider;\nimport org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;\nimport org.springframework.security.ldap.search.FilterBasedLdapUserSearch;\nimport org.springframework.security.ldap.search.LdapUserSearch;\nimport org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;\nimport org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;\nimport org.springframework.security.ldap.userdetails.LdapUserDetailsMapper;\nimport org.springframework.security.web.server.SecurityWebFilterChain;\n\n@Configuration\n@EnableWebFluxSecurity\n@ConditionalOnProperty(value = \"auth.type\", havingValue = \"LDAP\")\n@Import(LdapAutoConfiguration.class)\n@EnableConfigurationProperties(LdapProperties.class)\n@RequiredArgsConstructor\n@Slf4j\npublic class LdapSecurityConfig {\n\n  private final LdapProperties props;\n\n  @Bean\n  public ReactiveAuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource,\n                                                             LdapAuthoritiesPopulator authoritiesExtractor,\n                                                             AccessControlService acs) {\n    var rbacEnabled = acs.isRbacEnabled();\n    BindAuthenticator ba = new BindAuthenticator(contextSource);\n    if (props.getBase() != null) {\n      ba.setUserDnPatterns(new String[] {props.getBase()});\n    }\n    if (props.getUserFilterSearchFilter() != null) {\n      LdapUserSearch userSearch =\n          new FilterBasedLdapUserSearch(props.getUserFilterSearchBase(), props.getUserFilterSearchFilter(),\n              contextSource);\n      ba.setUserSearch(userSearch);\n    }\n\n    AbstractLdapAuthenticationProvider authenticationProvider;\n    if (!props.isActiveDirectory()) {\n      authenticationProvider = rbacEnabled\n          ? new LdapAuthenticationProvider(ba, authoritiesExtractor)\n          : new LdapAuthenticationProvider(ba);\n    } else {\n      authenticationProvider = new ActiveDirectoryLdapAuthenticationProvider(props.getActiveDirectoryDomain(),\n          props.getUrls()); // TODO Issue #3741\n      authenticationProvider.setUseAuthenticationRequestCredentials(true);\n    }\n\n    if (rbacEnabled) {\n      authenticationProvider.setUserDetailsContextMapper(new UserDetailsMapper());\n    }\n\n    AuthenticationManager am = new ProviderManager(List.of(authenticationProvider));\n\n    return new ReactiveAuthenticationManagerAdapter(am);\n  }\n\n  @Bean\n  @Primary\n  public BaseLdapPathContextSource contextSource() {\n    LdapContextSource ctx = new LdapContextSource();\n    ctx.setUrl(props.getUrls());\n    ctx.setUserDn(props.getAdminUser());\n    ctx.setPassword(props.getAdminPassword());\n    ctx.afterPropertiesSet();\n    return ctx;\n  }\n\n  @Bean\n  @Primary\n  public DefaultLdapAuthoritiesPopulator ldapAuthoritiesExtractor(ApplicationContext context,\n                                                                  BaseLdapPathContextSource contextSource,\n                                                                  AccessControlService acs) {\n    var rbacEnabled = acs != null && acs.isRbacEnabled();\n\n    DefaultLdapAuthoritiesPopulator extractor;\n\n    if (rbacEnabled) {\n      extractor = new RbacLdapAuthoritiesExtractor(context, contextSource, props.getGroupFilterSearchBase());\n    } else {\n      extractor = new DefaultLdapAuthoritiesPopulator(contextSource, props.getGroupFilterSearchBase());\n    }\n\n    Optional.ofNullable(props.getGroupFilterSearchFilter()).ifPresent(extractor::setGroupSearchFilter);\n    extractor.setRolePrefix(\"\");\n    extractor.setConvertToUpperCase(false);\n    extractor.setSearchSubtree(true);\n    return extractor;\n  }\n\n  @Bean\n  public SecurityWebFilterChain configureLdap(ServerHttpSecurity http) {\n    log.info(\"Configuring LDAP authentication.\");\n    if (props.isActiveDirectory()) {\n      log.info(\"Active Directory support for LDAP has been enabled.\");\n    }\n\n    return http.authorizeExchange(spec -> spec\n            .pathMatchers(AUTH_WHITELIST)\n            .permitAll()\n            .anyExchange()\n            .authenticated()\n        )\n        .formLogin(Customizer.withDefaults())\n        .logout(Customizer.withDefaults())\n        .csrf(ServerHttpSecurity.CsrfSpec::disable)\n        .build();\n  }\n\n  private static class UserDetailsMapper extends LdapUserDetailsMapper {\n    @Override\n    public UserDetails mapUserFromContext(DirContextOperations ctx, String username,\n                                          Collection<? extends GrantedAuthority> authorities) {\n      UserDetails userDetails = super.mapUserFromContext(ctx, username, authorities);\n      return new RbacLdapUser(userDetails);\n    }\n  }\n\n}\n\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthProperties.java",
    "content": "package com.provectus.kafka.ui.config.auth;\n\nimport jakarta.annotation.PostConstruct;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\nimport lombok.Data;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.util.Assert;\n\n@ConfigurationProperties(\"auth.oauth2\")\n@Data\npublic class OAuthProperties {\n  private Map<String, OAuth2Provider> client = new HashMap<>();\n\n  @PostConstruct\n  public void init() {\n    getClient().values().forEach((provider) -> {\n      if (provider.getCustomParams() == null) {\n        provider.setCustomParams(Collections.emptyMap());\n      }\n      if (provider.getScope() == null) {\n        provider.setScope(Collections.emptySet());\n      }\n    });\n\n    getClient().values().forEach(this::validateProvider);\n  }\n\n  private void validateProvider(final OAuth2Provider provider) {\n    Assert.hasText(provider.getClientId(), \"Client id must not be empty.\");\n    Assert.hasText(provider.getProvider(), \"Provider name must not be empty\");\n  }\n\n  @Data\n  public static class OAuth2Provider {\n    private String provider;\n    private String clientId;\n    private String clientSecret;\n    private String clientName;\n    private String redirectUri;\n    private String authorizationGrantType;\n    private Set<String> scope;\n    private String issuerUri;\n    private String authorizationUri;\n    private String tokenUri;\n    private String userInfoUri;\n    private String jwkSetUri;\n    private String userNameAttribute;\n    private Map<String, String> customParams;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthPropertiesConverter.java",
    "content": "package com.provectus.kafka.ui.config.auth;\n\nimport static com.provectus.kafka.ui.config.auth.OAuthProperties.OAuth2Provider;\nimport static org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Provider;\nimport static org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Registration;\n\nimport java.util.Optional;\nimport java.util.Set;\nimport lombok.AccessLevel;\nimport lombok.NoArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;\nimport org.springframework.security.config.oauth2.client.CommonOAuth2Provider;\n\n@NoArgsConstructor(access = AccessLevel.PRIVATE)\npublic final class OAuthPropertiesConverter {\n\n  private static final String TYPE = \"type\";\n  private static final String GOOGLE = \"google\";\n  public static final String DUMMY = \"dummy\";\n\n  public static OAuth2ClientProperties convertProperties(final OAuthProperties properties) {\n    final var result = new OAuth2ClientProperties();\n    properties.getClient().forEach((key, provider) -> {\n      var registration = new Registration();\n      registration.setClientId(provider.getClientId());\n      registration.setClientSecret(provider.getClientSecret());\n      registration.setClientName(provider.getClientName());\n      registration.setScope(Optional.ofNullable(provider.getScope()).orElse(Set.of()));\n      registration.setRedirectUri(provider.getRedirectUri());\n      registration.setAuthorizationGrantType(provider.getAuthorizationGrantType());\n\n      result.getRegistration().put(key, registration);\n\n      var clientProvider = new Provider();\n      applyCustomTransformations(provider);\n\n      clientProvider.setAuthorizationUri(provider.getAuthorizationUri());\n      clientProvider.setIssuerUri(provider.getIssuerUri());\n      clientProvider.setJwkSetUri(provider.getJwkSetUri());\n      clientProvider.setTokenUri(provider.getTokenUri());\n      clientProvider.setUserInfoUri(provider.getUserInfoUri());\n      clientProvider.setUserNameAttribute(provider.getUserNameAttribute());\n\n      result.getProvider().put(key, clientProvider);\n    });\n    return result;\n  }\n\n  private static void applyCustomTransformations(OAuth2Provider provider) {\n    applyGoogleTransformations(provider);\n  }\n\n  private static void applyGoogleTransformations(OAuth2Provider provider) {\n    if (!isGoogle(provider)) {\n      return;\n    }\n\n    String allowedDomain = provider.getCustomParams().get(\"allowedDomain\");\n    if (StringUtils.isEmpty(allowedDomain)) {\n      return;\n    }\n\n    String authorizationUri = CommonOAuth2Provider.GOOGLE\n        .getBuilder(DUMMY)\n        .clientId(DUMMY)\n        .build()\n        .getProviderDetails()\n        .getAuthorizationUri();\n\n    final String newUri = authorizationUri + \"?hd=\" + allowedDomain;\n    provider.setAuthorizationUri(newUri);\n  }\n\n  private static boolean isGoogle(OAuth2Provider provider) {\n    return GOOGLE.equalsIgnoreCase(provider.getCustomParams().get(TYPE));\n  }\n}\n\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthSecurityConfig.java",
    "content": "package com.provectus.kafka.ui.config.auth;\n\nimport com.provectus.kafka.ui.config.auth.logout.OAuthLogoutSuccessHandler;\nimport com.provectus.kafka.ui.service.rbac.AccessControlService;\nimport com.provectus.kafka.ui.service.rbac.extractor.ProviderAuthorityExtractor;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.log4j.Log4j2;\nimport org.jetbrains.annotations.Nullable;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;\nimport org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesMapper;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.security.config.Customizer;\nimport org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;\nimport org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;\nimport org.springframework.security.config.web.server.ServerHttpSecurity;\nimport org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService;\nimport org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;\nimport org.springframework.security.oauth2.client.oidc.web.server.logout.OidcClientInitiatedServerLogoutSuccessHandler;\nimport org.springframework.security.oauth2.client.registration.ClientRegistration;\nimport org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;\nimport org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;\nimport org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService;\nimport org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;\nimport org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;\nimport org.springframework.security.oauth2.core.oidc.user.OidcUser;\nimport org.springframework.security.oauth2.core.user.OAuth2User;\nimport org.springframework.security.web.server.SecurityWebFilterChain;\nimport org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;\nimport reactor.core.publisher.Mono;\n\n@Configuration\n@ConditionalOnProperty(value = \"auth.type\", havingValue = \"OAUTH2\")\n@EnableConfigurationProperties(OAuthProperties.class)\n@EnableWebFluxSecurity\n@EnableReactiveMethodSecurity\n@RequiredArgsConstructor\n@Log4j2\npublic class OAuthSecurityConfig extends AbstractAuthSecurityConfig {\n\n  private final OAuthProperties properties;\n\n  @Bean\n  public SecurityWebFilterChain configure(ServerHttpSecurity http, OAuthLogoutSuccessHandler logoutHandler) {\n    log.info(\"Configuring OAUTH2 authentication.\");\n\n    return http.authorizeExchange(spec -> spec\n            .pathMatchers(AUTH_WHITELIST)\n            .permitAll()\n            .anyExchange()\n            .authenticated()\n        )\n        .oauth2Login(Customizer.withDefaults())\n        .logout(spec -> spec.logoutSuccessHandler(logoutHandler))\n        .csrf(ServerHttpSecurity.CsrfSpec::disable)\n        .build();\n  }\n\n  @Bean\n  public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> customOidcUserService(AccessControlService acs) {\n    final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService();\n    return request -> delegate.loadUser(request)\n        .flatMap(user -> {\n          var provider = getProviderByProviderId(request.getClientRegistration().getRegistrationId());\n          final var extractor = getExtractor(provider, acs);\n          if (extractor == null) {\n            return Mono.just(user);\n          }\n\n          return extractor.extract(acs, user, Map.of(\"request\", request, \"provider\", provider))\n              .map(groups -> new RbacOidcUser(user, groups));\n        });\n  }\n\n  @Bean\n  public ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> customOauth2UserService(AccessControlService acs) {\n    final DefaultReactiveOAuth2UserService delegate = new DefaultReactiveOAuth2UserService();\n    return request -> delegate.loadUser(request)\n        .flatMap(user -> {\n          var provider = getProviderByProviderId(request.getClientRegistration().getRegistrationId());\n          final var extractor = getExtractor(provider, acs);\n          if (extractor == null) {\n            return Mono.just(user);\n          }\n\n          return extractor.extract(acs, user, Map.of(\"request\", request, \"provider\", provider))\n              .map(groups -> new RbacOAuth2User(user, groups));\n        });\n  }\n\n  @Bean\n  public InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() {\n    final OAuth2ClientProperties props = OAuthPropertiesConverter.convertProperties(properties);\n    final List<ClientRegistration> registrations =\n        new ArrayList<>(new OAuth2ClientPropertiesMapper(props).asClientRegistrations().values());\n    if (registrations.isEmpty()) {\n      throw new IllegalArgumentException(\"OAuth2 authentication is enabled but no providers specified.\");\n    }\n    return new InMemoryReactiveClientRegistrationRepository(registrations);\n  }\n\n  @Bean\n  public ServerLogoutSuccessHandler defaultOidcLogoutHandler(final ReactiveClientRegistrationRepository repository) {\n    return new OidcClientInitiatedServerLogoutSuccessHandler(repository);\n  }\n\n  @Nullable\n  private ProviderAuthorityExtractor getExtractor(final OAuthProperties.OAuth2Provider provider,\n                                                  AccessControlService acs) {\n    Optional<ProviderAuthorityExtractor> extractor = acs.getOauthExtractors()\n        .stream()\n        .filter(e -> e.isApplicable(provider.getProvider(), provider.getCustomParams()))\n        .findFirst();\n\n    return extractor.orElse(null);\n  }\n\n  private OAuthProperties.OAuth2Provider getProviderByProviderId(final String providerId) {\n    return properties.getClient().get(providerId);\n  }\n\n}\n\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacLdapUser.java",
    "content": "package com.provectus.kafka.ui.config.auth;\n\nimport java.util.Collection;\nimport java.util.stream.Collectors;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.userdetails.UserDetails;\n\npublic class RbacLdapUser implements UserDetails, RbacUser {\n\n  private final UserDetails userDetails;\n\n  public RbacLdapUser(UserDetails userDetails) {\n    this.userDetails = userDetails;\n  }\n\n  @Override\n  public String name() {\n    return userDetails.getUsername();\n  }\n\n  @Override\n  public Collection<String> groups() {\n    return userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());\n  }\n\n  @Override\n  public Collection<? extends GrantedAuthority> getAuthorities() {\n    return userDetails.getAuthorities();\n  }\n\n  @Override\n  public String getPassword() {\n    return userDetails.getPassword();\n  }\n\n  @Override\n  public String getUsername() {\n    return userDetails.getUsername();\n  }\n\n  @Override\n  public boolean isAccountNonExpired() {\n    return userDetails.isAccountNonExpired();\n  }\n\n  @Override\n  public boolean isAccountNonLocked() {\n    return userDetails.isAccountNonLocked();\n  }\n\n  @Override\n  public boolean isCredentialsNonExpired() {\n    return userDetails.isCredentialsNonExpired();\n  }\n\n  @Override\n  public boolean isEnabled() {\n    return userDetails.isEnabled();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacOAuth2User.java",
    "content": "package com.provectus.kafka.ui.config.auth;\n\nimport java.util.Collection;\nimport java.util.Map;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.oauth2.core.user.OAuth2User;\n\npublic record RbacOAuth2User(OAuth2User user, Collection<String> groups) implements RbacUser, OAuth2User {\n\n  @Override\n  public Map<String, Object> getAttributes() {\n    return user.getAttributes();\n  }\n\n  @Override\n  public Collection<? extends GrantedAuthority> getAuthorities() {\n    return user.getAuthorities();\n  }\n\n  @Override\n  public String getName() {\n    return user.getName();\n  }\n\n  @Override\n  public String name() {\n    return user.getName();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacOidcUser.java",
    "content": "package com.provectus.kafka.ui.config.auth;\n\nimport java.util.Collection;\nimport java.util.Map;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.oauth2.core.oidc.OidcIdToken;\nimport org.springframework.security.oauth2.core.oidc.OidcUserInfo;\nimport org.springframework.security.oauth2.core.oidc.user.OidcUser;\n\npublic record RbacOidcUser(OidcUser user, Collection<String> groups) implements RbacUser, OidcUser {\n\n  @Override\n  public Map<String, Object> getClaims() {\n    return user.getClaims();\n  }\n\n  @Override\n  public OidcUserInfo getUserInfo() {\n    return user.getUserInfo();\n  }\n\n  @Override\n  public OidcIdToken getIdToken() {\n    return user.getIdToken();\n  }\n\n  @Override\n  public Map<String, Object> getAttributes() {\n    return user.getAttributes();\n  }\n\n  @Override\n  public Collection<? extends GrantedAuthority> getAuthorities() {\n    return user.getAuthorities();\n  }\n\n  @Override\n  public String getName() {\n    return user.getName();\n  }\n\n  @Override\n  public String name() {\n    return user.getName();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacUser.java",
    "content": "package com.provectus.kafka.ui.config.auth;\n\nimport java.util.Collection;\n\npublic interface RbacUser {\n  String name();\n\n  Collection<String> groups();\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RoleBasedAccessControlProperties.java",
    "content": "package com.provectus.kafka.ui.config.auth;\n\nimport com.provectus.kafka.ui.model.rbac.Role;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.annotation.PostConstruct;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\n\n@ConfigurationProperties(\"rbac\")\npublic class RoleBasedAccessControlProperties {\n\n  private final List<Role> roles = new ArrayList<>();\n\n  @PostConstruct\n  public void init() {\n    roles.forEach(Role::validate);\n  }\n\n  public List<Role> getRoles() {\n    return roles;\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/condition/ActiveDirectoryCondition.java",
    "content": "package com.provectus.kafka.ui.config.auth.condition;\n\nimport org.springframework.boot.autoconfigure.condition.AllNestedConditions;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\n\npublic class ActiveDirectoryCondition extends AllNestedConditions {\n\n  public ActiveDirectoryCondition() {\n    super(ConfigurationPhase.PARSE_CONFIGURATION);\n  }\n\n  @ConditionalOnProperty(value = \"auth.type\", havingValue = \"LDAP\")\n  public static class OnAuthType {\n\n  }\n\n  @ConditionalOnProperty(value = \"${oauth2.ldap.activeDirectory}:false\", havingValue = \"true\", matchIfMissing = false)\n  public static class OnActiveDirectory {\n\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/condition/CognitoCondition.java",
    "content": "package com.provectus.kafka.ui.config.auth.condition;\n\nimport com.provectus.kafka.ui.service.rbac.AbstractProviderCondition;\nimport org.jetbrains.annotations.NotNull;\nimport org.springframework.context.annotation.Condition;\nimport org.springframework.context.annotation.ConditionContext;\nimport org.springframework.core.type.AnnotatedTypeMetadata;\n\npublic class CognitoCondition extends AbstractProviderCondition implements Condition {\n  @Override\n  public boolean matches(final ConditionContext context, final @NotNull AnnotatedTypeMetadata metadata) {\n    return getRegisteredProvidersTypes(context.getEnvironment()).stream().anyMatch(a -> a.equalsIgnoreCase(\"cognito\"));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/CognitoLogoutSuccessHandler.java",
    "content": "package com.provectus.kafka.ui.config.auth.logout;\n\nimport com.provectus.kafka.ui.config.auth.OAuthProperties;\nimport com.provectus.kafka.ui.config.auth.condition.CognitoCondition;\nimport com.provectus.kafka.ui.model.rbac.provider.Provider;\nimport java.net.URI;\nimport java.nio.charset.StandardCharsets;\nimport org.springframework.context.annotation.Conditional;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.server.reactive.ServerHttpResponse;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.web.server.WebFilterExchange;\nimport org.springframework.security.web.util.UrlUtils;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.web.server.WebSession;\nimport org.springframework.web.util.UriComponents;\nimport org.springframework.web.util.UriComponentsBuilder;\nimport reactor.core.publisher.Mono;\n\n@Component\n@Conditional(CognitoCondition.class)\npublic class CognitoLogoutSuccessHandler implements LogoutSuccessHandler {\n\n  @Override\n  public boolean isApplicable(String provider) {\n    return Provider.Name.COGNITO.equalsIgnoreCase(provider);\n  }\n\n  @Override\n  public Mono<Void> handle(WebFilterExchange exchange, Authentication authentication,\n                           OAuthProperties.OAuth2Provider provider) {\n    final ServerHttpResponse response = exchange.getExchange().getResponse();\n    response.setStatusCode(HttpStatus.FOUND);\n\n    final var requestUri = exchange.getExchange().getRequest().getURI();\n\n    final var fullUrl = UrlUtils.buildFullRequestUrl(requestUri.getScheme(),\n        requestUri.getHost(), requestUri.getPort(),\n        requestUri.getPath(), requestUri.getQuery());\n\n    final UriComponents baseUrl = UriComponentsBuilder\n        .fromHttpUrl(fullUrl)\n        .replacePath(\"/\")\n        .replaceQuery(null)\n        .fragment(null)\n        .build();\n\n    Assert.isTrue(provider.getCustomParams().containsKey(\"logoutUrl\"),\n        \"Custom params should contain 'logoutUrl'\");\n    final var uri = UriComponentsBuilder\n        .fromUri(URI.create(provider.getCustomParams().get(\"logoutUrl\")))\n        .queryParam(\"client_id\", provider.getClientId())\n        .queryParam(\"logout_uri\", baseUrl)\n        .encode(StandardCharsets.UTF_8)\n        .build()\n        .toUri();\n\n    response.getHeaders().setLocation(uri);\n    return exchange.getExchange().getSession().flatMap(WebSession::invalidate);\n  }\n\n}\n\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/LogoutSuccessHandler.java",
    "content": "package com.provectus.kafka.ui.config.auth.logout;\n\nimport com.provectus.kafka.ui.config.auth.OAuthProperties;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.web.server.WebFilterExchange;\nimport reactor.core.publisher.Mono;\n\npublic interface LogoutSuccessHandler {\n\n  boolean isApplicable(final String provider);\n\n  Mono<Void> handle(final WebFilterExchange exchange,\n                    final Authentication authentication,\n                    final OAuthProperties.OAuth2Provider provider);\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/OAuthLogoutSuccessHandler.java",
    "content": "package com.provectus.kafka.ui.config.auth.logout;\n\nimport com.provectus.kafka.ui.config.auth.OAuthProperties;\nimport java.util.List;\nimport java.util.Optional;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;\nimport org.springframework.security.web.server.WebFilterExchange;\nimport org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\n\n@Component\n@ConditionalOnProperty(value = \"auth.type\", havingValue = \"OAUTH2\")\npublic class OAuthLogoutSuccessHandler implements ServerLogoutSuccessHandler {\n  private final OAuthProperties properties;\n  private final List<LogoutSuccessHandler> logoutSuccessHandlers;\n  private final ServerLogoutSuccessHandler defaultOidcLogoutHandler;\n\n  public OAuthLogoutSuccessHandler(final OAuthProperties properties,\n                                   final List<LogoutSuccessHandler> logoutSuccessHandlers,\n                                   final @Qualifier(\"defaultOidcLogoutHandler\") ServerLogoutSuccessHandler handler) {\n    this.properties = properties;\n    this.logoutSuccessHandlers = logoutSuccessHandlers;\n    this.defaultOidcLogoutHandler = handler;\n  }\n\n  @Override\n  public Mono<Void> onLogoutSuccess(final WebFilterExchange exchange,\n                                    final Authentication authentication) {\n    final OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;\n    final String providerId = oauthToken.getAuthorizedClientRegistrationId();\n    final OAuthProperties.OAuth2Provider oAuth2Provider = properties.getClient().get(providerId);\n    return getLogoutHandler(oAuth2Provider.getProvider())\n        .map(handler -> handler.handle(exchange, authentication, oAuth2Provider))\n        .orElseGet(() -> defaultOidcLogoutHandler.onLogoutSuccess(exchange, authentication));\n  }\n\n  private Optional<LogoutSuccessHandler> getLogoutHandler(final String provider) {\n    return logoutSuccessHandlers.stream()\n        .filter(h -> h.isApplicable(provider))\n        .findFirst();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AbstractController.java",
    "content": "package com.provectus.kafka.ui.controller;\n\nimport com.provectus.kafka.ui.exception.ClusterNotFoundException;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.rbac.AccessContext;\nimport com.provectus.kafka.ui.service.ClustersStorage;\nimport com.provectus.kafka.ui.service.audit.AuditService;\nimport com.provectus.kafka.ui.service.rbac.AccessControlService;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport reactor.core.publisher.Mono;\nimport reactor.core.publisher.Signal;\n\npublic abstract class AbstractController {\n\n  protected ClustersStorage clustersStorage;\n  protected AccessControlService accessControlService;\n  protected AuditService auditService;\n\n  protected KafkaCluster getCluster(String name) {\n    return clustersStorage.getClusterByName(name)\n        .orElseThrow(() -> new ClusterNotFoundException(\n            String.format(\"Cluster with name '%s' not found\", name)));\n  }\n\n  protected Mono<Void> validateAccess(AccessContext context) {\n    return accessControlService.validateAccess(context);\n  }\n\n  protected void audit(AccessContext acxt, Signal<?> sig) {\n    auditService.audit(acxt, sig);\n  }\n\n  @Autowired\n  public void setClustersStorage(ClustersStorage clustersStorage) {\n    this.clustersStorage = clustersStorage;\n  }\n\n  @Autowired\n  public void setAccessControlService(AccessControlService accessControlService) {\n    this.accessControlService = accessControlService;\n  }\n\n  @Autowired\n  public void setAuditService(AuditService auditService) {\n    this.auditService = auditService;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AccessController.java",
    "content": "package com.provectus.kafka.ui.controller;\n\nimport com.provectus.kafka.ui.api.AuthorizationApi;\nimport com.provectus.kafka.ui.model.ActionDTO;\nimport com.provectus.kafka.ui.model.AuthenticationInfoDTO;\nimport com.provectus.kafka.ui.model.ResourceTypeDTO;\nimport com.provectus.kafka.ui.model.UserInfoDTO;\nimport com.provectus.kafka.ui.model.UserPermissionDTO;\nimport com.provectus.kafka.ui.model.rbac.Permission;\nimport com.provectus.kafka.ui.service.rbac.AccessControlService;\nimport java.security.Principal;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Objects;\nimport javax.annotation.Nullable;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\n@RestController\n@RequiredArgsConstructor\n@Slf4j\npublic class AccessController implements AuthorizationApi {\n\n  private final AccessControlService accessControlService;\n\n  public Mono<ResponseEntity<AuthenticationInfoDTO>> getUserAuthInfo(ServerWebExchange exchange) {\n    Mono<List<UserPermissionDTO>> permissions = accessControlService.getUser()\n        .map(user -> accessControlService.getRoles()\n            .stream()\n            .filter(role -> user.groups().contains(role.getName()))\n            .map(role -> mapPermissions(role.getPermissions(), role.getClusters()))\n            .flatMap(Collection::stream)\n            .toList()\n        )\n        .switchIfEmpty(Mono.just(Collections.emptyList()));\n\n    Mono<String> userName = ReactiveSecurityContextHolder.getContext()\n        .map(SecurityContext::getAuthentication)\n        .map(Principal::getName);\n\n    return userName\n        .zipWith(permissions)\n        .map(data -> {\n          var dto = new AuthenticationInfoDTO(accessControlService.isRbacEnabled());\n          dto.setUserInfo(new UserInfoDTO(data.getT1(), data.getT2()));\n          return dto;\n        })\n        .switchIfEmpty(Mono.just(new AuthenticationInfoDTO(accessControlService.isRbacEnabled())))\n        .map(ResponseEntity::ok);\n  }\n\n  private List<UserPermissionDTO> mapPermissions(List<Permission> permissions, List<String> clusters) {\n    return permissions\n        .stream()\n        .map(permission -> {\n          UserPermissionDTO dto = new UserPermissionDTO();\n          dto.setClusters(clusters);\n          dto.setResource(ResourceTypeDTO.fromValue(permission.getResource().toString().toUpperCase()));\n          dto.setValue(permission.getValue());\n          dto.setActions(permission.getActions()\n              .stream()\n              .map(String::toUpperCase)\n              .map(this::mapAction)\n              .filter(Objects::nonNull)\n              .toList());\n          return dto;\n        })\n        .toList();\n  }\n\n  @Nullable\n  private ActionDTO mapAction(String name) {\n    try {\n      return ActionDTO.fromValue(name);\n    } catch (IllegalArgumentException e) {\n      log.warn(\"Unknown Action [{}], skipping\", name);\n      return null;\n    }\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AclsController.java",
    "content": "package com.provectus.kafka.ui.controller;\n\nimport com.provectus.kafka.ui.api.AclsApi;\nimport com.provectus.kafka.ui.mapper.ClusterMapper;\nimport com.provectus.kafka.ui.model.CreateConsumerAclDTO;\nimport com.provectus.kafka.ui.model.CreateProducerAclDTO;\nimport com.provectus.kafka.ui.model.CreateStreamAppAclDTO;\nimport com.provectus.kafka.ui.model.KafkaAclDTO;\nimport com.provectus.kafka.ui.model.KafkaAclNamePatternTypeDTO;\nimport com.provectus.kafka.ui.model.KafkaAclResourceTypeDTO;\nimport com.provectus.kafka.ui.model.rbac.AccessContext;\nimport com.provectus.kafka.ui.model.rbac.permission.AclAction;\nimport com.provectus.kafka.ui.service.acl.AclsService;\nimport java.util.Optional;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.kafka.common.resource.PatternType;\nimport org.apache.kafka.common.resource.ResourcePatternFilter;\nimport org.apache.kafka.common.resource.ResourceType;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\n@RestController\n@RequiredArgsConstructor\npublic class AclsController extends AbstractController implements AclsApi {\n\n  private final AclsService aclsService;\n\n  @Override\n  public Mono<ResponseEntity<Void>> createAcl(String clusterName, Mono<KafkaAclDTO> kafkaAclDto,\n                                              ServerWebExchange exchange) {\n    AccessContext context = AccessContext.builder()\n        .cluster(clusterName)\n        .aclActions(AclAction.EDIT)\n        .operationName(\"createAcl\")\n        .build();\n\n    return validateAccess(context)\n        .then(kafkaAclDto)\n        .map(ClusterMapper::toAclBinding)\n        .flatMap(binding -> aclsService.createAcl(getCluster(clusterName), binding))\n        .doOnEach(sig -> audit(context, sig))\n        .thenReturn(ResponseEntity.ok().build());\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> deleteAcl(String clusterName, Mono<KafkaAclDTO> kafkaAclDto,\n                                              ServerWebExchange exchange) {\n    AccessContext context = AccessContext.builder()\n        .cluster(clusterName)\n        .aclActions(AclAction.EDIT)\n        .operationName(\"deleteAcl\")\n        .build();\n\n    return validateAccess(context)\n        .then(kafkaAclDto)\n        .map(ClusterMapper::toAclBinding)\n        .flatMap(binding -> aclsService.deleteAcl(getCluster(clusterName), binding))\n        .doOnEach(sig -> audit(context, sig))\n        .thenReturn(ResponseEntity.ok().build());\n  }\n\n  @Override\n  public Mono<ResponseEntity<Flux<KafkaAclDTO>>> listAcls(String clusterName,\n                                                          KafkaAclResourceTypeDTO resourceTypeDto,\n                                                          String resourceName,\n                                                          KafkaAclNamePatternTypeDTO namePatternTypeDto,\n                                                          ServerWebExchange exchange) {\n    AccessContext context = AccessContext.builder()\n        .cluster(clusterName)\n        .aclActions(AclAction.VIEW)\n        .operationName(\"listAcls\")\n        .build();\n\n    var resourceType = Optional.ofNullable(resourceTypeDto)\n        .map(ClusterMapper::mapAclResourceTypeDto)\n        .orElse(ResourceType.ANY);\n\n    var namePatternType = Optional.ofNullable(namePatternTypeDto)\n        .map(ClusterMapper::mapPatternTypeDto)\n        .orElse(PatternType.ANY);\n\n    var filter = new ResourcePatternFilter(resourceType, resourceName, namePatternType);\n\n    return validateAccess(context).then(\n        Mono.just(\n            ResponseEntity.ok(\n                aclsService.listAcls(getCluster(clusterName), filter)\n                    .map(ClusterMapper::toKafkaAclDto)))\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<String>> getAclAsCsv(String clusterName, ServerWebExchange exchange) {\n    AccessContext context = AccessContext.builder()\n        .cluster(clusterName)\n        .aclActions(AclAction.VIEW)\n        .operationName(\"getAclAsCsv\")\n        .build();\n\n    return validateAccess(context).then(\n        aclsService.getAclAsCsvString(getCluster(clusterName))\n            .map(ResponseEntity::ok)\n            .flatMap(Mono::just)\n            .doOnEach(sig -> audit(context, sig))\n    );\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> syncAclsCsv(String clusterName, Mono<String> csvMono, ServerWebExchange exchange) {\n    AccessContext context = AccessContext.builder()\n        .cluster(clusterName)\n        .aclActions(AclAction.EDIT)\n        .operationName(\"syncAclsCsv\")\n        .build();\n\n    return validateAccess(context)\n        .then(csvMono)\n        .flatMap(csv -> aclsService.syncAclWithAclCsv(getCluster(clusterName), csv))\n        .doOnEach(sig -> audit(context, sig))\n        .thenReturn(ResponseEntity.ok().build());\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> createConsumerAcl(String clusterName,\n                                                      Mono<CreateConsumerAclDTO> createConsumerAclDto,\n                                                      ServerWebExchange exchange) {\n    AccessContext context = AccessContext.builder()\n        .cluster(clusterName)\n        .aclActions(AclAction.EDIT)\n        .operationName(\"createConsumerAcl\")\n        .build();\n\n    return validateAccess(context)\n        .then(createConsumerAclDto)\n        .flatMap(req -> aclsService.createConsumerAcl(getCluster(clusterName), req))\n        .doOnEach(sig -> audit(context, sig))\n        .thenReturn(ResponseEntity.ok().build());\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> createProducerAcl(String clusterName,\n                                                      Mono<CreateProducerAclDTO> createProducerAclDto,\n                                                      ServerWebExchange exchange) {\n    AccessContext context = AccessContext.builder()\n        .cluster(clusterName)\n        .aclActions(AclAction.EDIT)\n        .operationName(\"createProducerAcl\")\n        .build();\n\n    return validateAccess(context)\n        .then(createProducerAclDto)\n        .flatMap(req -> aclsService.createProducerAcl(getCluster(clusterName), req))\n        .doOnEach(sig -> audit(context, sig))\n        .thenReturn(ResponseEntity.ok().build());\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> createStreamAppAcl(String clusterName,\n                                                       Mono<CreateStreamAppAclDTO> createStreamAppAclDto,\n                                                       ServerWebExchange exchange) {\n    AccessContext context = AccessContext.builder()\n        .cluster(clusterName)\n        .aclActions(AclAction.EDIT)\n        .operationName(\"createStreamAppAcl\")\n        .build();\n\n    return validateAccess(context)\n        .then(createStreamAppAclDto)\n        .flatMap(req -> aclsService.createStreamAppAcl(getCluster(clusterName), req))\n        .doOnEach(sig -> audit(context, sig))\n        .thenReturn(ResponseEntity.ok().build());\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ApplicationConfigController.java",
    "content": "package com.provectus.kafka.ui.controller;\n\nimport static com.provectus.kafka.ui.model.rbac.permission.ApplicationConfigAction.EDIT;\nimport static com.provectus.kafka.ui.model.rbac.permission.ApplicationConfigAction.VIEW;\n\nimport com.provectus.kafka.ui.api.ApplicationConfigApi;\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.model.ApplicationConfigDTO;\nimport com.provectus.kafka.ui.model.ApplicationConfigPropertiesDTO;\nimport com.provectus.kafka.ui.model.ApplicationConfigValidationDTO;\nimport com.provectus.kafka.ui.model.ApplicationInfoDTO;\nimport com.provectus.kafka.ui.model.ClusterConfigValidationDTO;\nimport com.provectus.kafka.ui.model.RestartRequestDTO;\nimport com.provectus.kafka.ui.model.UploadedFileInfoDTO;\nimport com.provectus.kafka.ui.model.rbac.AccessContext;\nimport com.provectus.kafka.ui.service.ApplicationInfoService;\nimport com.provectus.kafka.ui.service.KafkaClusterFactory;\nimport com.provectus.kafka.ui.util.ApplicationRestarter;\nimport com.provectus.kafka.ui.util.DynamicConfigOperations;\nimport com.provectus.kafka.ui.util.DynamicConfigOperations.PropertiesStructure;\nimport java.util.Map;\nimport javax.annotation.Nullable;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.mapstruct.Mapper;\nimport org.mapstruct.factory.Mappers;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.http.codec.multipart.FilePart;\nimport org.springframework.http.codec.multipart.Part;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.util.function.Tuple2;\nimport reactor.util.function.Tuples;\n\n@Slf4j\n@RestController\n@RequiredArgsConstructor\npublic class ApplicationConfigController extends AbstractController implements ApplicationConfigApi {\n\n  private static final PropertiesMapper MAPPER = Mappers.getMapper(PropertiesMapper.class);\n\n  @Mapper\n  interface PropertiesMapper {\n\n    PropertiesStructure fromDto(ApplicationConfigPropertiesDTO dto);\n\n    ApplicationConfigPropertiesDTO toDto(PropertiesStructure propertiesStructure);\n  }\n\n  private final DynamicConfigOperations dynamicConfigOperations;\n  private final ApplicationRestarter restarter;\n  private final KafkaClusterFactory kafkaClusterFactory;\n  private final ApplicationInfoService applicationInfoService;\n\n  @Override\n  public Mono<ResponseEntity<ApplicationInfoDTO>> getApplicationInfo(ServerWebExchange exchange) {\n    return Mono.just(applicationInfoService.getApplicationInfo()).map(ResponseEntity::ok);\n  }\n\n  @Override\n  public Mono<ResponseEntity<ApplicationConfigDTO>> getCurrentConfig(ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .applicationConfigActions(VIEW)\n        .operationName(\"getCurrentConfig\")\n        .build();\n    return validateAccess(context)\n        .then(Mono.fromSupplier(() -> ResponseEntity.ok(\n            new ApplicationConfigDTO()\n                .properties(MAPPER.toDto(dynamicConfigOperations.getCurrentProperties()))\n        )))\n        .doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> restartWithConfig(Mono<RestartRequestDTO> restartRequestDto,\n                                                      ServerWebExchange exchange) {\n    var context =  AccessContext.builder()\n        .applicationConfigActions(EDIT)\n        .operationName(\"restartWithConfig\")\n        .build();\n    return validateAccess(context)\n        .then(restartRequestDto)\n        .doOnNext(restartDto -> {\n          var newConfig = MAPPER.fromDto(restartDto.getConfig().getProperties());\n          dynamicConfigOperations.persist(newConfig);\n        })\n        .doOnEach(sig -> audit(context, sig))\n        .doOnSuccess(dto -> restarter.requestRestart())\n        .map(dto -> ResponseEntity.ok().build());\n  }\n\n  @Override\n  public Mono<ResponseEntity<UploadedFileInfoDTO>> uploadConfigRelatedFile(Flux<Part> fileFlux,\n                                                                           ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .applicationConfigActions(EDIT)\n        .operationName(\"uploadConfigRelatedFile\")\n        .build();\n    return validateAccess(context)\n        .then(fileFlux.single())\n        .flatMap(file ->\n            dynamicConfigOperations.uploadConfigRelatedFile((FilePart) file)\n                .map(path -> new UploadedFileInfoDTO().location(path.toString()))\n                .map(ResponseEntity::ok))\n        .doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<ApplicationConfigValidationDTO>> validateConfig(Mono<ApplicationConfigDTO> configDto,\n                                                                             ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .applicationConfigActions(EDIT)\n        .operationName(\"validateConfig\")\n        .build();\n    return validateAccess(context)\n        .then(configDto)\n        .flatMap(config -> {\n          PropertiesStructure newConfig = MAPPER.fromDto(config.getProperties());\n          ClustersProperties clustersProperties = newConfig.getKafka();\n          return validateClustersConfig(clustersProperties)\n              .map(validations -> new ApplicationConfigValidationDTO().clusters(validations));\n        })\n        .map(ResponseEntity::ok)\n        .doOnEach(sig -> audit(context, sig));\n  }\n\n  private Mono<Map<String, ClusterConfigValidationDTO>> validateClustersConfig(\n      @Nullable ClustersProperties properties) {\n    if (properties == null || properties.getClusters() == null) {\n      return Mono.just(Map.of());\n    }\n    properties.validateAndSetDefaults();\n    return Flux.fromIterable(properties.getClusters())\n        .flatMap(c -> kafkaClusterFactory.validate(c).map(v -> Tuples.of(c.getName(), v)))\n        .collectMap(Tuple2::getT1, Tuple2::getT2);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AuthController.java",
    "content": "package com.provectus.kafka.ui.controller;\n\nimport java.nio.charset.Charset;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.web.server.csrf.CsrfToken;\nimport org.springframework.util.MultiValueMap;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\n@RestController\n@RequiredArgsConstructor\n@Slf4j\npublic class AuthController {\n\n  @GetMapping(value = \"/auth\", produces = {\"text/html\"})\n  public Mono<byte[]> getAuth(ServerWebExchange exchange) {\n    Mono<CsrfToken> token = exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty());\n    return token\n        .map(AuthController::csrfToken)\n        .defaultIfEmpty(\"\")\n        .map(csrfTokenHtmlInput -> createPage(exchange, csrfTokenHtmlInput));\n  }\n\n  private byte[] createPage(ServerWebExchange exchange, String csrfTokenHtmlInput) {\n    MultiValueMap<String, String> queryParams = exchange.getRequest()\n        .getQueryParams();\n    String contextPath = exchange.getRequest().getPath().contextPath().value();\n    String page =\n        \"<!DOCTYPE html>\\n\" + \"<html lang=\\\"en\\\">\\n\" + \"  <head>\\n\"\n        + \"    <meta charset=\\\"utf-8\\\">\\n\"\n        + \"    <meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1, \"\n        + \"shrink-to-fit=no\\\">\\n\"\n        + \"    <meta name=\\\"description\\\" content=\\\"\\\">\\n\"\n        + \"    <meta name=\\\"author\\\" content=\\\"\\\">\\n\"\n        + \"    <title>Please sign in</title>\\n\"\n        + \"    <link href=\\\"\" + contextPath + \"/static/css/bootstrap.min.css\\\" rel=\\\"stylesheet\\\" \"\n        + \"integrity=\\\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\\\" \"\n        + \"crossorigin=\\\"anonymous\\\">\\n\"\n        + \"    <link href=\\\"\" + contextPath + \"/static/css/signin.css\\\" \"\n        + \"rel=\\\"stylesheet\\\" crossorigin=\\\"anonymous\\\"/>\\n\"\n        + \"  </head>\\n\"\n        + \"  <body>\\n\"\n        + \"     <div class=\\\"container\\\">\\n\"\n        + formLogin(queryParams, contextPath, csrfTokenHtmlInput)\n        + \"    </div>\\n\"\n        + \"  </body>\\n\"\n        + \"</html>\";\n\n    return page.getBytes(Charset.defaultCharset());\n  }\n\n  private String formLogin(\n      MultiValueMap<String, String> queryParams,\n      String contextPath, String csrfTokenHtmlInput) {\n\n    boolean isError = queryParams.containsKey(\"error\");\n    boolean isLogoutSuccess = queryParams.containsKey(\"logout\");\n    return\n        \"      <form class=\\\"form-signin\\\" method=\\\"post\\\" action=\\\"\" + contextPath + \"/auth\\\">\\n\"\n        + \"        <h2 class=\\\"form-signin-heading\\\">Please sign in</h2>\\n\"\n        + createError(isError)\n        + createLogoutSuccess(isLogoutSuccess)\n        + \"        <p>\\n\"\n        + \"          <label for=\\\"username\\\" class=\\\"sr-only\\\">Username</label>\\n\"\n        + \"          <input type=\\\"text\\\" id=\\\"username\\\" name=\\\"username\\\" class=\\\"form-control\\\" \"\n        + \"placeholder=\\\"Username\\\" required autofocus>\\n\"\n        + \"        </p>\\n\" + \"        <p>\\n\"\n        + \"          <label for=\\\"password\\\" class=\\\"sr-only\\\">Password</label>\\n\"\n        + \"          <input type=\\\"password\\\" id=\\\"password\\\" name=\\\"password\\\" \"\n        + \"class=\\\"form-control\\\" placeholder=\\\"Password\\\" required>\\n\"\n        + \"        </p>\\n\" + csrfTokenHtmlInput\n        + \"        <button class=\\\"btn btn-lg btn-primary btn-block\\\" \"\n        + \"type=\\\"submit\\\">Sign in</button>\\n\"\n        + \"      </form>\\n\";\n  }\n\n  private static String csrfToken(CsrfToken token) {\n    return \"          <input type=\\\"hidden\\\" name=\\\"\"\n        + token.getParameterName()\n        + \"\\\" value=\\\"\"\n        + token.getToken()\n        + \"\\\">\\n\";\n  }\n\n  private static String createError(boolean isError) {\n    return isError\n        ? \"<div class=\\\"alert alert-danger\\\" role=\\\"alert\\\">Invalid credentials</div>\"\n        : \"\";\n  }\n\n  private static String createLogoutSuccess(boolean isLogoutSuccess) {\n    return isLogoutSuccess\n        ? \"<div class=\\\"alert alert-success\\\" role=\\\"alert\\\">You have been signed out</div>\"\n        : \"\";\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/BrokersController.java",
    "content": "package com.provectus.kafka.ui.controller;\n\nimport com.provectus.kafka.ui.api.BrokersApi;\nimport com.provectus.kafka.ui.mapper.ClusterMapper;\nimport com.provectus.kafka.ui.model.BrokerConfigDTO;\nimport com.provectus.kafka.ui.model.BrokerConfigItemDTO;\nimport com.provectus.kafka.ui.model.BrokerDTO;\nimport com.provectus.kafka.ui.model.BrokerLogdirUpdateDTO;\nimport com.provectus.kafka.ui.model.BrokerMetricsDTO;\nimport com.provectus.kafka.ui.model.BrokersLogdirsDTO;\nimport com.provectus.kafka.ui.model.rbac.AccessContext;\nimport com.provectus.kafka.ui.model.rbac.permission.ClusterConfigAction;\nimport com.provectus.kafka.ui.service.BrokerService;\nimport java.util.List;\nimport java.util.Map;\nimport javax.annotation.Nullable;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\n@RestController\n@RequiredArgsConstructor\n@Slf4j\npublic class BrokersController extends AbstractController implements BrokersApi {\n  private static final String BROKER_ID = \"brokerId\";\n\n  private final BrokerService brokerService;\n  private final ClusterMapper clusterMapper;\n\n  @Override\n  public Mono<ResponseEntity<Flux<BrokerDTO>>> getBrokers(String clusterName,\n                                                          ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .operationName(\"getBrokers\")\n        .build();\n\n    var job = brokerService.getBrokers(getCluster(clusterName)).map(clusterMapper::toBrokerDto);\n    return validateAccess(context)\n        .thenReturn(ResponseEntity.ok(job))\n        .doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<BrokerMetricsDTO>> getBrokersMetrics(String clusterName, Integer id,\n                                                                  ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .operationName(\"getBrokersMetrics\")\n        .operationParams(Map.of(\"id\", id))\n        .build();\n\n    return validateAccess(context)\n        .then(\n            brokerService.getBrokerMetrics(getCluster(clusterName), id)\n                .map(clusterMapper::toBrokerMetrics)\n                .map(ResponseEntity::ok)\n                .onErrorReturn(ResponseEntity.notFound().build())\n        )\n        .doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Flux<BrokersLogdirsDTO>>> getAllBrokersLogdirs(String clusterName,\n                                                                            @Nullable List<Integer> brokers,\n                                                                            ServerWebExchange exchange) {\n\n    List<Integer> brokerIds = brokers == null ? List.of() : brokers;\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .operationName(\"getAllBrokersLogdirs\")\n        .operationParams(Map.of(\"brokerIds\", brokerIds))\n        .build();\n\n    return validateAccess(context)\n        .thenReturn(ResponseEntity.ok(\n            brokerService.getAllBrokersLogdirs(getCluster(clusterName), brokerIds)))\n        .doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Flux<BrokerConfigDTO>>> getBrokerConfig(String clusterName,\n                                                                     Integer id,\n                                                                     ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .clusterConfigActions(ClusterConfigAction.VIEW)\n        .operationName(\"getBrokerConfig\")\n        .operationParams(Map.of(BROKER_ID, id))\n        .build();\n\n    return validateAccess(context).thenReturn(\n        ResponseEntity.ok(\n            brokerService.getBrokerConfig(getCluster(clusterName), id)\n                .map(clusterMapper::toBrokerConfig))\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> updateBrokerTopicPartitionLogDir(String clusterName,\n                                                                     Integer id,\n                                                                     Mono<BrokerLogdirUpdateDTO> brokerLogdir,\n                                                                     ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .clusterConfigActions(ClusterConfigAction.VIEW, ClusterConfigAction.EDIT)\n        .operationName(\"updateBrokerTopicPartitionLogDir\")\n        .operationParams(Map.of(BROKER_ID, id))\n        .build();\n\n    return validateAccess(context).then(\n        brokerLogdir\n            .flatMap(bld -> brokerService.updateBrokerLogDir(getCluster(clusterName), id, bld))\n            .map(ResponseEntity::ok)\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> updateBrokerConfigByName(String clusterName,\n                                                             Integer id,\n                                                             String name,\n                                                             Mono<BrokerConfigItemDTO> brokerConfig,\n                                                             ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .clusterConfigActions(ClusterConfigAction.VIEW, ClusterConfigAction.EDIT)\n        .operationName(\"updateBrokerConfigByName\")\n        .operationParams(Map.of(BROKER_ID, id))\n        .build();\n\n    return validateAccess(context).then(\n        brokerConfig\n            .flatMap(bci -> brokerService.updateBrokerConfigByName(\n                getCluster(clusterName), id, name, bci.getValue()))\n            .map(ResponseEntity::ok)\n    ).doOnEach(sig -> audit(context, sig));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ClustersController.java",
    "content": "package com.provectus.kafka.ui.controller;\n\nimport com.provectus.kafka.ui.api.ClustersApi;\nimport com.provectus.kafka.ui.model.ClusterDTO;\nimport com.provectus.kafka.ui.model.ClusterMetricsDTO;\nimport com.provectus.kafka.ui.model.ClusterStatsDTO;\nimport com.provectus.kafka.ui.model.rbac.AccessContext;\nimport com.provectus.kafka.ui.service.ClusterService;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\n@RestController\n@RequiredArgsConstructor\n@Slf4j\npublic class ClustersController extends AbstractController implements ClustersApi {\n  private final ClusterService clusterService;\n\n  @Override\n  public Mono<ResponseEntity<Flux<ClusterDTO>>> getClusters(ServerWebExchange exchange) {\n    Flux<ClusterDTO> job = Flux.fromIterable(clusterService.getClusters())\n        .filterWhen(accessControlService::isClusterAccessible);\n\n    return Mono.just(ResponseEntity.ok(job));\n  }\n\n  @Override\n  public Mono<ResponseEntity<ClusterMetricsDTO>> getClusterMetrics(String clusterName,\n                                                                   ServerWebExchange exchange) {\n    AccessContext context = AccessContext.builder()\n        .cluster(clusterName)\n        .operationName(\"getClusterMetrics\")\n        .build();\n\n    return validateAccess(context)\n        .then(\n            clusterService.getClusterMetrics(getCluster(clusterName))\n                .map(ResponseEntity::ok)\n                .onErrorReturn(ResponseEntity.notFound().build())\n        )\n        .doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<ClusterStatsDTO>> getClusterStats(String clusterName,\n                                                               ServerWebExchange exchange) {\n    AccessContext context = AccessContext.builder()\n        .cluster(clusterName)\n        .operationName(\"getClusterStats\")\n        .build();\n\n    return validateAccess(context)\n        .then(\n            clusterService.getClusterStats(getCluster(clusterName))\n                .map(ResponseEntity::ok)\n                .onErrorReturn(ResponseEntity.notFound().build())\n        )\n        .doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<ClusterDTO>> updateClusterInfo(String clusterName,\n                                                            ServerWebExchange exchange) {\n\n    AccessContext context = AccessContext.builder()\n        .cluster(clusterName)\n        .operationName(\"updateClusterInfo\")\n        .build();\n\n    return validateAccess(context)\n        .then(clusterService.updateCluster(getCluster(clusterName)).map(ResponseEntity::ok))\n        .doOnEach(sig -> audit(context, sig));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ConsumerGroupsController.java",
    "content": "package com.provectus.kafka.ui.controller;\n\nimport static com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction.DELETE;\nimport static com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction.RESET_OFFSETS;\nimport static com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction.VIEW;\nimport static java.util.stream.Collectors.toMap;\n\nimport com.provectus.kafka.ui.api.ConsumerGroupsApi;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.mapper.ConsumerGroupMapper;\nimport com.provectus.kafka.ui.model.ConsumerGroupDTO;\nimport com.provectus.kafka.ui.model.ConsumerGroupDetailsDTO;\nimport com.provectus.kafka.ui.model.ConsumerGroupOffsetsResetDTO;\nimport com.provectus.kafka.ui.model.ConsumerGroupOrderingDTO;\nimport com.provectus.kafka.ui.model.ConsumerGroupsPageResponseDTO;\nimport com.provectus.kafka.ui.model.PartitionOffsetDTO;\nimport com.provectus.kafka.ui.model.SortOrderDTO;\nimport com.provectus.kafka.ui.model.rbac.AccessContext;\nimport com.provectus.kafka.ui.model.rbac.permission.TopicAction;\nimport com.provectus.kafka.ui.service.ConsumerGroupService;\nimport com.provectus.kafka.ui.service.OffsetsResetService;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.function.Supplier;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.util.CollectionUtils;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\n@RestController\n@RequiredArgsConstructor\n@Slf4j\npublic class ConsumerGroupsController extends AbstractController implements ConsumerGroupsApi {\n\n  private final ConsumerGroupService consumerGroupService;\n  private final OffsetsResetService offsetsResetService;\n\n  @Value(\"${consumer.groups.page.size:25}\")\n  private int defaultConsumerGroupsPageSize;\n\n  @Override\n  public Mono<ResponseEntity<Void>> deleteConsumerGroup(String clusterName,\n                                                        String id,\n                                                        ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .consumerGroup(id)\n        .consumerGroupActions(DELETE)\n        .operationName(\"deleteConsumerGroup\")\n        .build();\n\n    return validateAccess(context)\n        .then(consumerGroupService.deleteConsumerGroupById(getCluster(clusterName), id))\n        .doOnEach(sig -> audit(context, sig))\n        .thenReturn(ResponseEntity.ok().build());\n  }\n\n  @Override\n  public Mono<ResponseEntity<ConsumerGroupDetailsDTO>> getConsumerGroup(String clusterName,\n                                                                        String consumerGroupId,\n                                                                        ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .consumerGroup(consumerGroupId)\n        .consumerGroupActions(VIEW)\n        .operationName(\"getConsumerGroup\")\n        .build();\n\n    return validateAccess(context)\n        .then(consumerGroupService.getConsumerGroupDetail(getCluster(clusterName), consumerGroupId)\n            .map(ConsumerGroupMapper::toDetailsDto)\n            .map(ResponseEntity::ok))\n        .doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Flux<ConsumerGroupDTO>>> getTopicConsumerGroups(String clusterName,\n                                                                             String topicName,\n                                                                             ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .topic(topicName)\n        .topicActions(TopicAction.VIEW)\n        .operationName(\"getTopicConsumerGroups\")\n        .build();\n\n    Mono<ResponseEntity<Flux<ConsumerGroupDTO>>> job =\n        consumerGroupService.getConsumerGroupsForTopic(getCluster(clusterName), topicName)\n            .flatMapMany(Flux::fromIterable)\n            .filterWhen(cg -> accessControlService.isConsumerGroupAccessible(cg.getGroupId(), clusterName))\n            .map(ConsumerGroupMapper::toDto)\n            .collectList()\n            .map(Flux::fromIterable)\n            .map(ResponseEntity::ok)\n            .switchIfEmpty(Mono.just(ResponseEntity.notFound().build()));\n\n    return validateAccess(context)\n        .then(job)\n        .doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<ConsumerGroupsPageResponseDTO>> getConsumerGroupsPage(\n      String clusterName,\n      Integer page,\n      Integer perPage,\n      String search,\n      ConsumerGroupOrderingDTO orderBy,\n      SortOrderDTO sortOrderDto,\n      ServerWebExchange exchange) {\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        // consumer group access validation is within the service\n        .operationName(\"getConsumerGroupsPage\")\n        .build();\n\n    return validateAccess(context).then(\n        consumerGroupService.getConsumerGroupsPage(\n                getCluster(clusterName),\n                Optional.ofNullable(page).filter(i -> i > 0).orElse(1),\n                Optional.ofNullable(perPage).filter(i -> i > 0).orElse(defaultConsumerGroupsPageSize),\n                search,\n                Optional.ofNullable(orderBy).orElse(ConsumerGroupOrderingDTO.NAME),\n                Optional.ofNullable(sortOrderDto).orElse(SortOrderDTO.ASC)\n            )\n            .map(this::convertPage)\n            .map(ResponseEntity::ok)\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> resetConsumerGroupOffsets(String clusterName,\n                                                              String group,\n                                                              Mono<ConsumerGroupOffsetsResetDTO> resetDto,\n                                                              ServerWebExchange exchange) {\n    return resetDto.flatMap(reset -> {\n      var context = AccessContext.builder()\n          .cluster(clusterName)\n          .topic(reset.getTopic())\n          .topicActions(TopicAction.VIEW)\n          .consumerGroupActions(RESET_OFFSETS)\n          .operationName(\"resetConsumerGroupOffsets\")\n          .build();\n\n      Supplier<Mono<Void>> mono = () -> {\n        var cluster = getCluster(clusterName);\n        switch (reset.getResetType()) {\n          case EARLIEST:\n            return offsetsResetService\n                .resetToEarliest(cluster, group, reset.getTopic(), reset.getPartitions());\n          case LATEST:\n            return offsetsResetService\n                .resetToLatest(cluster, group, reset.getTopic(), reset.getPartitions());\n          case TIMESTAMP:\n            if (reset.getResetToTimestamp() == null) {\n              return Mono.error(\n                  new ValidationException(\n                      \"resetToTimestamp is required when TIMESTAMP reset type used\"\n                  )\n              );\n            }\n            return offsetsResetService\n                .resetToTimestamp(cluster, group, reset.getTopic(), reset.getPartitions(),\n                    reset.getResetToTimestamp());\n          case OFFSET:\n            if (CollectionUtils.isEmpty(reset.getPartitionsOffsets())) {\n              return Mono.error(\n                  new ValidationException(\n                      \"partitionsOffsets is required when OFFSET reset type used\"\n                  )\n              );\n            }\n            Map<Integer, Long> offsets = reset.getPartitionsOffsets().stream()\n                .collect(toMap(PartitionOffsetDTO::getPartition, PartitionOffsetDTO::getOffset));\n            return offsetsResetService.resetToOffsets(cluster, group, reset.getTopic(), offsets);\n          default:\n            return Mono.error(\n                new ValidationException(\"Unknown resetType \" + reset.getResetType())\n            );\n        }\n      };\n\n      return validateAccess(context)\n          .then(mono.get())\n          .doOnEach(sig -> audit(context, sig));\n    }).thenReturn(ResponseEntity.ok().build());\n  }\n\n  private ConsumerGroupsPageResponseDTO convertPage(ConsumerGroupService.ConsumerGroupsPage\n                                                        consumerGroupConsumerGroupsPage) {\n    return new ConsumerGroupsPageResponseDTO()\n        .pageCount(consumerGroupConsumerGroupsPage.totalPages())\n        .consumerGroups(consumerGroupConsumerGroupsPage.consumerGroups()\n            .stream()\n            .map(ConsumerGroupMapper::toDto)\n            .toList());\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KafkaConnectController.java",
    "content": "package com.provectus.kafka.ui.controller;\n\nimport static com.provectus.kafka.ui.model.ConnectorActionDTO.RESTART;\nimport static com.provectus.kafka.ui.model.ConnectorActionDTO.RESTART_ALL_TASKS;\nimport static com.provectus.kafka.ui.model.ConnectorActionDTO.RESTART_FAILED_TASKS;\n\nimport com.provectus.kafka.ui.api.KafkaConnectApi;\nimport com.provectus.kafka.ui.model.ConnectDTO;\nimport com.provectus.kafka.ui.model.ConnectorActionDTO;\nimport com.provectus.kafka.ui.model.ConnectorColumnsToSortDTO;\nimport com.provectus.kafka.ui.model.ConnectorDTO;\nimport com.provectus.kafka.ui.model.ConnectorPluginConfigValidationResponseDTO;\nimport com.provectus.kafka.ui.model.ConnectorPluginDTO;\nimport com.provectus.kafka.ui.model.FullConnectorInfoDTO;\nimport com.provectus.kafka.ui.model.NewConnectorDTO;\nimport com.provectus.kafka.ui.model.SortOrderDTO;\nimport com.provectus.kafka.ui.model.TaskDTO;\nimport com.provectus.kafka.ui.model.rbac.AccessContext;\nimport com.provectus.kafka.ui.model.rbac.permission.ConnectAction;\nimport com.provectus.kafka.ui.service.KafkaConnectService;\nimport java.util.Comparator;\nimport java.util.Map;\nimport java.util.Set;\nimport javax.validation.Valid;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\n@RestController\n@RequiredArgsConstructor\n@Slf4j\npublic class KafkaConnectController extends AbstractController implements KafkaConnectApi {\n  private static final Set<ConnectorActionDTO> RESTART_ACTIONS\n      = Set.of(RESTART, RESTART_FAILED_TASKS, RESTART_ALL_TASKS);\n  private static final String CONNECTOR_NAME = \"connectorName\";\n\n  private final KafkaConnectService kafkaConnectService;\n\n  @Override\n  public Mono<ResponseEntity<Flux<ConnectDTO>>> getConnects(String clusterName,\n                                                            ServerWebExchange exchange) {\n\n    Flux<ConnectDTO> availableConnects = kafkaConnectService.getConnects(getCluster(clusterName))\n        .filterWhen(dto -> accessControlService.isConnectAccessible(dto, clusterName));\n\n    return Mono.just(ResponseEntity.ok(availableConnects));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Flux<String>>> getConnectors(String clusterName, String connectName,\n                                                          ServerWebExchange exchange) {\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .connect(connectName)\n        .connectActions(ConnectAction.VIEW)\n        .operationName(\"getConnectors\")\n        .build();\n\n    return validateAccess(context)\n        .thenReturn(ResponseEntity.ok(kafkaConnectService.getConnectorNames(getCluster(clusterName), connectName)))\n        .doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<ConnectorDTO>> createConnector(String clusterName, String connectName,\n                                                            @Valid Mono<NewConnectorDTO> connector,\n                                                            ServerWebExchange exchange) {\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .connect(connectName)\n        .connectActions(ConnectAction.VIEW, ConnectAction.CREATE)\n        .operationName(\"createConnector\")\n        .build();\n\n    return validateAccess(context).then(\n        kafkaConnectService.createConnector(getCluster(clusterName), connectName, connector)\n            .map(ResponseEntity::ok)\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<ConnectorDTO>> getConnector(String clusterName, String connectName,\n                                                         String connectorName,\n                                                         ServerWebExchange exchange) {\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .connect(connectName)\n        .connectActions(ConnectAction.VIEW)\n        .connector(connectorName)\n        .operationName(\"getConnector\")\n        .build();\n\n    return validateAccess(context).then(\n        kafkaConnectService.getConnector(getCluster(clusterName), connectName, connectorName)\n            .map(ResponseEntity::ok)\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> deleteConnector(String clusterName, String connectName,\n                                                    String connectorName,\n                                                    ServerWebExchange exchange) {\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .connect(connectName)\n        .connectActions(ConnectAction.VIEW, ConnectAction.EDIT)\n        .operationName(\"deleteConnector\")\n        .operationParams(Map.of(CONNECTOR_NAME, connectName))\n        .build();\n\n    return validateAccess(context).then(\n        kafkaConnectService.deleteConnector(getCluster(clusterName), connectName, connectorName)\n            .map(ResponseEntity::ok)\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n\n  @Override\n  public Mono<ResponseEntity<Flux<FullConnectorInfoDTO>>> getAllConnectors(\n      String clusterName,\n      String search,\n      ConnectorColumnsToSortDTO orderBy,\n      SortOrderDTO sortOrder,\n      ServerWebExchange exchange\n  ) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .connectActions(ConnectAction.VIEW, ConnectAction.EDIT)\n        .operationName(\"getAllConnectors\")\n        .build();\n\n    var comparator = sortOrder == null || sortOrder.equals(SortOrderDTO.ASC)\n        ? getConnectorsComparator(orderBy)\n        : getConnectorsComparator(orderBy).reversed();\n\n    Flux<FullConnectorInfoDTO> job = kafkaConnectService.getAllConnectors(getCluster(clusterName), search)\n        .filterWhen(dto -> accessControlService.isConnectAccessible(dto.getConnect(), clusterName))\n        .filterWhen(dto -> accessControlService.isConnectorAccessible(dto.getConnect(), dto.getName(), clusterName))\n        .sort(comparator);\n\n    return Mono.just(ResponseEntity.ok(job))\n        .doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Map<String, Object>>> getConnectorConfig(String clusterName,\n                                                                      String connectName,\n                                                                      String connectorName,\n                                                                      ServerWebExchange exchange) {\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .connect(connectName)\n        .connectActions(ConnectAction.VIEW)\n        .operationName(\"getConnectorConfig\")\n        .build();\n\n    return validateAccess(context).then(\n        kafkaConnectService\n            .getConnectorConfig(getCluster(clusterName), connectName, connectorName)\n            .map(ResponseEntity::ok)\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<ConnectorDTO>> setConnectorConfig(String clusterName, String connectName,\n                                                               String connectorName,\n                                                               Mono<Map<String, Object>> requestBody,\n                                                               ServerWebExchange exchange) {\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .connect(connectName)\n        .connectActions(ConnectAction.VIEW, ConnectAction.EDIT)\n        .operationName(\"setConnectorConfig\")\n        .operationParams(Map.of(CONNECTOR_NAME, connectorName))\n        .build();\n\n    return validateAccess(context).then(\n            kafkaConnectService\n                .setConnectorConfig(getCluster(clusterName), connectName, connectorName, requestBody)\n                .map(ResponseEntity::ok))\n        .doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> updateConnectorState(String clusterName, String connectName,\n                                                         String connectorName,\n                                                         ConnectorActionDTO action,\n                                                         ServerWebExchange exchange) {\n    ConnectAction[] connectActions;\n    if (RESTART_ACTIONS.contains(action)) {\n      connectActions = new ConnectAction[] {ConnectAction.VIEW, ConnectAction.RESTART};\n    } else {\n      connectActions = new ConnectAction[] {ConnectAction.VIEW, ConnectAction.EDIT};\n    }\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .connect(connectName)\n        .connectActions(connectActions)\n        .operationName(\"updateConnectorState\")\n        .operationParams(Map.of(CONNECTOR_NAME, connectorName))\n        .build();\n\n    return validateAccess(context).then(\n        kafkaConnectService\n            .updateConnectorState(getCluster(clusterName), connectName, connectorName, action)\n            .map(ResponseEntity::ok)\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Flux<TaskDTO>>> getConnectorTasks(String clusterName,\n                                                               String connectName,\n                                                               String connectorName,\n                                                               ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .connect(connectName)\n        .connectActions(ConnectAction.VIEW)\n        .operationName(\"getConnectorTasks\")\n        .operationParams(Map.of(CONNECTOR_NAME, connectorName))\n        .build();\n\n    return validateAccess(context).thenReturn(\n        ResponseEntity\n            .ok(kafkaConnectService\n                .getConnectorTasks(getCluster(clusterName), connectName, connectorName))\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> restartConnectorTask(String clusterName, String connectName,\n                                                         String connectorName, Integer taskId,\n                                                         ServerWebExchange exchange) {\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .connect(connectName)\n        .connectActions(ConnectAction.VIEW, ConnectAction.RESTART)\n        .operationName(\"restartConnectorTask\")\n        .operationParams(Map.of(CONNECTOR_NAME, connectorName))\n        .build();\n\n    return validateAccess(context).then(\n        kafkaConnectService\n            .restartConnectorTask(getCluster(clusterName), connectName, connectorName, taskId)\n            .map(ResponseEntity::ok)\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Flux<ConnectorPluginDTO>>> getConnectorPlugins(\n      String clusterName, String connectName, ServerWebExchange exchange) {\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .connect(connectName)\n        .connectActions(ConnectAction.VIEW)\n        .operationName(\"getConnectorPlugins\")\n        .build();\n\n    return validateAccess(context).then(\n        Mono.just(\n            ResponseEntity.ok(\n                kafkaConnectService.getConnectorPlugins(getCluster(clusterName), connectName)))\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<ConnectorPluginConfigValidationResponseDTO>> validateConnectorPluginConfig(\n      String clusterName, String connectName, String pluginName, @Valid Mono<Map<String, Object>> requestBody,\n      ServerWebExchange exchange) {\n    return kafkaConnectService\n        .validateConnectorPluginConfig(\n            getCluster(clusterName), connectName, pluginName, requestBody)\n        .map(ResponseEntity::ok);\n  }\n\n  private Comparator<FullConnectorInfoDTO> getConnectorsComparator(ConnectorColumnsToSortDTO orderBy) {\n    var defaultComparator = Comparator.comparing(FullConnectorInfoDTO::getName);\n    if (orderBy == null) {\n      return defaultComparator;\n    }\n    return switch (orderBy) {\n      case CONNECT -> Comparator.comparing(FullConnectorInfoDTO::getConnect);\n      case TYPE -> Comparator.comparing(FullConnectorInfoDTO::getType);\n      case STATUS -> Comparator.comparing(fullConnectorInfoDTO -> fullConnectorInfoDTO.getStatus().getState());\n      default -> defaultComparator;\n    };\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KsqlController.java",
    "content": "package com.provectus.kafka.ui.controller;\n\nimport com.provectus.kafka.ui.api.KsqlApi;\nimport com.provectus.kafka.ui.model.KsqlCommandV2DTO;\nimport com.provectus.kafka.ui.model.KsqlCommandV2ResponseDTO;\nimport com.provectus.kafka.ui.model.KsqlResponseDTO;\nimport com.provectus.kafka.ui.model.KsqlStreamDescriptionDTO;\nimport com.provectus.kafka.ui.model.KsqlTableDescriptionDTO;\nimport com.provectus.kafka.ui.model.KsqlTableResponseDTO;\nimport com.provectus.kafka.ui.model.rbac.AccessContext;\nimport com.provectus.kafka.ui.model.rbac.permission.KsqlAction;\nimport com.provectus.kafka.ui.service.ksql.KsqlServiceV2;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\n@RestController\n@RequiredArgsConstructor\n@Slf4j\npublic class KsqlController extends AbstractController implements KsqlApi {\n\n  private final KsqlServiceV2 ksqlServiceV2;\n\n  @Override\n  public Mono<ResponseEntity<KsqlCommandV2ResponseDTO>> executeKsql(String clusterName,\n                                                                    Mono<KsqlCommandV2DTO> ksqlCmdDo,\n                                                                    ServerWebExchange exchange) {\n    return ksqlCmdDo.flatMap(\n            command -> {\n              var context = AccessContext.builder()\n                  .cluster(clusterName)\n                  .ksqlActions(KsqlAction.EXECUTE)\n                  .operationName(\"executeKsql\")\n                  .operationParams(command)\n                  .build();\n              return validateAccess(context).thenReturn(\n                      new KsqlCommandV2ResponseDTO().pipeId(\n                          ksqlServiceV2.registerCommand(\n                              getCluster(clusterName),\n                              command.getKsql(),\n                              Optional.ofNullable(command.getStreamsProperties()).orElse(Map.of()))))\n                  .doOnEach(sig -> audit(context, sig));\n            }\n        )\n        .map(ResponseEntity::ok);\n  }\n\n  @Override\n  public Mono<ResponseEntity<Flux<KsqlResponseDTO>>> openKsqlResponsePipe(String clusterName,\n                                                                          String pipeId,\n                                                                          ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .ksqlActions(KsqlAction.EXECUTE)\n        .operationName(\"openKsqlResponsePipe\")\n        .build();\n\n    return validateAccess(context).thenReturn(\n        ResponseEntity.ok(ksqlServiceV2.execute(pipeId)\n            .map(table -> new KsqlResponseDTO()\n                .table(\n                    new KsqlTableResponseDTO()\n                        .header(table.getHeader())\n                        .columnNames(table.getColumnNames())\n                        .values((List<List<Object>>) ((List<?>) (table.getValues()))))))\n    );\n  }\n\n  @Override\n  public Mono<ResponseEntity<Flux<KsqlStreamDescriptionDTO>>> listStreams(String clusterName,\n                                                                          ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .ksqlActions(KsqlAction.EXECUTE)\n        .operationName(\"listStreams\")\n        .build();\n\n    return validateAccess(context)\n        .thenReturn(ResponseEntity.ok(ksqlServiceV2.listStreams(getCluster(clusterName))))\n        .doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Flux<KsqlTableDescriptionDTO>>> listTables(String clusterName,\n                                                                        ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .ksqlActions(KsqlAction.EXECUTE)\n        .operationName(\"listTables\")\n        .build();\n\n    return validateAccess(context)\n        .thenReturn(ResponseEntity.ok(ksqlServiceV2.listTables(getCluster(clusterName))))\n        .doOnEach(sig -> audit(context, sig));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/MessagesController.java",
    "content": "package com.provectus.kafka.ui.controller;\n\nimport static com.provectus.kafka.ui.model.rbac.permission.TopicAction.MESSAGES_DELETE;\nimport static com.provectus.kafka.ui.model.rbac.permission.TopicAction.MESSAGES_PRODUCE;\nimport static com.provectus.kafka.ui.model.rbac.permission.TopicAction.MESSAGES_READ;\nimport static com.provectus.kafka.ui.serde.api.Serde.Target.KEY;\nimport static com.provectus.kafka.ui.serde.api.Serde.Target.VALUE;\nimport static java.util.stream.Collectors.toMap;\n\nimport com.provectus.kafka.ui.api.MessagesApi;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.model.ConsumerPosition;\nimport com.provectus.kafka.ui.model.CreateTopicMessageDTO;\nimport com.provectus.kafka.ui.model.MessageFilterTypeDTO;\nimport com.provectus.kafka.ui.model.SeekDirectionDTO;\nimport com.provectus.kafka.ui.model.SeekTypeDTO;\nimport com.provectus.kafka.ui.model.SerdeUsageDTO;\nimport com.provectus.kafka.ui.model.SmartFilterTestExecutionDTO;\nimport com.provectus.kafka.ui.model.SmartFilterTestExecutionResultDTO;\nimport com.provectus.kafka.ui.model.TopicMessageEventDTO;\nimport com.provectus.kafka.ui.model.TopicSerdeSuggestionDTO;\nimport com.provectus.kafka.ui.model.rbac.AccessContext;\nimport com.provectus.kafka.ui.model.rbac.permission.AuditAction;\nimport com.provectus.kafka.ui.model.rbac.permission.TopicAction;\nimport com.provectus.kafka.ui.service.DeserializationService;\nimport com.provectus.kafka.ui.service.MessagesService;\nimport com.provectus.kafka.ui.util.DynamicConfigOperations;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport javax.annotation.Nullable;\nimport javax.validation.Valid;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.commons.lang3.tuple.Pair;\nimport org.apache.kafka.common.TopicPartition;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Schedulers;\n\n@RestController\n@RequiredArgsConstructor\n@Slf4j\npublic class MessagesController extends AbstractController implements MessagesApi {\n\n  private final MessagesService messagesService;\n  private final DeserializationService deserializationService;\n  private final DynamicConfigOperations dynamicConfigOperations;\n\n  @Override\n  public Mono<ResponseEntity<Void>> deleteTopicMessages(\n      String clusterName, String topicName, @Valid List<Integer> partitions,\n      ServerWebExchange exchange) {\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .topic(topicName)\n        .topicActions(MESSAGES_DELETE)\n        .build();\n\n    return validateAccess(context).<ResponseEntity<Void>>then(\n        messagesService.deleteTopicMessages(\n            getCluster(clusterName),\n            topicName,\n            Optional.ofNullable(partitions).orElse(List.of())\n        ).thenReturn(ResponseEntity.ok().build())\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<SmartFilterTestExecutionResultDTO>> executeSmartFilterTest(\n      Mono<SmartFilterTestExecutionDTO> smartFilterTestExecutionDto, ServerWebExchange exchange) {\n    return smartFilterTestExecutionDto\n        .map(MessagesService::execSmartFilterTest)\n        .map(ResponseEntity::ok);\n  }\n\n  @Override\n  public Mono<ResponseEntity<Flux<TopicMessageEventDTO>>> getTopicMessages(String clusterName,\n                                                                           String topicName,\n                                                                           SeekTypeDTO seekType,\n                                                                           List<String> seekTo,\n                                                                           Integer limit,\n                                                                           String q,\n                                                                           MessageFilterTypeDTO filterQueryType,\n                                                                           SeekDirectionDTO seekDirection,\n                                                                           String keySerde,\n                                                                           String valueSerde,\n                                                                           ServerWebExchange exchange) {\n    var contextBuilder = AccessContext.builder()\n        .cluster(clusterName)\n        .topic(topicName)\n        .topicActions(MESSAGES_READ)\n        .operationName(\"getTopicMessages\");\n\n    if (StringUtils.isNoneEmpty(q) && MessageFilterTypeDTO.GROOVY_SCRIPT == filterQueryType) {\n      dynamicConfigOperations.checkIfFilteringGroovyEnabled();\n    }\n\n    if (auditService.isAuditTopic(getCluster(clusterName), topicName)) {\n      contextBuilder.auditActions(AuditAction.VIEW);\n    }\n\n    seekType = seekType != null ? seekType : SeekTypeDTO.BEGINNING;\n    seekDirection = seekDirection != null ? seekDirection : SeekDirectionDTO.FORWARD;\n    filterQueryType = filterQueryType != null ? filterQueryType : MessageFilterTypeDTO.STRING_CONTAINS;\n\n    var positions = new ConsumerPosition(\n        seekType,\n        topicName,\n        parseSeekTo(topicName, seekType, seekTo)\n    );\n    Mono<ResponseEntity<Flux<TopicMessageEventDTO>>> job = Mono.just(\n        ResponseEntity.ok(\n            messagesService.loadMessages(\n                getCluster(clusterName), topicName, positions, q, filterQueryType,\n                limit, seekDirection, keySerde, valueSerde)\n        )\n    );\n\n    var context = contextBuilder.build();\n    return validateAccess(context)\n        .then(job)\n        .doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> sendTopicMessages(\n      String clusterName, String topicName, @Valid Mono<CreateTopicMessageDTO> createTopicMessage,\n      ServerWebExchange exchange) {\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .topic(topicName)\n        .topicActions(MESSAGES_PRODUCE)\n        .operationName(\"sendTopicMessages\")\n        .build();\n\n    return validateAccess(context).then(\n        createTopicMessage.flatMap(msg ->\n            messagesService.sendMessage(getCluster(clusterName), topicName, msg).then()\n        ).map(ResponseEntity::ok)\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  /**\n   * The format is [partition]::[offset] for specifying offsets\n   * or [partition]::[timestamp in millis] for specifying timestamps.\n   */\n  @Nullable\n  private Map<TopicPartition, Long> parseSeekTo(String topic, SeekTypeDTO seekType, List<String> seekTo) {\n    if (seekTo == null || seekTo.isEmpty()) {\n      if (seekType == SeekTypeDTO.LATEST || seekType == SeekTypeDTO.BEGINNING) {\n        return null;\n      }\n      throw new ValidationException(\"seekTo should be set if seekType is \" + seekType);\n    }\n    return seekTo.stream()\n        .map(p -> {\n          String[] split = p.split(\"::\");\n          if (split.length != 2) {\n            throw new IllegalArgumentException(\n                \"Wrong seekTo argument format. See API docs for details\");\n          }\n\n          return Pair.of(\n              new TopicPartition(topic, Integer.parseInt(split[0])),\n              Long.parseLong(split[1])\n          );\n        })\n        .collect(toMap(Pair::getKey, Pair::getValue));\n  }\n\n  @Override\n  public Mono<ResponseEntity<TopicSerdeSuggestionDTO>> getSerdes(String clusterName,\n                                                                 String topicName,\n                                                                 SerdeUsageDTO use,\n                                                                 ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .topic(topicName)\n        .topicActions(TopicAction.VIEW)\n        .operationName(\"getSerdes\")\n        .build();\n\n    TopicSerdeSuggestionDTO dto = new TopicSerdeSuggestionDTO()\n        .key(use == SerdeUsageDTO.SERIALIZE\n            ? deserializationService.getSerdesForSerialize(getCluster(clusterName), topicName, KEY)\n            : deserializationService.getSerdesForDeserialize(getCluster(clusterName), topicName, KEY))\n        .value(use == SerdeUsageDTO.SERIALIZE\n            ? deserializationService.getSerdesForSerialize(getCluster(clusterName), topicName, VALUE)\n            : deserializationService.getSerdesForDeserialize(getCluster(clusterName), topicName, VALUE));\n\n    return validateAccess(context).then(\n        Mono.just(dto)\n            .subscribeOn(Schedulers.boundedElastic())\n            .map(ResponseEntity::ok)\n    );\n  }\n\n\n\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/SchemasController.java",
    "content": "package com.provectus.kafka.ui.controller;\n\nimport com.provectus.kafka.ui.api.SchemasApi;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.mapper.KafkaSrMapper;\nimport com.provectus.kafka.ui.mapper.KafkaSrMapperImpl;\nimport com.provectus.kafka.ui.model.CompatibilityCheckResponseDTO;\nimport com.provectus.kafka.ui.model.CompatibilityLevelDTO;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.NewSchemaSubjectDTO;\nimport com.provectus.kafka.ui.model.SchemaSubjectDTO;\nimport com.provectus.kafka.ui.model.SchemaSubjectsResponseDTO;\nimport com.provectus.kafka.ui.model.rbac.AccessContext;\nimport com.provectus.kafka.ui.model.rbac.permission.SchemaAction;\nimport com.provectus.kafka.ui.service.SchemaRegistryService;\nimport java.util.List;\nimport java.util.Map;\nimport javax.validation.Valid;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\n@RestController\n@RequiredArgsConstructor\n@Slf4j\npublic class SchemasController extends AbstractController implements SchemasApi {\n\n  private static final Integer DEFAULT_PAGE_SIZE = 25;\n\n  private final KafkaSrMapper kafkaSrMapper = new KafkaSrMapperImpl();\n\n  private final SchemaRegistryService schemaRegistryService;\n\n  @Override\n  protected KafkaCluster getCluster(String clusterName) {\n    var c = super.getCluster(clusterName);\n    if (c.getSchemaRegistryClient() == null) {\n      throw new ValidationException(\"Schema Registry is not set for cluster \" + clusterName);\n    }\n    return c;\n  }\n\n  @Override\n  public Mono<ResponseEntity<CompatibilityCheckResponseDTO>> checkSchemaCompatibility(\n      String clusterName, String subject, @Valid Mono<NewSchemaSubjectDTO> newSchemaSubjectMono,\n      ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .schema(subject)\n        .schemaActions(SchemaAction.VIEW)\n        .operationName(\"checkSchemaCompatibility\")\n        .build();\n\n    return validateAccess(context).then(\n        newSchemaSubjectMono.flatMap(subjectDTO ->\n                schemaRegistryService.checksSchemaCompatibility(\n                    getCluster(clusterName),\n                    subject,\n                    kafkaSrMapper.fromDto(subjectDTO)\n                ))\n            .map(kafkaSrMapper::toDto)\n            .map(ResponseEntity::ok)\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<SchemaSubjectDTO>> createNewSchema(\n      String clusterName, @Valid Mono<NewSchemaSubjectDTO> newSchemaSubjectMono,\n      ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .schemaActions(SchemaAction.CREATE)\n        .operationName(\"createNewSchema\")\n        .build();\n\n    return validateAccess(context).then(\n        newSchemaSubjectMono.flatMap(newSubject ->\n                schemaRegistryService.registerNewSchema(\n                    getCluster(clusterName),\n                    newSubject.getSubject(),\n                    kafkaSrMapper.fromDto(newSubject)\n                )\n            ).map(kafkaSrMapper::toDto)\n            .map(ResponseEntity::ok)\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> deleteLatestSchema(\n      String clusterName, String subject, ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .schema(subject)\n        .schemaActions(SchemaAction.DELETE)\n        .operationName(\"deleteLatestSchema\")\n        .build();\n\n    return validateAccess(context).then(\n        schemaRegistryService.deleteLatestSchemaSubject(getCluster(clusterName), subject)\n            .doOnEach(sig -> audit(context, sig))\n            .thenReturn(ResponseEntity.ok().build())\n    );\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> deleteSchema(\n      String clusterName, String subject, ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .schema(subject)\n        .schemaActions(SchemaAction.DELETE)\n        .operationName(\"deleteSchema\")\n        .build();\n\n    return validateAccess(context).then(\n        schemaRegistryService.deleteSchemaSubjectEntirely(getCluster(clusterName), subject)\n            .doOnEach(sig -> audit(context, sig))\n            .thenReturn(ResponseEntity.ok().build())\n    );\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> deleteSchemaByVersion(\n      String clusterName, String subjectName, Integer version, ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .schema(subjectName)\n        .schemaActions(SchemaAction.DELETE)\n        .operationName(\"deleteSchemaByVersion\")\n        .build();\n\n    return validateAccess(context).then(\n        schemaRegistryService.deleteSchemaSubjectByVersion(getCluster(clusterName), subjectName, version)\n            .doOnEach(sig -> audit(context, sig))\n            .thenReturn(ResponseEntity.ok().build())\n    );\n  }\n\n  @Override\n  public Mono<ResponseEntity<Flux<SchemaSubjectDTO>>> getAllVersionsBySubject(\n      String clusterName, String subjectName, ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .schema(subjectName)\n        .schemaActions(SchemaAction.VIEW)\n        .operationName(\"getAllVersionsBySubject\")\n        .build();\n\n    Flux<SchemaSubjectDTO> schemas =\n        schemaRegistryService.getAllVersionsBySubject(getCluster(clusterName), subjectName)\n            .map(kafkaSrMapper::toDto);\n\n    return validateAccess(context)\n        .thenReturn(ResponseEntity.ok(schemas))\n        .doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<CompatibilityLevelDTO>> getGlobalSchemaCompatibilityLevel(\n      String clusterName, ServerWebExchange exchange) {\n    return schemaRegistryService.getGlobalSchemaCompatibilityLevel(getCluster(clusterName))\n        .map(c -> new CompatibilityLevelDTO().compatibility(kafkaSrMapper.toDto(c)))\n        .map(ResponseEntity::ok)\n        .defaultIfEmpty(ResponseEntity.notFound().build());\n  }\n\n  @Override\n  public Mono<ResponseEntity<SchemaSubjectDTO>> getLatestSchema(String clusterName,\n                                                                String subject,\n                                                                ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .schema(subject)\n        .schemaActions(SchemaAction.VIEW)\n        .operationName(\"getLatestSchema\")\n        .build();\n\n    return validateAccess(context).then(\n        schemaRegistryService.getLatestSchemaVersionBySubject(getCluster(clusterName), subject)\n            .map(kafkaSrMapper::toDto)\n            .map(ResponseEntity::ok)\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<SchemaSubjectDTO>> getSchemaByVersion(\n      String clusterName, String subject, Integer version, ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .schema(subject)\n        .schemaActions(SchemaAction.VIEW)\n        .operationName(\"getSchemaByVersion\")\n        .operationParams(Map.of(\"subject\", subject, \"version\", version))\n        .build();\n\n    return validateAccess(context).then(\n        schemaRegistryService.getSchemaSubjectByVersion(\n                getCluster(clusterName), subject, version)\n            .map(kafkaSrMapper::toDto)\n            .map(ResponseEntity::ok)\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<SchemaSubjectsResponseDTO>> getSchemas(String clusterName,\n                                                                    @Valid Integer pageNum,\n                                                                    @Valid Integer perPage,\n                                                                    @Valid String search,\n                                                                    ServerWebExchange serverWebExchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .operationName(\"getSchemas\")\n        .build();\n\n    return schemaRegistryService\n        .getAllSubjectNames(getCluster(clusterName))\n        .flatMapIterable(l -> l)\n        .filterWhen(schema -> accessControlService.isSchemaAccessible(schema, clusterName))\n        .collectList()\n        .flatMap(subjects -> {\n          int pageSize = perPage != null && perPage > 0 ? perPage : DEFAULT_PAGE_SIZE;\n          int subjectToSkip = ((pageNum != null && pageNum > 0 ? pageNum : 1) - 1) * pageSize;\n          List<String> filteredSubjects = subjects\n              .stream()\n              .filter(subj -> search == null || StringUtils.containsIgnoreCase(subj, search))\n              .sorted().toList();\n          var totalPages = (filteredSubjects.size() / pageSize)\n              + (filteredSubjects.size() % pageSize == 0 ? 0 : 1);\n          List<String> subjectsToRender = filteredSubjects.stream()\n              .skip(subjectToSkip)\n              .limit(pageSize)\n              .toList();\n          return schemaRegistryService.getAllLatestVersionSchemas(getCluster(clusterName), subjectsToRender)\n              .map(subjs -> subjs.stream().map(kafkaSrMapper::toDto).toList())\n              .map(subjs -> new SchemaSubjectsResponseDTO().pageCount(totalPages).schemas(subjs));\n        }).map(ResponseEntity::ok)\n        .doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> updateGlobalSchemaCompatibilityLevel(\n      String clusterName, @Valid Mono<CompatibilityLevelDTO> compatibilityLevelMono,\n      ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .schemaActions(SchemaAction.MODIFY_GLOBAL_COMPATIBILITY)\n        .operationName(\"updateGlobalSchemaCompatibilityLevel\")\n        .build();\n\n    return validateAccess(context).then(\n        compatibilityLevelMono\n            .flatMap(compatibilityLevelDTO ->\n                schemaRegistryService.updateGlobalSchemaCompatibility(\n                    getCluster(clusterName),\n                    kafkaSrMapper.fromDto(compatibilityLevelDTO.getCompatibility())\n                ))\n            .doOnEach(sig -> audit(context, sig))\n            .thenReturn(ResponseEntity.ok().build())\n    );\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> updateSchemaCompatibilityLevel(\n      String clusterName, String subject, @Valid Mono<CompatibilityLevelDTO> compatibilityLevelMono,\n      ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .schemaActions(SchemaAction.EDIT)\n        .operationName(\"updateSchemaCompatibilityLevel\")\n        .operationParams(Map.of(\"subject\", subject))\n        .build();\n\n    return validateAccess(context).then(\n        compatibilityLevelMono\n            .flatMap(compatibilityLevelDTO ->\n                schemaRegistryService.updateSchemaCompatibility(\n                    getCluster(clusterName),\n                    subject,\n                    kafkaSrMapper.fromDto(compatibilityLevelDTO.getCompatibility())\n                ))\n            .doOnEach(sig -> audit(context, sig))\n            .thenReturn(ResponseEntity.ok().build())\n    );\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/StaticController.java",
    "content": "package com.provectus.kafka.ui.controller;\n\nimport com.provectus.kafka.ui.util.ResourceUtil;\nimport java.util.concurrent.atomic.AtomicReference;\nimport lombok.RequiredArgsConstructor;\nimport lombok.SneakyThrows;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.core.io.Resource;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\n@RestController\n@RequiredArgsConstructor\n@Slf4j\npublic class StaticController {\n\n  @Value(\"classpath:static/index.html\")\n  private Resource indexFile;\n  @Value(\"classpath:static/manifest.json\")\n  private Resource manifestFile;\n\n  private final AtomicReference<String> renderedIndexFile = new AtomicReference<>();\n  private final AtomicReference<String> renderedManifestFile = new AtomicReference<>();\n\n  @GetMapping(value = \"/index.html\", produces = {\"text/html\"})\n  public Mono<ResponseEntity<String>> getIndex(ServerWebExchange exchange) {\n    return Mono.just(ResponseEntity.ok(getRenderedFile(exchange, renderedIndexFile, indexFile)));\n  }\n\n  @GetMapping(value = \"/manifest.json\", produces = {\"application/json\"})\n  public Mono<ResponseEntity<String>> getManifest(ServerWebExchange exchange) {\n    return Mono.just(ResponseEntity.ok(getRenderedFile(exchange, renderedManifestFile, manifestFile)));\n  }\n\n  public String getRenderedFile(ServerWebExchange exchange, AtomicReference<String> renderedFile, Resource file) {\n    String rendered = renderedFile.get();\n    if (rendered == null) {\n      rendered = buildFile(file, exchange.getRequest().getPath().contextPath().value());\n      if (renderedFile.compareAndSet(null, rendered)) {\n        return rendered;\n      } else {\n        return renderedFile.get();\n      }\n    } else {\n      return rendered;\n    }\n  }\n\n  @SneakyThrows\n  private String buildFile(Resource file, String contextPath) {\n    return ResourceUtil.readAsString(file)\n        .replace(\"\\\"assets/\", \"\\\"\" + contextPath + \"/assets/\")\n        .replace(\"PUBLIC-PATH-VARIABLE\",  contextPath);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/TopicsController.java",
    "content": "package com.provectus.kafka.ui.controller;\n\nimport static com.provectus.kafka.ui.model.rbac.permission.TopicAction.CREATE;\nimport static com.provectus.kafka.ui.model.rbac.permission.TopicAction.DELETE;\nimport static com.provectus.kafka.ui.model.rbac.permission.TopicAction.EDIT;\nimport static com.provectus.kafka.ui.model.rbac.permission.TopicAction.MESSAGES_READ;\nimport static com.provectus.kafka.ui.model.rbac.permission.TopicAction.VIEW;\nimport static java.util.stream.Collectors.toList;\n\nimport com.provectus.kafka.ui.api.TopicsApi;\nimport com.provectus.kafka.ui.mapper.ClusterMapper;\nimport com.provectus.kafka.ui.model.InternalTopic;\nimport com.provectus.kafka.ui.model.InternalTopicConfig;\nimport com.provectus.kafka.ui.model.PartitionsIncreaseDTO;\nimport com.provectus.kafka.ui.model.PartitionsIncreaseResponseDTO;\nimport com.provectus.kafka.ui.model.ReplicationFactorChangeDTO;\nimport com.provectus.kafka.ui.model.ReplicationFactorChangeResponseDTO;\nimport com.provectus.kafka.ui.model.SortOrderDTO;\nimport com.provectus.kafka.ui.model.TopicAnalysisDTO;\nimport com.provectus.kafka.ui.model.TopicColumnsToSortDTO;\nimport com.provectus.kafka.ui.model.TopicConfigDTO;\nimport com.provectus.kafka.ui.model.TopicCreationDTO;\nimport com.provectus.kafka.ui.model.TopicDTO;\nimport com.provectus.kafka.ui.model.TopicDetailsDTO;\nimport com.provectus.kafka.ui.model.TopicProducerStateDTO;\nimport com.provectus.kafka.ui.model.TopicUpdateDTO;\nimport com.provectus.kafka.ui.model.TopicsResponseDTO;\nimport com.provectus.kafka.ui.model.rbac.AccessContext;\nimport com.provectus.kafka.ui.service.TopicsService;\nimport com.provectus.kafka.ui.service.analyze.TopicAnalysisService;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Map;\nimport javax.validation.Valid;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\n@RestController\n@RequiredArgsConstructor\n@Slf4j\npublic class TopicsController extends AbstractController implements TopicsApi {\n\n  private static final Integer DEFAULT_PAGE_SIZE = 25;\n\n  private final TopicsService topicsService;\n  private final TopicAnalysisService topicAnalysisService;\n  private final ClusterMapper clusterMapper;\n\n  @Override\n  public Mono<ResponseEntity<TopicDTO>> createTopic(\n      String clusterName, @Valid Mono<TopicCreationDTO> topicCreationMono, ServerWebExchange exchange) {\n    return topicCreationMono.flatMap(topicCreation -> {\n      var context = AccessContext.builder()\n          .cluster(clusterName)\n          .topicActions(CREATE)\n          .operationName(\"createTopic\")\n          .operationParams(topicCreation)\n          .build();\n\n      return validateAccess(context)\n          .then(topicsService.createTopic(getCluster(clusterName), topicCreation))\n          .map(clusterMapper::toTopic)\n          .map(s -> new ResponseEntity<>(s, HttpStatus.OK))\n          .switchIfEmpty(Mono.just(ResponseEntity.notFound().build()))\n          .doOnEach(sig -> audit(context, sig));\n    });\n  }\n\n  @Override\n  public Mono<ResponseEntity<TopicDTO>> recreateTopic(String clusterName,\n                                                      String topicName, ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .topic(topicName)\n        .topicActions(VIEW, CREATE, DELETE)\n        .operationName(\"recreateTopic\")\n        .build();\n\n    return validateAccess(context).then(\n        topicsService.recreateTopic(getCluster(clusterName), topicName)\n            .map(clusterMapper::toTopic)\n            .map(s -> new ResponseEntity<>(s, HttpStatus.CREATED))\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<TopicDTO>> cloneTopic(\n      String clusterName, String topicName, String newTopicName, ServerWebExchange exchange) {\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .topic(topicName)\n        .topicActions(VIEW, CREATE)\n        .operationName(\"cloneTopic\")\n        .operationParams(Map.of(\"newTopicName\", newTopicName))\n        .build();\n\n    return validateAccess(context)\n        .then(topicsService.cloneTopic(getCluster(clusterName), topicName, newTopicName)\n            .map(clusterMapper::toTopic)\n            .map(s -> new ResponseEntity<>(s, HttpStatus.CREATED))\n        ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> deleteTopic(\n      String clusterName, String topicName, ServerWebExchange exchange) {\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .topic(topicName)\n        .topicActions(DELETE)\n        .operationName(\"deleteTopic\")\n        .build();\n\n    return validateAccess(context)\n        .then(\n            topicsService.deleteTopic(getCluster(clusterName), topicName)\n                .thenReturn(ResponseEntity.ok().<Void>build())\n        ).doOnEach(sig -> audit(context, sig));\n  }\n\n\n  @Override\n  public Mono<ResponseEntity<Flux<TopicConfigDTO>>> getTopicConfigs(\n      String clusterName, String topicName, ServerWebExchange exchange) {\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .topic(topicName)\n        .topicActions(VIEW)\n        .operationName(\"getTopicConfigs\")\n        .build();\n\n    return validateAccess(context).then(\n        topicsService.getTopicConfigs(getCluster(clusterName), topicName)\n            .map(lst -> lst.stream()\n                .map(InternalTopicConfig::from)\n                .map(clusterMapper::toTopicConfig)\n                .toList())\n            .map(Flux::fromIterable)\n            .map(ResponseEntity::ok)\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<TopicDetailsDTO>> getTopicDetails(\n      String clusterName, String topicName, ServerWebExchange exchange) {\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .topic(topicName)\n        .topicActions(VIEW)\n        .operationName(\"getTopicDetails\")\n        .build();\n\n    return validateAccess(context).then(\n        topicsService.getTopicDetails(getCluster(clusterName), topicName)\n            .map(clusterMapper::toTopicDetails)\n            .map(ResponseEntity::ok)\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<TopicsResponseDTO>> getTopics(String clusterName,\n                                                           @Valid Integer page,\n                                                           @Valid Integer perPage,\n                                                           @Valid Boolean showInternal,\n                                                           @Valid String search,\n                                                           @Valid TopicColumnsToSortDTO orderBy,\n                                                           @Valid SortOrderDTO sortOrder,\n                                                           ServerWebExchange exchange) {\n\n    AccessContext context = AccessContext.builder()\n        .cluster(clusterName)\n        .operationName(\"getTopics\")\n        .build();\n\n    return topicsService.getTopicsForPagination(getCluster(clusterName))\n        .flatMap(topics -> accessControlService.filterViewableTopics(topics, clusterName))\n        .flatMap(topics -> {\n          int pageSize = perPage != null && perPage > 0 ? perPage : DEFAULT_PAGE_SIZE;\n          var topicsToSkip = ((page != null && page > 0 ? page : 1) - 1) * pageSize;\n          var comparator = sortOrder == null || !sortOrder.equals(SortOrderDTO.DESC)\n              ? getComparatorForTopic(orderBy) : getComparatorForTopic(orderBy).reversed();\n          List<InternalTopic> filtered = topics.stream()\n              .filter(topic -> !topic.isInternal()\n                  || showInternal != null && showInternal)\n              .filter(topic -> search == null || StringUtils.containsIgnoreCase(topic.getName(), search))\n              .sorted(comparator)\n              .toList();\n          var totalPages = (filtered.size() / pageSize)\n              + (filtered.size() % pageSize == 0 ? 0 : 1);\n\n          List<String> topicsPage = filtered.stream()\n              .skip(topicsToSkip)\n              .limit(pageSize)\n              .map(InternalTopic::getName)\n              .collect(toList());\n\n          return topicsService.loadTopics(getCluster(clusterName), topicsPage)\n              .map(topicsToRender ->\n                  new TopicsResponseDTO()\n                      .topics(topicsToRender.stream().map(clusterMapper::toTopic).toList())\n                      .pageCount(totalPages));\n        })\n        .map(ResponseEntity::ok)\n        .doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<TopicDTO>> updateTopic(\n      String clusterName, String topicName, @Valid Mono<TopicUpdateDTO> topicUpdate,\n      ServerWebExchange exchange) {\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .topic(topicName)\n        .topicActions(VIEW, EDIT)\n        .operationName(\"updateTopic\")\n        .build();\n\n    return validateAccess(context).then(\n        topicsService\n            .updateTopic(getCluster(clusterName), topicName, topicUpdate)\n            .map(clusterMapper::toTopic)\n            .map(ResponseEntity::ok)\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<PartitionsIncreaseResponseDTO>> increaseTopicPartitions(\n      String clusterName, String topicName,\n      Mono<PartitionsIncreaseDTO> partitionsIncrease,\n      ServerWebExchange exchange) {\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .topic(topicName)\n        .topicActions(VIEW, EDIT)\n        .build();\n\n    return validateAccess(context).then(\n        partitionsIncrease.flatMap(partitions ->\n            topicsService.increaseTopicPartitions(getCluster(clusterName), topicName, partitions)\n        ).map(ResponseEntity::ok)\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<ReplicationFactorChangeResponseDTO>> changeReplicationFactor(\n      String clusterName, String topicName,\n      Mono<ReplicationFactorChangeDTO> replicationFactorChange,\n      ServerWebExchange exchange) {\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .topic(topicName)\n        .topicActions(VIEW, EDIT)\n        .operationName(\"changeReplicationFactor\")\n        .build();\n\n    return validateAccess(context).then(\n        replicationFactorChange\n            .flatMap(rfc ->\n                topicsService.changeReplicationFactor(getCluster(clusterName), topicName, rfc))\n            .map(ResponseEntity::ok)\n    ).doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> analyzeTopic(String clusterName, String topicName, ServerWebExchange exchange) {\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .topic(topicName)\n        .topicActions(MESSAGES_READ)\n        .operationName(\"analyzeTopic\")\n        .build();\n\n    return validateAccess(context).then(\n        topicAnalysisService.analyze(getCluster(clusterName), topicName)\n            .doOnEach(sig -> audit(context, sig))\n            .thenReturn(ResponseEntity.ok().build())\n    );\n  }\n\n  @Override\n  public Mono<ResponseEntity<Void>> cancelTopicAnalysis(String clusterName, String topicName,\n                                                        ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .topic(topicName)\n        .topicActions(MESSAGES_READ)\n        .operationName(\"cancelTopicAnalysis\")\n        .build();\n\n    return validateAccess(context)\n        .then(Mono.fromRunnable(() -> topicAnalysisService.cancelAnalysis(getCluster(clusterName), topicName)))\n        .doOnEach(sig -> audit(context, sig))\n        .thenReturn(ResponseEntity.ok().build());\n  }\n\n\n  @Override\n  public Mono<ResponseEntity<TopicAnalysisDTO>> getTopicAnalysis(String clusterName,\n                                                                 String topicName,\n                                                                 ServerWebExchange exchange) {\n\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .topic(topicName)\n        .topicActions(MESSAGES_READ)\n        .operationName(\"getTopicAnalysis\")\n        .build();\n\n    return validateAccess(context)\n        .thenReturn(topicAnalysisService.getTopicAnalysis(getCluster(clusterName), topicName)\n            .map(ResponseEntity::ok)\n            .orElseGet(() -> ResponseEntity.notFound().build()))\n        .doOnEach(sig -> audit(context, sig));\n  }\n\n  @Override\n  public Mono<ResponseEntity<Flux<TopicProducerStateDTO>>> getActiveProducerStates(String clusterName,\n                                                                                   String topicName,\n                                                                                   ServerWebExchange exchange) {\n    var context = AccessContext.builder()\n        .cluster(clusterName)\n        .topic(topicName)\n        .topicActions(VIEW)\n        .operationName(\"getActiveProducerStates\")\n        .build();\n\n    Comparator<TopicProducerStateDTO> ordering =\n        Comparator.comparingInt(TopicProducerStateDTO::getPartition)\n            .thenComparing(Comparator.comparing(TopicProducerStateDTO::getProducerId).reversed());\n\n    Flux<TopicProducerStateDTO> states = topicsService.getActiveProducersState(getCluster(clusterName), topicName)\n        .flatMapMany(statesMap ->\n            Flux.fromStream(\n                statesMap.entrySet().stream()\n                    .flatMap(e -> e.getValue().stream().map(p -> clusterMapper.map(e.getKey().partition(), p)))\n                    .sorted(ordering)));\n\n    return validateAccess(context)\n        .thenReturn(states)\n        .map(ResponseEntity::ok)\n        .doOnEach(sig -> audit(context, sig));\n  }\n\n  private Comparator<InternalTopic> getComparatorForTopic(\n      TopicColumnsToSortDTO orderBy) {\n    var defaultComparator = Comparator.comparing(InternalTopic::getName);\n    if (orderBy == null) {\n      return defaultComparator;\n    }\n    switch (orderBy) {\n      case TOTAL_PARTITIONS:\n        return Comparator.comparing(InternalTopic::getPartitionCount);\n      case OUT_OF_SYNC_REPLICAS:\n        return Comparator.comparing(t -> t.getReplicas() - t.getInSyncReplicas());\n      case REPLICATION_FACTOR:\n        return Comparator.comparing(InternalTopic::getReplicationFactor);\n      case SIZE:\n        return Comparator.comparing(InternalTopic::getSegmentSize);\n      case NAME:\n      default:\n        return defaultComparator;\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/AbstractEmitter.java",
    "content": "package com.provectus.kafka.ui.emitter;\n\nimport com.provectus.kafka.ui.model.TopicMessageEventDTO;\nimport org.apache.kafka.clients.consumer.ConsumerRecord;\nimport org.apache.kafka.common.utils.Bytes;\nimport reactor.core.publisher.FluxSink;\n\nabstract class AbstractEmitter implements java.util.function.Consumer<FluxSink<TopicMessageEventDTO>> {\n\n  private final MessagesProcessing messagesProcessing;\n  private final PollingSettings pollingSettings;\n\n  protected AbstractEmitter(MessagesProcessing messagesProcessing, PollingSettings pollingSettings) {\n    this.messagesProcessing = messagesProcessing;\n    this.pollingSettings = pollingSettings;\n  }\n\n  protected PolledRecords poll(FluxSink<TopicMessageEventDTO> sink, EnhancedConsumer consumer) {\n    var records = consumer.pollEnhanced(pollingSettings.getPollTimeout());\n    sendConsuming(sink, records);\n    return records;\n  }\n\n  protected boolean sendLimitReached() {\n    return messagesProcessing.limitReached();\n  }\n\n  protected void send(FluxSink<TopicMessageEventDTO> sink, Iterable<ConsumerRecord<Bytes, Bytes>> records) {\n    messagesProcessing.send(sink, records);\n  }\n\n  protected void sendPhase(FluxSink<TopicMessageEventDTO> sink, String name) {\n    messagesProcessing.sendPhase(sink, name);\n  }\n\n  protected void sendConsuming(FluxSink<TopicMessageEventDTO> sink, PolledRecords records) {\n    messagesProcessing.sentConsumingInfo(sink, records);\n  }\n\n  protected void sendFinishStatsAndCompleteSink(FluxSink<TopicMessageEventDTO> sink) {\n    messagesProcessing.sendFinishEvent(sink);\n    sink.complete();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/BackwardEmitter.java",
    "content": "package com.provectus.kafka.ui.emitter;\n\nimport com.provectus.kafka.ui.model.ConsumerPosition;\nimport com.provectus.kafka.ui.model.TopicMessageDTO;\nimport com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer;\nimport java.util.Comparator;\nimport java.util.Map;\nimport java.util.TreeMap;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\nimport org.apache.kafka.common.TopicPartition;\n\npublic class BackwardEmitter extends RangePollingEmitter {\n\n  public BackwardEmitter(Supplier<EnhancedConsumer> consumerSupplier,\n                         ConsumerPosition consumerPosition,\n                         int messagesPerPage,\n                         ConsumerRecordDeserializer deserializer,\n                         Predicate<TopicMessageDTO> filter,\n                         PollingSettings pollingSettings) {\n    super(\n        consumerSupplier,\n        consumerPosition,\n        messagesPerPage,\n        new MessagesProcessing(\n            deserializer,\n            filter,\n            false,\n            messagesPerPage\n        ),\n        pollingSettings\n    );\n  }\n\n  @Override\n  protected TreeMap<TopicPartition, FromToOffset> nextPollingRange(TreeMap<TopicPartition, FromToOffset> prevRange,\n                                                                   SeekOperations seekOperations) {\n    TreeMap<TopicPartition, Long> readToOffsets = new TreeMap<>(Comparator.comparingInt(TopicPartition::partition));\n    if (prevRange.isEmpty()) {\n      readToOffsets.putAll(seekOperations.getOffsetsForSeek());\n    } else {\n      readToOffsets.putAll(\n          prevRange.entrySet()\n              .stream()\n              .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().from()))\n      );\n    }\n\n    int msgsToPollPerPartition = (int) Math.ceil((double) messagesPerPage / readToOffsets.size());\n    TreeMap<TopicPartition, FromToOffset> result = new TreeMap<>(Comparator.comparingInt(TopicPartition::partition));\n    readToOffsets.forEach((tp, toOffset) -> {\n      long tpStartOffset = seekOperations.getBeginOffsets().get(tp);\n      if (toOffset > tpStartOffset) {\n        result.put(tp, new FromToOffset(Math.max(tpStartOffset, toOffset - msgsToPollPerPartition), toOffset));\n      }\n    });\n    return result;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ConsumingStats.java",
    "content": "package com.provectus.kafka.ui.emitter;\n\nimport com.provectus.kafka.ui.model.TopicMessageConsumingDTO;\nimport com.provectus.kafka.ui.model.TopicMessageEventDTO;\nimport reactor.core.publisher.FluxSink;\n\nclass ConsumingStats {\n\n  private long bytes = 0;\n  private int records = 0;\n  private long elapsed = 0;\n  private int filterApplyErrors = 0;\n\n  void sendConsumingEvt(FluxSink<TopicMessageEventDTO> sink, PolledRecords polledRecords) {\n    bytes += polledRecords.bytes();\n    records += polledRecords.count();\n    elapsed += polledRecords.elapsed().toMillis();\n    sink.next(\n        new TopicMessageEventDTO()\n            .type(TopicMessageEventDTO.TypeEnum.CONSUMING)\n            .consuming(createConsumingStats())\n    );\n  }\n\n  void incFilterApplyError() {\n    filterApplyErrors++;\n  }\n\n  void sendFinishEvent(FluxSink<TopicMessageEventDTO> sink) {\n    sink.next(\n        new TopicMessageEventDTO()\n            .type(TopicMessageEventDTO.TypeEnum.DONE)\n            .consuming(createConsumingStats())\n    );\n  }\n\n  private TopicMessageConsumingDTO createConsumingStats() {\n    return new TopicMessageConsumingDTO()\n        .bytesConsumed(bytes)\n        .elapsedMs(elapsed)\n        .isCancelled(false)\n        .filterApplyErrors(filterApplyErrors)\n        .messagesConsumed(records);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/EnhancedConsumer.java",
    "content": "package com.provectus.kafka.ui.emitter;\n\nimport com.google.common.base.Preconditions;\nimport com.google.common.base.Stopwatch;\nimport com.provectus.kafka.ui.util.ApplicationMetrics;\nimport java.time.Duration;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Properties;\nimport java.util.Set;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\nimport lombok.RequiredArgsConstructor;\nimport lombok.experimental.Delegate;\nimport org.apache.kafka.clients.consumer.Consumer;\nimport org.apache.kafka.clients.consumer.ConsumerRebalanceListener;\nimport org.apache.kafka.clients.consumer.ConsumerRecords;\nimport org.apache.kafka.clients.consumer.KafkaConsumer;\nimport org.apache.kafka.common.TopicPartition;\nimport org.apache.kafka.common.serialization.BytesDeserializer;\nimport org.apache.kafka.common.utils.Bytes;\n\n\npublic class EnhancedConsumer extends KafkaConsumer<Bytes, Bytes> {\n\n  private final PollingThrottler throttler;\n  private final ApplicationMetrics metrics;\n  private String pollingTopic;\n\n  public EnhancedConsumer(Properties properties,\n                          PollingThrottler throttler,\n                          ApplicationMetrics metrics) {\n    super(properties, new BytesDeserializer(), new BytesDeserializer());\n    this.throttler = throttler;\n    this.metrics = metrics;\n    metrics.activeConsumers().incrementAndGet();\n  }\n\n  public PolledRecords pollEnhanced(Duration dur) {\n    var stopwatch = Stopwatch.createStarted();\n    ConsumerRecords<Bytes, Bytes> polled = poll(dur);\n    PolledRecords polledEnhanced = PolledRecords.create(polled, stopwatch.elapsed());\n    var throttled = throttler.throttleAfterPoll(polledEnhanced.bytes());\n    metrics.meterPolledRecords(pollingTopic, polledEnhanced, throttled);\n    return polledEnhanced;\n  }\n\n  @Override\n  public void assign(Collection<TopicPartition> partitions) {\n    super.assign(partitions);\n    Set<String> assignedTopics = partitions.stream().map(TopicPartition::topic).collect(Collectors.toSet());\n    Preconditions.checkState(assignedTopics.size() == 1);\n    this.pollingTopic = assignedTopics.iterator().next();\n  }\n\n  @Override\n  public void subscribe(Pattern pattern) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void subscribe(Collection<String> topics) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void subscribe(Pattern pattern, ConsumerRebalanceListener listener) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void subscribe(Collection<String> topics, ConsumerRebalanceListener listener) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void close(Duration timeout) {\n    metrics.activeConsumers().decrementAndGet();\n    super.close(timeout);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ForwardEmitter.java",
    "content": "package com.provectus.kafka.ui.emitter;\n\nimport com.provectus.kafka.ui.model.ConsumerPosition;\nimport com.provectus.kafka.ui.model.TopicMessageDTO;\nimport com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer;\nimport java.util.Comparator;\nimport java.util.Map;\nimport java.util.TreeMap;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\nimport org.apache.kafka.common.TopicPartition;\n\npublic class ForwardEmitter extends RangePollingEmitter {\n\n  public ForwardEmitter(Supplier<EnhancedConsumer> consumerSupplier,\n                        ConsumerPosition consumerPosition,\n                        int messagesPerPage,\n                        ConsumerRecordDeserializer deserializer,\n                        Predicate<TopicMessageDTO> filter,\n                        PollingSettings pollingSettings) {\n    super(\n        consumerSupplier,\n        consumerPosition,\n        messagesPerPage,\n        new MessagesProcessing(\n            deserializer,\n            filter,\n            true,\n            messagesPerPage\n        ),\n        pollingSettings\n    );\n  }\n\n  @Override\n  protected TreeMap<TopicPartition, FromToOffset> nextPollingRange(TreeMap<TopicPartition, FromToOffset> prevRange,\n                                                                   SeekOperations seekOperations) {\n    TreeMap<TopicPartition, Long> readFromOffsets = new TreeMap<>(Comparator.comparingInt(TopicPartition::partition));\n    if (prevRange.isEmpty()) {\n      readFromOffsets.putAll(seekOperations.getOffsetsForSeek());\n    } else {\n      readFromOffsets.putAll(\n          prevRange.entrySet()\n              .stream()\n              .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().to()))\n      );\n    }\n\n    int msgsToPollPerPartition = (int) Math.ceil((double) messagesPerPage / readFromOffsets.size());\n    TreeMap<TopicPartition, FromToOffset> result = new TreeMap<>(Comparator.comparingInt(TopicPartition::partition));\n    readFromOffsets.forEach((tp, fromOffset) -> {\n      long tpEndOffset = seekOperations.getEndOffsets().get(tp);\n      if (fromOffset < tpEndOffset) {\n        result.put(tp, new FromToOffset(fromOffset, Math.min(tpEndOffset, fromOffset + msgsToPollPerPartition)));\n      }\n    });\n    return result;\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/MessageFilters.java",
    "content": "package com.provectus.kafka.ui.emitter;\n\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.model.MessageFilterTypeDTO;\nimport com.provectus.kafka.ui.model.TopicMessageDTO;\nimport groovy.json.JsonSlurper;\nimport java.util.function.Predicate;\nimport javax.annotation.Nullable;\nimport javax.script.CompiledScript;\nimport javax.script.ScriptEngineManager;\nimport javax.script.ScriptException;\nimport lombok.SneakyThrows;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.codehaus.groovy.jsr223.GroovyScriptEngineImpl;\n\n@Slf4j\npublic class MessageFilters {\n\n  private static GroovyScriptEngineImpl GROOVY_ENGINE;\n\n  private MessageFilters() {\n  }\n\n  public static Predicate<TopicMessageDTO> createMsgFilter(String query, MessageFilterTypeDTO type) {\n    switch (type) {\n      case STRING_CONTAINS:\n        return containsStringFilter(query);\n      case GROOVY_SCRIPT:\n        return groovyScriptFilter(query);\n      default:\n        throw new IllegalStateException(\"Unknown query type: \" + type);\n    }\n  }\n\n  static Predicate<TopicMessageDTO> containsStringFilter(String string) {\n    return msg -> StringUtils.contains(msg.getKey(), string)\n        || StringUtils.contains(msg.getContent(), string);\n  }\n\n  static Predicate<TopicMessageDTO> groovyScriptFilter(String script) {\n    var engine = getGroovyEngine();\n    var compiledScript = compileScript(engine, script);\n    var jsonSlurper = new JsonSlurper();\n    return new Predicate<TopicMessageDTO>() {\n      @SneakyThrows\n      @Override\n      public boolean test(TopicMessageDTO msg) {\n        var bindings = engine.createBindings();\n        bindings.put(\"partition\", msg.getPartition());\n        bindings.put(\"offset\", msg.getOffset());\n        bindings.put(\"timestampMs\", msg.getTimestamp().toInstant().toEpochMilli());\n        bindings.put(\"keyAsText\", msg.getKey());\n        bindings.put(\"valueAsText\", msg.getContent());\n        bindings.put(\"headers\", msg.getHeaders());\n        bindings.put(\"key\", parseToJsonOrReturnAsIs(jsonSlurper, msg.getKey()));\n        bindings.put(\"value\", parseToJsonOrReturnAsIs(jsonSlurper, msg.getContent()));\n        var result = compiledScript.eval(bindings);\n        if (result instanceof Boolean) {\n          return (Boolean) result;\n        } else {\n          throw new ValidationException(\n              \"Unexpected script result: %s, Boolean should be returned instead\".formatted(result));\n        }\n      }\n    };\n  }\n\n  @Nullable\n  private static Object parseToJsonOrReturnAsIs(JsonSlurper parser, @Nullable String str) {\n    if (str == null) {\n      return null;\n    }\n    try {\n      return parser.parseText(str);\n    } catch (Exception e) {\n      return str;\n    }\n  }\n\n  private static synchronized GroovyScriptEngineImpl getGroovyEngine() {\n    // it is pretty heavy object, so initializing it on-demand\n    if (GROOVY_ENGINE == null) {\n      GROOVY_ENGINE = (GroovyScriptEngineImpl)\n          new ScriptEngineManager().getEngineByName(\"groovy\");\n    }\n    return GROOVY_ENGINE;\n  }\n\n  private static CompiledScript compileScript(GroovyScriptEngineImpl engine, String script) {\n    try {\n      return engine.compile(script);\n    } catch (ScriptException e) {\n      throw new ValidationException(\"Script syntax error: \" + e.getMessage());\n    }\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/MessagesProcessing.java",
    "content": "package com.provectus.kafka.ui.emitter;\n\nimport static java.util.stream.Collectors.collectingAndThen;\nimport static java.util.stream.Collectors.groupingBy;\nimport static java.util.stream.Collectors.toList;\n\nimport com.google.common.annotations.VisibleForTesting;\nimport com.google.common.collect.Iterables;\nimport com.google.common.collect.Streams;\nimport com.provectus.kafka.ui.model.TopicMessageDTO;\nimport com.provectus.kafka.ui.model.TopicMessageEventDTO;\nimport com.provectus.kafka.ui.model.TopicMessagePhaseDTO;\nimport com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.TreeMap;\nimport java.util.function.Predicate;\nimport javax.annotation.Nullable;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.kafka.clients.consumer.ConsumerRecord;\nimport org.apache.kafka.common.utils.Bytes;\nimport reactor.core.publisher.FluxSink;\n\n@Slf4j\n@RequiredArgsConstructor\nclass MessagesProcessing {\n\n  private final ConsumingStats consumingStats = new ConsumingStats();\n  private long sentMessages = 0;\n\n  private final ConsumerRecordDeserializer deserializer;\n  private final Predicate<TopicMessageDTO> filter;\n  private final boolean ascendingSortBeforeSend;\n  private final @Nullable Integer limit;\n\n  boolean limitReached() {\n    return limit != null && sentMessages >= limit;\n  }\n\n  void send(FluxSink<TopicMessageEventDTO> sink, Iterable<ConsumerRecord<Bytes, Bytes>> polled) {\n    sortForSending(polled, ascendingSortBeforeSend)\n        .forEach(rec -> {\n          if (!limitReached() && !sink.isCancelled()) {\n            TopicMessageDTO topicMessage = deserializer.deserialize(rec);\n            try {\n              if (filter.test(topicMessage)) {\n                sink.next(\n                    new TopicMessageEventDTO()\n                        .type(TopicMessageEventDTO.TypeEnum.MESSAGE)\n                        .message(topicMessage)\n                );\n                sentMessages++;\n              }\n            } catch (Exception e) {\n              consumingStats.incFilterApplyError();\n              log.trace(\"Error applying filter for message {}\", topicMessage);\n            }\n          }\n        });\n  }\n\n  void sentConsumingInfo(FluxSink<TopicMessageEventDTO> sink, PolledRecords polledRecords) {\n    if (!sink.isCancelled()) {\n      consumingStats.sendConsumingEvt(sink, polledRecords);\n    }\n  }\n\n  void sendFinishEvent(FluxSink<TopicMessageEventDTO> sink) {\n    if (!sink.isCancelled()) {\n      consumingStats.sendFinishEvent(sink);\n    }\n  }\n\n  void sendPhase(FluxSink<TopicMessageEventDTO> sink, String name) {\n    if (!sink.isCancelled()) {\n      sink.next(\n          new TopicMessageEventDTO()\n              .type(TopicMessageEventDTO.TypeEnum.PHASE)\n              .phase(new TopicMessagePhaseDTO().name(name))\n      );\n    }\n  }\n\n  /*\n   * Sorting by timestamps, BUT requesting that records within same partitions should be ordered by offsets.\n   */\n  @VisibleForTesting\n  static Iterable<ConsumerRecord<Bytes, Bytes>> sortForSending(Iterable<ConsumerRecord<Bytes, Bytes>> records,\n                                                               boolean asc) {\n    Comparator<ConsumerRecord> offsetComparator = asc\n        ? Comparator.comparingLong(ConsumerRecord::offset)\n        : Comparator.<ConsumerRecord>comparingLong(ConsumerRecord::offset).reversed();\n\n    // partition -> sorted by offsets records\n    Map<Integer, List<ConsumerRecord<Bytes, Bytes>>> perPartition = Streams.stream(records)\n        .collect(\n            groupingBy(\n                ConsumerRecord::partition,\n                TreeMap::new,\n                collectingAndThen(toList(), lst -> lst.stream().sorted(offsetComparator).toList())));\n\n    Comparator<ConsumerRecord> tsComparator = asc\n        ? Comparator.comparing(ConsumerRecord::timestamp)\n        : Comparator.<ConsumerRecord>comparingLong(ConsumerRecord::timestamp).reversed();\n\n    // merge-sorting records from partitions one by one using timestamp comparator\n    return Iterables.mergeSorted(perPartition.values(), tsComparator);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/OffsetsInfo.java",
    "content": "package com.provectus.kafka.ui.emitter;\n\nimport com.google.common.base.Preconditions;\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Set;\nimport lombok.Getter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.mutable.MutableLong;\nimport org.apache.kafka.clients.consumer.Consumer;\nimport org.apache.kafka.common.TopicPartition;\n\n@Slf4j\n@Getter\nclass OffsetsInfo {\n\n  private final Consumer<?, ?> consumer;\n\n  private final Map<TopicPartition, Long> beginOffsets;\n  private final Map<TopicPartition, Long> endOffsets;\n\n  private final Set<TopicPartition> nonEmptyPartitions = new HashSet<>();\n  private final Set<TopicPartition> emptyPartitions = new HashSet<>();\n\n  OffsetsInfo(Consumer<?, ?> consumer, String topic) {\n    this(consumer,\n        consumer.partitionsFor(topic).stream()\n            .map(pi -> new TopicPartition(topic, pi.partition()))\n            .toList()\n    );\n  }\n\n  OffsetsInfo(Consumer<?, ?> consumer, Collection<TopicPartition> targetPartitions) {\n    this.consumer = consumer;\n    this.beginOffsets = consumer.beginningOffsets(targetPartitions);\n    this.endOffsets = consumer.endOffsets(targetPartitions);\n    endOffsets.forEach((tp, endOffset) -> {\n      var beginningOffset = beginOffsets.get(tp);\n      if (endOffset > beginningOffset) {\n        nonEmptyPartitions.add(tp);\n      } else {\n        emptyPartitions.add(tp);\n      }\n    });\n  }\n\n  boolean assignedPartitionsFullyPolled() {\n    for (var tp : consumer.assignment()) {\n      Preconditions.checkArgument(endOffsets.containsKey(tp));\n      if (endOffsets.get(tp) > consumer.position(tp)) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  long summaryOffsetsRange() {\n    MutableLong cnt = new MutableLong();\n    nonEmptyPartitions.forEach(tp -> cnt.add(endOffsets.get(tp) - beginOffsets.get(tp)));\n    return cnt.getValue();\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/PolledRecords.java",
    "content": "package com.provectus.kafka.ui.emitter;\n\nimport java.time.Duration;\nimport java.util.Iterator;\nimport java.util.List;\nimport org.apache.kafka.clients.consumer.ConsumerRecord;\nimport org.apache.kafka.clients.consumer.ConsumerRecords;\nimport org.apache.kafka.common.TopicPartition;\nimport org.apache.kafka.common.header.Header;\nimport org.apache.kafka.common.utils.Bytes;\n\npublic record PolledRecords(int count,\n                            int bytes,\n                            Duration elapsed,\n                            ConsumerRecords<Bytes, Bytes> records) implements Iterable<ConsumerRecord<Bytes, Bytes>> {\n\n  static PolledRecords create(ConsumerRecords<Bytes, Bytes> polled, Duration pollDuration) {\n    return new PolledRecords(\n        polled.count(),\n        calculatePolledRecSize(polled),\n        pollDuration,\n        polled\n    );\n  }\n\n  public List<ConsumerRecord<Bytes, Bytes>> records(TopicPartition tp) {\n    return records.records(tp);\n  }\n\n  @Override\n  public Iterator<ConsumerRecord<Bytes, Bytes>> iterator() {\n    return records.iterator();\n  }\n\n  private static int calculatePolledRecSize(Iterable<ConsumerRecord<Bytes, Bytes>> recs) {\n    int polledBytes = 0;\n    for (ConsumerRecord<Bytes, Bytes> rec : recs) {\n      for (Header header : rec.headers()) {\n        polledBytes +=\n            (header.key() != null ? header.key().getBytes().length : 0)\n                + (header.value() != null ? header.value().length : 0);\n      }\n      polledBytes += rec.key() == null ? 0 : rec.serializedKeySize();\n      polledBytes += rec.value() == null ? 0 : rec.serializedValueSize();\n    }\n    return polledBytes;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/PollingSettings.java",
    "content": "package com.provectus.kafka.ui.emitter;\n\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport java.time.Duration;\nimport java.util.Optional;\nimport java.util.function.Supplier;\n\npublic class PollingSettings {\n\n  private static final Duration DEFAULT_POLL_TIMEOUT = Duration.ofMillis(1_000);\n\n  private final Duration pollTimeout;\n  private final Supplier<PollingThrottler> throttlerSupplier;\n\n  public static PollingSettings create(ClustersProperties.Cluster cluster,\n                                       ClustersProperties clustersProperties) {\n    var pollingProps = Optional.ofNullable(clustersProperties.getPolling())\n        .orElseGet(ClustersProperties.PollingProperties::new);\n\n    var pollTimeout = pollingProps.getPollTimeoutMs() != null\n        ? Duration.ofMillis(pollingProps.getPollTimeoutMs())\n        : DEFAULT_POLL_TIMEOUT;\n\n    return new PollingSettings(\n        pollTimeout,\n        PollingThrottler.throttlerSupplier(cluster)\n    );\n  }\n\n  public static PollingSettings createDefault() {\n    return new PollingSettings(\n        DEFAULT_POLL_TIMEOUT,\n        PollingThrottler::noop\n    );\n  }\n\n  private PollingSettings(Duration pollTimeout,\n                          Supplier<PollingThrottler> throttlerSupplier) {\n    this.pollTimeout = pollTimeout;\n    this.throttlerSupplier = throttlerSupplier;\n  }\n\n  public Duration getPollTimeout() {\n    return pollTimeout;\n  }\n\n  public PollingThrottler getPollingThrottler() {\n    return throttlerSupplier.get();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/PollingThrottler.java",
    "content": "package com.provectus.kafka.ui.emitter;\n\nimport com.google.common.annotations.VisibleForTesting;\nimport com.google.common.util.concurrent.RateLimiter;\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport java.util.function.Supplier;\nimport lombok.extern.slf4j.Slf4j;\n\n@Slf4j\npublic class PollingThrottler {\n\n  public static Supplier<PollingThrottler> throttlerSupplier(ClustersProperties.Cluster cluster) {\n    Long rate = cluster.getPollingThrottleRate();\n    if (rate == null || rate <= 0) {\n      return PollingThrottler::noop;\n    }\n    // RateLimiter instance should be shared across all created throttlers\n    var rateLimiter = RateLimiter.create(rate);\n    return () -> new PollingThrottler(cluster.getName(), rateLimiter);\n  }\n\n  private final String clusterName;\n  private final RateLimiter rateLimiter;\n  private boolean throttled;\n\n  @VisibleForTesting\n  public PollingThrottler(String clusterName, RateLimiter rateLimiter) {\n    this.clusterName = clusterName;\n    this.rateLimiter = rateLimiter;\n  }\n\n  public static PollingThrottler noop() {\n    return new PollingThrottler(\"noop\", RateLimiter.create(Long.MAX_VALUE));\n  }\n\n  //returns true if polling was throttled\n  public boolean throttleAfterPoll(int polledBytes) {\n    if (polledBytes > 0) {\n      double sleptSeconds = rateLimiter.acquire(polledBytes);\n      if (!throttled && sleptSeconds > 0.0) {\n        throttled = true;\n        log.debug(\"Polling throttling enabled for cluster {} at rate {} bytes/sec\", clusterName, rateLimiter.getRate());\n        return true;\n      }\n    }\n    return false;\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/RangePollingEmitter.java",
    "content": "package com.provectus.kafka.ui.emitter;\n\nimport com.provectus.kafka.ui.model.ConsumerPosition;\nimport com.provectus.kafka.ui.model.TopicMessageEventDTO;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.TreeMap;\nimport java.util.function.Supplier;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.kafka.clients.consumer.ConsumerRecord;\nimport org.apache.kafka.common.TopicPartition;\nimport org.apache.kafka.common.errors.InterruptException;\nimport org.apache.kafka.common.utils.Bytes;\nimport reactor.core.publisher.FluxSink;\n\n@Slf4j\nabstract class RangePollingEmitter extends AbstractEmitter {\n\n  private final Supplier<EnhancedConsumer> consumerSupplier;\n  protected final ConsumerPosition consumerPosition;\n  protected final int messagesPerPage;\n\n  protected RangePollingEmitter(Supplier<EnhancedConsumer> consumerSupplier,\n                                ConsumerPosition consumerPosition,\n                                int messagesPerPage,\n                                MessagesProcessing messagesProcessing,\n                                PollingSettings pollingSettings) {\n    super(messagesProcessing, pollingSettings);\n    this.consumerPosition = consumerPosition;\n    this.messagesPerPage = messagesPerPage;\n    this.consumerSupplier = consumerSupplier;\n  }\n\n  protected record FromToOffset(/*inclusive*/ long from, /*exclusive*/ long to) {\n  }\n\n  //should return empty map if polling should be stopped\n  protected abstract TreeMap<TopicPartition, FromToOffset> nextPollingRange(\n      TreeMap<TopicPartition, FromToOffset> prevRange, //empty on start\n      SeekOperations seekOperations\n  );\n\n  @Override\n  public void accept(FluxSink<TopicMessageEventDTO> sink) {\n    log.debug(\"Starting polling for {}\", consumerPosition);\n    try (EnhancedConsumer consumer = consumerSupplier.get()) {\n      sendPhase(sink, \"Consumer created\");\n      var seekOperations = SeekOperations.create(consumer, consumerPosition);\n      TreeMap<TopicPartition, FromToOffset> pollRange = nextPollingRange(new TreeMap<>(), seekOperations);\n      log.debug(\"Starting from offsets {}\", pollRange);\n\n      while (!sink.isCancelled() && !pollRange.isEmpty() && !sendLimitReached()) {\n        var polled = poll(consumer, sink, pollRange);\n        send(sink, polled);\n        pollRange = nextPollingRange(pollRange, seekOperations);\n      }\n      if (sink.isCancelled()) {\n        log.debug(\"Polling finished due to sink cancellation\");\n      }\n      sendFinishStatsAndCompleteSink(sink);\n      log.debug(\"Polling finished\");\n    } catch (InterruptException kafkaInterruptException) {\n      log.debug(\"Polling finished due to thread interruption\");\n      sink.complete();\n    } catch (Exception e) {\n      log.error(\"Error occurred while consuming records\", e);\n      sink.error(e);\n    }\n  }\n\n  private List<ConsumerRecord<Bytes, Bytes>> poll(EnhancedConsumer consumer,\n                                                  FluxSink<TopicMessageEventDTO> sink,\n                                                  TreeMap<TopicPartition, FromToOffset> range) {\n    log.trace(\"Polling range {}\", range);\n    sendPhase(sink,\n        \"Polling partitions: %s\".formatted(range.keySet().stream().map(TopicPartition::partition).sorted().toList()));\n\n    consumer.assign(range.keySet());\n    range.forEach((tp, fromTo) -> consumer.seek(tp, fromTo.from));\n\n    List<ConsumerRecord<Bytes, Bytes>> result = new ArrayList<>();\n    while (!sink.isCancelled() && consumer.paused().size() < range.size()) {\n      var polledRecords = poll(sink, consumer);\n      range.forEach((tp, fromTo) -> {\n        polledRecords.records(tp).stream()\n            .filter(r -> r.offset() < fromTo.to)\n            .forEach(result::add);\n\n        //next position is out of target range -> pausing partition\n        if (consumer.position(tp) >= fromTo.to) {\n          consumer.pause(List.of(tp));\n        }\n      });\n    }\n    consumer.resume(consumer.paused());\n    return result;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/ResultSizeLimiter.java",
    "content": "package com.provectus.kafka.ui.emitter;\n\nimport com.provectus.kafka.ui.model.TopicMessageEventDTO;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.function.Predicate;\n\npublic class ResultSizeLimiter implements Predicate<TopicMessageEventDTO> {\n  private final AtomicInteger processed = new AtomicInteger();\n  private final int limit;\n\n  public ResultSizeLimiter(int limit) {\n    this.limit = limit;\n  }\n\n  @Override\n  public boolean test(TopicMessageEventDTO event) {\n    if (event.getType().equals(TopicMessageEventDTO.TypeEnum.MESSAGE)) {\n      final int i = processed.incrementAndGet();\n      return i <= limit;\n    }\n    return true;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/SeekOperations.java",
    "content": "package com.provectus.kafka.ui.emitter;\n\nimport com.google.common.annotations.VisibleForTesting;\nimport com.google.common.base.Preconditions;\nimport com.provectus.kafka.ui.model.ConsumerPosition;\nimport com.provectus.kafka.ui.model.SeekTypeDTO;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport javax.annotation.Nullable;\nimport lombok.AccessLevel;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.mutable.MutableLong;\nimport org.apache.kafka.clients.consumer.Consumer;\nimport org.apache.kafka.common.TopicPartition;\n\n@RequiredArgsConstructor(access = AccessLevel.PACKAGE)\npublic class SeekOperations {\n\n  private final Consumer<?, ?> consumer;\n  private final OffsetsInfo offsetsInfo;\n  private final Map<TopicPartition, Long> offsetsForSeek; //only contains non-empty partitions!\n\n  public static SeekOperations create(Consumer<?, ?> consumer, ConsumerPosition consumerPosition) {\n    OffsetsInfo offsetsInfo;\n    if (consumerPosition.getSeekTo() == null) {\n      offsetsInfo = new OffsetsInfo(consumer, consumerPosition.getTopic());\n    } else {\n      offsetsInfo = new OffsetsInfo(consumer, consumerPosition.getSeekTo().keySet());\n    }\n    return new SeekOperations(\n        consumer,\n        offsetsInfo,\n        getOffsetsForSeek(consumer, offsetsInfo, consumerPosition.getSeekType(), consumerPosition.getSeekTo())\n    );\n  }\n\n  public void assignAndSeekNonEmptyPartitions() {\n    consumer.assign(offsetsForSeek.keySet());\n    offsetsForSeek.forEach(consumer::seek);\n  }\n\n  public Map<TopicPartition, Long> getBeginOffsets() {\n    return offsetsInfo.getBeginOffsets();\n  }\n\n  public Map<TopicPartition, Long> getEndOffsets() {\n    return offsetsInfo.getEndOffsets();\n  }\n\n  public boolean assignedPartitionsFullyPolled() {\n    return offsetsInfo.assignedPartitionsFullyPolled();\n  }\n\n  // sum of (end - start) offsets for all partitions\n  public long summaryOffsetsRange() {\n    return offsetsInfo.summaryOffsetsRange();\n  }\n\n  // sum of differences between initial consumer seek and current consumer position (across all partitions)\n  public long offsetsProcessedFromSeek() {\n    MutableLong count = new MutableLong();\n    offsetsForSeek.forEach((tp, initialOffset) -> count.add(consumer.position(tp) - initialOffset));\n    return count.getValue();\n  }\n\n  // Get offsets to seek to. NOTE: offsets do not contain empty partitions offsets\n  public Map<TopicPartition, Long> getOffsetsForSeek() {\n    return offsetsForSeek;\n  }\n\n  /**\n   * Finds offsets for ConsumerPosition. Note: will return empty map if no offsets found for desired criteria.\n   */\n  @VisibleForTesting\n  static Map<TopicPartition, Long> getOffsetsForSeek(Consumer<?, ?> consumer,\n                                                     OffsetsInfo offsetsInfo,\n                                                     SeekTypeDTO seekType,\n                                                     @Nullable Map<TopicPartition, Long> seekTo) {\n    switch (seekType) {\n      case LATEST:\n        return consumer.endOffsets(offsetsInfo.getNonEmptyPartitions());\n      case BEGINNING:\n        return consumer.beginningOffsets(offsetsInfo.getNonEmptyPartitions());\n      case OFFSET:\n        Preconditions.checkNotNull(seekTo);\n        return fixOffsets(offsetsInfo, seekTo);\n      case TIMESTAMP:\n        Preconditions.checkNotNull(seekTo);\n        return offsetsForTimestamp(consumer, offsetsInfo, seekTo);\n      default:\n        throw new IllegalStateException();\n    }\n  }\n\n  private static Map<TopicPartition, Long> fixOffsets(OffsetsInfo offsetsInfo, Map<TopicPartition, Long> offsets) {\n    offsets = new HashMap<>(offsets);\n    offsets.keySet().retainAll(offsetsInfo.getNonEmptyPartitions());\n\n    Map<TopicPartition, Long> result = new HashMap<>();\n    offsets.forEach((tp, targetOffset) -> {\n      long endOffset = offsetsInfo.getEndOffsets().get(tp);\n      long beginningOffset = offsetsInfo.getBeginOffsets().get(tp);\n      // fixing offsets with min - max bounds\n      if (targetOffset > endOffset) {\n        targetOffset = endOffset;\n      } else if (targetOffset < beginningOffset) {\n        targetOffset = beginningOffset;\n      }\n      result.put(tp, targetOffset);\n    });\n    return result;\n  }\n\n  private static Map<TopicPartition, Long> offsetsForTimestamp(Consumer<?, ?> consumer, OffsetsInfo offsetsInfo,\n                                                               Map<TopicPartition, Long> timestamps) {\n    timestamps = new HashMap<>(timestamps);\n    timestamps.keySet().retainAll(offsetsInfo.getNonEmptyPartitions());\n\n    return consumer.offsetsForTimes(timestamps).entrySet().stream()\n        .filter(e -> e.getValue() != null)\n        .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().offset()));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/TailingEmitter.java",
    "content": "package com.provectus.kafka.ui.emitter;\n\nimport com.provectus.kafka.ui.model.ConsumerPosition;\nimport com.provectus.kafka.ui.model.TopicMessageDTO;\nimport com.provectus.kafka.ui.model.TopicMessageEventDTO;\nimport com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer;\nimport java.util.HashMap;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.kafka.common.errors.InterruptException;\nimport reactor.core.publisher.FluxSink;\n\n@Slf4j\npublic class TailingEmitter extends AbstractEmitter {\n\n  private final Supplier<EnhancedConsumer> consumerSupplier;\n  private final ConsumerPosition consumerPosition;\n\n  public TailingEmitter(Supplier<EnhancedConsumer> consumerSupplier,\n                        ConsumerPosition consumerPosition,\n                        ConsumerRecordDeserializer deserializer,\n                        Predicate<TopicMessageDTO> filter,\n                        PollingSettings pollingSettings) {\n    super(new MessagesProcessing(deserializer, filter, false, null), pollingSettings);\n    this.consumerSupplier = consumerSupplier;\n    this.consumerPosition = consumerPosition;\n  }\n\n  @Override\n  public void accept(FluxSink<TopicMessageEventDTO> sink) {\n    log.debug(\"Starting tailing polling for {}\", consumerPosition);\n    try (EnhancedConsumer consumer = consumerSupplier.get()) {\n      assignAndSeek(consumer);\n      while (!sink.isCancelled()) {\n        sendPhase(sink, \"Polling\");\n        var polled = poll(sink, consumer);\n        send(sink, polled);\n      }\n      sink.complete();\n      log.debug(\"Tailing finished\");\n    } catch (InterruptException kafkaInterruptException) {\n      log.debug(\"Tailing finished due to thread interruption\");\n      sink.complete();\n    } catch (Exception e) {\n      log.error(\"Error consuming {}\", consumerPosition, e);\n      sink.error(e);\n    }\n  }\n\n  private void assignAndSeek(EnhancedConsumer consumer) {\n    var seekOperations = SeekOperations.create(consumer, consumerPosition);\n    var seekOffsets = new HashMap<>(seekOperations.getEndOffsets()); // defaulting offsets to topic end\n    seekOffsets.putAll(seekOperations.getOffsetsForSeek()); // this will only set non-empty partitions\n    consumer.assign(seekOffsets.keySet());\n    seekOffsets.forEach(consumer::seek);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ClusterNotFoundException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\npublic class ClusterNotFoundException extends CustomBaseException {\n\n  public ClusterNotFoundException() {\n    super(\"Cluster not found\");\n  }\n\n  public ClusterNotFoundException(String message) {\n    super(message);\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.CLUSTER_NOT_FOUND;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ConnectNotFoundException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\npublic class ConnectNotFoundException extends CustomBaseException {\n\n  public ConnectNotFoundException() {\n    super(\"Connect not found\");\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.CONNECT_NOT_FOUND;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/CustomBaseException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\n\npublic abstract class CustomBaseException extends RuntimeException {\n  protected CustomBaseException() {\n    super();\n  }\n\n  protected CustomBaseException(String message) {\n    super(message);\n  }\n\n  protected CustomBaseException(String message, Throwable cause) {\n    super(message, cause);\n  }\n\n  protected CustomBaseException(Throwable cause) {\n    super(cause);\n  }\n\n  protected CustomBaseException(String message, Throwable cause, boolean enableSuppression,\n                                boolean writableStackTrace) {\n    super(message, cause, enableSuppression, writableStackTrace);\n  }\n\n  public abstract ErrorCode getErrorCode();\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/DuplicateEntityException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\npublic class DuplicateEntityException extends CustomBaseException {\n\n  public DuplicateEntityException(String message) {\n    super(message);\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.DUPLICATED_ENTITY;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ErrorCode.java",
    "content": "package com.provectus.kafka.ui.exception;\n\nimport java.util.HashSet;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.http.HttpStatus;\n\n\npublic enum ErrorCode {\n\n  FORBIDDEN(403, HttpStatus.FORBIDDEN),\n\n  UNEXPECTED(5000, HttpStatus.INTERNAL_SERVER_ERROR),\n  KSQL_API_ERROR(5001, HttpStatus.INTERNAL_SERVER_ERROR),\n  BINDING_FAIL(4001, HttpStatus.BAD_REQUEST),\n  NOT_FOUND(404, HttpStatus.NOT_FOUND),\n  VALIDATION_FAIL(4002, HttpStatus.BAD_REQUEST),\n  READ_ONLY_MODE_ENABLE(4003, HttpStatus.METHOD_NOT_ALLOWED),\n  CONNECT_CONFLICT_RESPONSE(4004, HttpStatus.CONFLICT),\n  DUPLICATED_ENTITY(4005, HttpStatus.CONFLICT),\n  UNPROCESSABLE_ENTITY(4006, HttpStatus.UNPROCESSABLE_ENTITY),\n  CLUSTER_NOT_FOUND(4007, HttpStatus.NOT_FOUND),\n  TOPIC_NOT_FOUND(4008, HttpStatus.NOT_FOUND),\n  SCHEMA_NOT_FOUND(4009, HttpStatus.NOT_FOUND),\n  CONNECT_NOT_FOUND(4010, HttpStatus.NOT_FOUND),\n  KSQLDB_NOT_FOUND(4011, HttpStatus.NOT_FOUND),\n  DIR_NOT_FOUND(4012, HttpStatus.BAD_REQUEST),\n  TOPIC_OR_PARTITION_NOT_FOUND(4013, HttpStatus.BAD_REQUEST),\n  INVALID_REQUEST(4014, HttpStatus.BAD_REQUEST),\n  RECREATE_TOPIC_TIMEOUT(4015, HttpStatus.REQUEST_TIMEOUT),\n  INVALID_ENTITY_STATE(4016, HttpStatus.BAD_REQUEST),\n  SCHEMA_NOT_DELETED(4017, HttpStatus.INTERNAL_SERVER_ERROR),\n  TOPIC_ANALYSIS_ERROR(4018, HttpStatus.BAD_REQUEST),\n  FILE_UPLOAD_EXCEPTION(4019, HttpStatus.INTERNAL_SERVER_ERROR),\n  ;\n\n  static {\n    // codes uniqueness check\n    var codes = new HashSet<Integer>();\n    for (ErrorCode value : ErrorCode.values()) {\n      if (!codes.add(value.code())) {\n        LoggerFactory.getLogger(ErrorCode.class)\n            .warn(\"Multiple {} values refer to code {}\", ErrorCode.class, value.code);\n      }\n    }\n  }\n\n  private final int code;\n  private final HttpStatus httpStatus;\n\n  ErrorCode(int code, HttpStatus httpStatus) {\n    this.code = code;\n    this.httpStatus = httpStatus;\n  }\n\n  public int code() {\n    return code;\n  }\n\n  public HttpStatus httpStatus() {\n    return httpStatus;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/FileUploadException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\nimport java.nio.file.Path;\n\npublic class FileUploadException extends CustomBaseException {\n\n  public FileUploadException(String msg, Throwable cause) {\n    super(msg, cause);\n  }\n\n  public FileUploadException(Path path, Throwable cause) {\n    super(\"Error uploading file %s\".formatted(path), cause);\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.FILE_UPLOAD_EXCEPTION;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/GlobalErrorWebExceptionHandler.java",
    "content": "package com.provectus.kafka.ui.exception;\n\nimport com.google.common.base.Throwables;\nimport com.google.common.collect.Sets;\nimport com.provectus.kafka.ui.model.ErrorResponseDTO;\nimport java.math.BigDecimal;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport org.springframework.boot.autoconfigure.web.WebProperties;\nimport org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler;\nimport org.springframework.boot.web.reactive.error.ErrorAttributes;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.core.Ordered;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.codec.ServerCodecConfigurer;\nimport org.springframework.stereotype.Component;\nimport org.springframework.validation.FieldError;\nimport org.springframework.web.bind.support.WebExchangeBindException;\nimport org.springframework.web.reactive.function.server.RequestPredicates;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ResponseStatusException;\nimport reactor.core.publisher.Mono;\n\n\n@Component\n@Order(Ordered.HIGHEST_PRECEDENCE)\npublic class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {\n\n  public GlobalErrorWebExceptionHandler(ErrorAttributes errorAttributes,\n                                        ApplicationContext applicationContext,\n                                        ServerCodecConfigurer codecConfigurer) {\n    super(errorAttributes, new WebProperties.Resources(), applicationContext);\n    this.setMessageWriters(codecConfigurer.getWriters());\n  }\n\n  @Override\n  protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {\n    return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);\n  }\n\n  private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {\n    Throwable throwable = getError(request);\n\n    // validation and params binding errors\n    if (throwable instanceof WebExchangeBindException) {\n      return render((WebExchangeBindException) throwable, request);\n    }\n\n    // requests mapping & access errors\n    if (throwable instanceof ResponseStatusException) {\n      return render((ResponseStatusException) throwable, request);\n    }\n\n    // custom exceptions\n    if (throwable instanceof CustomBaseException) {\n      return render((CustomBaseException) throwable, request);\n    }\n\n    return renderDefault(throwable, request);\n  }\n\n  private Mono<ServerResponse> renderDefault(Throwable throwable, ServerRequest request) {\n    var response = new ErrorResponseDTO()\n        .code(ErrorCode.UNEXPECTED.code())\n        .message(coalesce(throwable.getMessage(), \"Unexpected internal error\"))\n        .requestId(requestId(request))\n        .timestamp(currentTimestamp())\n        .stackTrace(Throwables.getStackTraceAsString(throwable));\n    return ServerResponse\n        .status(ErrorCode.UNEXPECTED.httpStatus())\n        .contentType(MediaType.APPLICATION_JSON)\n        .bodyValue(response);\n  }\n\n  private Mono<ServerResponse> render(CustomBaseException baseException, ServerRequest request) {\n    ErrorCode errorCode = baseException.getErrorCode();\n    var response = new ErrorResponseDTO()\n        .code(errorCode.code())\n        .message(coalesce(baseException.getMessage(), \"Internal error\"))\n        .requestId(requestId(request))\n        .timestamp(currentTimestamp())\n        .stackTrace(Throwables.getStackTraceAsString(baseException));\n    return ServerResponse\n        .status(errorCode.httpStatus())\n        .contentType(MediaType.APPLICATION_JSON)\n        .bodyValue(response);\n  }\n\n  private Mono<ServerResponse> render(WebExchangeBindException exception, ServerRequest request) {\n    Map<String, Set<String>> fieldErrorsMap = exception.getFieldErrors().stream()\n        .collect(Collectors\n            .toMap(FieldError::getField, f -> Set.of(extractFieldErrorMsg(f)), Sets::union));\n\n    var fieldsErrors = fieldErrorsMap.entrySet().stream()\n        .map(e -> {\n          var err = new com.provectus.kafka.ui.model.FieldErrorDTO();\n          err.setFieldName(e.getKey());\n          err.setRestrictions(List.copyOf(e.getValue()));\n          return err;\n        }).toList();\n\n    var message = fieldsErrors.isEmpty()\n        ? exception.getMessage()\n        : \"Fields validation failure\";\n\n    var response = new ErrorResponseDTO()\n        .code(ErrorCode.BINDING_FAIL.code())\n        .message(message)\n        .requestId(requestId(request))\n        .timestamp(currentTimestamp())\n        .fieldsErrors(fieldsErrors)\n        .stackTrace(Throwables.getStackTraceAsString(exception));\n    return ServerResponse\n        .status(HttpStatus.BAD_REQUEST)\n        .contentType(MediaType.APPLICATION_JSON)\n        .bodyValue(response);\n  }\n\n  private Mono<ServerResponse> render(ResponseStatusException exception, ServerRequest request) {\n    String msg = coalesce(exception.getReason(), exception.getMessage(), \"Server error\");\n    var response = new ErrorResponseDTO()\n        .code(ErrorCode.UNEXPECTED.code())\n        .message(msg)\n        .requestId(requestId(request))\n        .timestamp(currentTimestamp())\n        .stackTrace(Throwables.getStackTraceAsString(exception));\n    return ServerResponse\n        .status(exception.getStatusCode())\n        .contentType(MediaType.APPLICATION_JSON)\n        .bodyValue(response);\n  }\n\n  private String requestId(ServerRequest request) {\n    return request.exchange().getRequest().getId();\n  }\n\n  private BigDecimal currentTimestamp() {\n    return BigDecimal.valueOf(System.currentTimeMillis());\n  }\n\n  private String extractFieldErrorMsg(FieldError fieldError) {\n    return coalesce(fieldError.getDefaultMessage(), fieldError.getCode(), \"Invalid field value\");\n  }\n\n  private <T> T coalesce(T... items) {\n    return Stream.of(items).filter(Objects::nonNull).findFirst().orElse(null);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/IllegalEntityStateException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\npublic class IllegalEntityStateException extends CustomBaseException {\n  public IllegalEntityStateException(String message) {\n    super(message);\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.INVALID_ENTITY_STATE;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/InvalidRequestApiException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\npublic class InvalidRequestApiException extends CustomBaseException {\n\n  public InvalidRequestApiException(String message) {\n    super(message);\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.INVALID_REQUEST;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/JsonAvroConversionException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\npublic class JsonAvroConversionException extends ValidationException {\n  public JsonAvroConversionException(String message) {\n    super(message);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/KafkaConnectConflictReponseException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\n\nimport org.springframework.web.reactive.function.client.WebClientResponseException;\n\npublic class KafkaConnectConflictReponseException extends CustomBaseException {\n\n  public KafkaConnectConflictReponseException(WebClientResponseException.Conflict e) {\n    super(\"Kafka Connect responded with 409 (Conflict) code. Response body: \"\n        + e.getResponseBodyAsString());\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.CONNECT_CONFLICT_RESPONSE;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/KsqlApiException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\npublic class KsqlApiException extends CustomBaseException {\n\n  public KsqlApiException(String message) {\n    super(message);\n  }\n\n  public KsqlApiException(String message, Throwable cause) {\n    super(message, cause);\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.KSQL_API_ERROR;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/KsqlDbNotFoundException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\npublic class KsqlDbNotFoundException extends CustomBaseException {\n\n  public KsqlDbNotFoundException() {\n    super(\"KSQL DB not found\");\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.KSQLDB_NOT_FOUND;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/LogDirNotFoundApiException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\npublic class LogDirNotFoundApiException extends CustomBaseException {\n\n  public LogDirNotFoundApiException() {\n    super(\"The user-specified log directory is not found in the broker config.\");\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.DIR_NOT_FOUND;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/NotFoundException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\npublic class NotFoundException extends CustomBaseException {\n\n  public NotFoundException(String message) {\n    super(message);\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.NOT_FOUND;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ReadOnlyModeException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\n\npublic class ReadOnlyModeException extends CustomBaseException {\n\n  public ReadOnlyModeException() {\n    super(\"This cluster is in read-only mode.\");\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.READ_ONLY_MODE_ENABLE;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/SchemaCompatibilityException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\npublic class SchemaCompatibilityException extends CustomBaseException {\n  public SchemaCompatibilityException() {\n    super(\"Schema being registered is incompatible with an earlier schema\");\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.UNPROCESSABLE_ENTITY;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/SchemaFailedToDeleteException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\npublic class SchemaFailedToDeleteException extends CustomBaseException {\n\n  public SchemaFailedToDeleteException(String schemaName) {\n    super(String.format(\"Unable to delete schema with name %s\", schemaName));\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.SCHEMA_NOT_DELETED;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/SchemaNotFoundException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\npublic class SchemaNotFoundException extends CustomBaseException {\n\n  public SchemaNotFoundException() {\n    super(\"Schema not found\");\n  }\n\n  public SchemaNotFoundException(String message) {\n    super(message);\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.SCHEMA_NOT_FOUND;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/TopicAnalysisException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\npublic class TopicAnalysisException extends CustomBaseException {\n\n  public TopicAnalysisException(String message) {\n    super(message);\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.TOPIC_ANALYSIS_ERROR;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/TopicMetadataException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\npublic class TopicMetadataException extends CustomBaseException {\n\n  public TopicMetadataException(String message) {\n    super(message);\n  }\n\n  public TopicMetadataException(String message, Throwable cause) {\n    super(message, cause);\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.INVALID_ENTITY_STATE;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/TopicNotFoundException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\npublic class TopicNotFoundException extends CustomBaseException {\n\n  public TopicNotFoundException() {\n    super(\"Topic not found\");\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.TOPIC_NOT_FOUND;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/TopicOrPartitionNotFoundException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\npublic class TopicOrPartitionNotFoundException extends CustomBaseException {\n\n  public TopicOrPartitionNotFoundException() {\n    super(\"This server does not host this topic-partition.\");\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.TOPIC_OR_PARTITION_NOT_FOUND;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/TopicRecreationException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\npublic class TopicRecreationException extends CustomBaseException {\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.RECREATE_TOPIC_TIMEOUT;\n  }\n\n  public TopicRecreationException(String topicName, int seconds) {\n    super(String.format(\"Can't create topic '%s' in %d seconds: \"\n        + \"topic deletion is still in progress\", topicName, seconds));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/UnprocessableEntityException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\n\npublic class UnprocessableEntityException extends CustomBaseException {\n\n  public UnprocessableEntityException(String message) {\n    super(message);\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.UNPROCESSABLE_ENTITY;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ValidationException.java",
    "content": "package com.provectus.kafka.ui.exception;\n\n\npublic class ValidationException extends CustomBaseException {\n  public ValidationException(String message) {\n    super(message);\n  }\n\n  public ValidationException(String message, Throwable cause) {\n    super(message, cause);\n  }\n\n  @Override\n  public ErrorCode getErrorCode() {\n    return ErrorCode.VALIDATION_FAIL;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/ClusterMapper.java",
    "content": "package com.provectus.kafka.ui.mapper;\n\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.model.BrokerConfigDTO;\nimport com.provectus.kafka.ui.model.BrokerDTO;\nimport com.provectus.kafka.ui.model.BrokerDiskUsageDTO;\nimport com.provectus.kafka.ui.model.BrokerMetricsDTO;\nimport com.provectus.kafka.ui.model.ClusterDTO;\nimport com.provectus.kafka.ui.model.ClusterFeature;\nimport com.provectus.kafka.ui.model.ClusterMetricsDTO;\nimport com.provectus.kafka.ui.model.ClusterStatsDTO;\nimport com.provectus.kafka.ui.model.ConfigSourceDTO;\nimport com.provectus.kafka.ui.model.ConfigSynonymDTO;\nimport com.provectus.kafka.ui.model.ConnectDTO;\nimport com.provectus.kafka.ui.model.InternalBroker;\nimport com.provectus.kafka.ui.model.InternalBrokerConfig;\nimport com.provectus.kafka.ui.model.InternalBrokerDiskUsage;\nimport com.provectus.kafka.ui.model.InternalClusterState;\nimport com.provectus.kafka.ui.model.InternalPartition;\nimport com.provectus.kafka.ui.model.InternalReplica;\nimport com.provectus.kafka.ui.model.InternalTopic;\nimport com.provectus.kafka.ui.model.InternalTopicConfig;\nimport com.provectus.kafka.ui.model.KafkaAclDTO;\nimport com.provectus.kafka.ui.model.KafkaAclNamePatternTypeDTO;\nimport com.provectus.kafka.ui.model.KafkaAclResourceTypeDTO;\nimport com.provectus.kafka.ui.model.MetricDTO;\nimport com.provectus.kafka.ui.model.Metrics;\nimport com.provectus.kafka.ui.model.PartitionDTO;\nimport com.provectus.kafka.ui.model.ReplicaDTO;\nimport com.provectus.kafka.ui.model.TopicConfigDTO;\nimport com.provectus.kafka.ui.model.TopicDTO;\nimport com.provectus.kafka.ui.model.TopicDetailsDTO;\nimport com.provectus.kafka.ui.model.TopicProducerStateDTO;\nimport com.provectus.kafka.ui.service.metrics.RawMetric;\nimport java.util.List;\nimport java.util.Map;\nimport org.apache.kafka.clients.admin.ConfigEntry;\nimport org.apache.kafka.clients.admin.ProducerState;\nimport org.apache.kafka.common.acl.AccessControlEntry;\nimport org.apache.kafka.common.acl.AclBinding;\nimport org.apache.kafka.common.acl.AclOperation;\nimport org.apache.kafka.common.acl.AclPermissionType;\nimport org.apache.kafka.common.resource.PatternType;\nimport org.apache.kafka.common.resource.ResourcePattern;\nimport org.apache.kafka.common.resource.ResourceType;\nimport org.mapstruct.Mapper;\nimport org.mapstruct.Mapping;\n\n@Mapper(componentModel = \"spring\")\npublic interface ClusterMapper {\n\n  ClusterDTO toCluster(InternalClusterState clusterState);\n\n  ClusterStatsDTO toClusterStats(InternalClusterState clusterState);\n\n  default ClusterMetricsDTO toClusterMetrics(Metrics metrics) {\n    return new ClusterMetricsDTO()\n        .items(metrics.getSummarizedMetrics().map(this::convert).toList());\n  }\n\n  private MetricDTO convert(RawMetric rawMetric) {\n    return new MetricDTO()\n        .name(rawMetric.name())\n        .labels(rawMetric.labels())\n        .value(rawMetric.value());\n  }\n\n  default BrokerMetricsDTO toBrokerMetrics(List<RawMetric> metrics) {\n    return new BrokerMetricsDTO()\n        .metrics(metrics.stream().map(this::convert).toList());\n  }\n\n  @Mapping(target = \"isSensitive\", source = \"sensitive\")\n  @Mapping(target = \"isReadOnly\", source = \"readOnly\")\n  BrokerConfigDTO toBrokerConfig(InternalBrokerConfig config);\n\n  default ConfigSynonymDTO toConfigSynonym(ConfigEntry.ConfigSynonym config) {\n    if (config == null) {\n      return null;\n    }\n\n    ConfigSynonymDTO configSynonym = new ConfigSynonymDTO();\n    configSynonym.setName(config.name());\n    configSynonym.setValue(config.value());\n    if (config.source() != null) {\n      configSynonym.setSource(ConfigSourceDTO.valueOf(config.source().name()));\n    }\n\n    return configSynonym;\n  }\n\n  TopicDTO toTopic(InternalTopic topic);\n\n  PartitionDTO toPartition(InternalPartition topic);\n\n  BrokerDTO toBrokerDto(InternalBroker broker);\n\n  TopicDetailsDTO toTopicDetails(InternalTopic topic);\n\n  @Mapping(target = \"isReadOnly\", source = \"readOnly\")\n  @Mapping(target = \"isSensitive\", source = \"sensitive\")\n  TopicConfigDTO toTopicConfig(InternalTopicConfig topic);\n\n  ReplicaDTO toReplica(InternalReplica replica);\n\n  ConnectDTO toKafkaConnect(ClustersProperties.ConnectCluster connect);\n\n  List<ClusterDTO.FeaturesEnum> toFeaturesEnum(List<ClusterFeature> features);\n\n  default List<PartitionDTO> map(Map<Integer, InternalPartition> map) {\n    return map.values().stream().map(this::toPartition).toList();\n  }\n\n  default BrokerDiskUsageDTO map(Integer id, InternalBrokerDiskUsage internalBrokerDiskUsage) {\n    final BrokerDiskUsageDTO brokerDiskUsage = new BrokerDiskUsageDTO();\n    brokerDiskUsage.setBrokerId(id);\n    brokerDiskUsage.segmentCount((int) internalBrokerDiskUsage.getSegmentCount());\n    brokerDiskUsage.segmentSize(internalBrokerDiskUsage.getSegmentSize());\n    return brokerDiskUsage;\n  }\n\n  default TopicProducerStateDTO map(int partition, ProducerState state) {\n    return new TopicProducerStateDTO()\n        .partition(partition)\n        .producerId(state.producerId())\n        .producerEpoch(state.producerEpoch())\n        .lastSequence(state.lastSequence())\n        .lastTimestampMs(state.lastTimestamp())\n        .coordinatorEpoch(state.coordinatorEpoch().stream().boxed().findAny().orElse(null))\n        .currentTransactionStartOffset(state.currentTransactionStartOffset().stream().boxed().findAny().orElse(null));\n  }\n\n  static KafkaAclDTO.OperationEnum mapAclOperation(AclOperation operation) {\n    return switch (operation) {\n      case ALL -> KafkaAclDTO.OperationEnum.ALL;\n      case READ -> KafkaAclDTO.OperationEnum.READ;\n      case WRITE -> KafkaAclDTO.OperationEnum.WRITE;\n      case CREATE -> KafkaAclDTO.OperationEnum.CREATE;\n      case DELETE -> KafkaAclDTO.OperationEnum.DELETE;\n      case ALTER -> KafkaAclDTO.OperationEnum.ALTER;\n      case DESCRIBE -> KafkaAclDTO.OperationEnum.DESCRIBE;\n      case CLUSTER_ACTION -> KafkaAclDTO.OperationEnum.CLUSTER_ACTION;\n      case DESCRIBE_CONFIGS -> KafkaAclDTO.OperationEnum.DESCRIBE_CONFIGS;\n      case ALTER_CONFIGS -> KafkaAclDTO.OperationEnum.ALTER_CONFIGS;\n      case IDEMPOTENT_WRITE -> KafkaAclDTO.OperationEnum.IDEMPOTENT_WRITE;\n      case CREATE_TOKENS -> KafkaAclDTO.OperationEnum.CREATE_TOKENS;\n      case DESCRIBE_TOKENS -> KafkaAclDTO.OperationEnum.DESCRIBE_TOKENS;\n      case ANY -> throw new IllegalArgumentException(\"ANY operation can be only part of filter\");\n      case UNKNOWN -> KafkaAclDTO.OperationEnum.UNKNOWN;\n    };\n  }\n\n  static KafkaAclResourceTypeDTO mapAclResourceType(ResourceType resourceType) {\n    return switch (resourceType) {\n      case CLUSTER -> KafkaAclResourceTypeDTO.CLUSTER;\n      case TOPIC -> KafkaAclResourceTypeDTO.TOPIC;\n      case GROUP -> KafkaAclResourceTypeDTO.GROUP;\n      case DELEGATION_TOKEN -> KafkaAclResourceTypeDTO.DELEGATION_TOKEN;\n      case TRANSACTIONAL_ID -> KafkaAclResourceTypeDTO.TRANSACTIONAL_ID;\n      case USER -> KafkaAclResourceTypeDTO.USER;\n      case ANY -> throw new IllegalArgumentException(\"ANY type can be only part of filter\");\n      case UNKNOWN -> KafkaAclResourceTypeDTO.UNKNOWN;\n    };\n  }\n\n  static ResourceType mapAclResourceTypeDto(KafkaAclResourceTypeDTO dto) {\n    return ResourceType.valueOf(dto.name());\n  }\n\n  static PatternType mapPatternTypeDto(KafkaAclNamePatternTypeDTO dto) {\n    return PatternType.valueOf(dto.name());\n  }\n\n  static AclBinding toAclBinding(KafkaAclDTO dto) {\n    return new AclBinding(\n        new ResourcePattern(\n            mapAclResourceTypeDto(dto.getResourceType()),\n            dto.getResourceName(),\n            mapPatternTypeDto(dto.getNamePatternType())\n        ),\n        new AccessControlEntry(\n            dto.getPrincipal(),\n            dto.getHost(),\n            AclOperation.valueOf(dto.getOperation().name()),\n            AclPermissionType.valueOf(dto.getPermission().name())\n        )\n    );\n  }\n\n  static KafkaAclDTO toKafkaAclDto(AclBinding binding) {\n    var pattern = binding.pattern();\n    var filter = binding.toFilter().entryFilter();\n    return new KafkaAclDTO()\n        .resourceType(mapAclResourceType(pattern.resourceType()))\n        .resourceName(pattern.name())\n        .namePatternType(KafkaAclNamePatternTypeDTO.fromValue(pattern.patternType().name()))\n        .principal(filter.principal())\n        .host(filter.host())\n        .operation(mapAclOperation(filter.operation()))\n        .permission(KafkaAclDTO.PermissionEnum.fromValue(filter.permissionType().name()));\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/ConsumerGroupMapper.java",
    "content": "package com.provectus.kafka.ui.mapper;\n\nimport com.provectus.kafka.ui.model.BrokerDTO;\nimport com.provectus.kafka.ui.model.ConsumerGroupDTO;\nimport com.provectus.kafka.ui.model.ConsumerGroupDetailsDTO;\nimport com.provectus.kafka.ui.model.ConsumerGroupStateDTO;\nimport com.provectus.kafka.ui.model.ConsumerGroupTopicPartitionDTO;\nimport com.provectus.kafka.ui.model.InternalConsumerGroup;\nimport com.provectus.kafka.ui.model.InternalTopicConsumerGroup;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.apache.kafka.common.Node;\nimport org.apache.kafka.common.TopicPartition;\n\npublic class ConsumerGroupMapper {\n\n  private ConsumerGroupMapper() {\n  }\n\n  public static ConsumerGroupDTO toDto(InternalConsumerGroup c) {\n    return convertToConsumerGroup(c, new ConsumerGroupDTO());\n  }\n\n  public static ConsumerGroupDTO toDto(InternalTopicConsumerGroup c) {\n    ConsumerGroupDTO consumerGroup = new ConsumerGroupDetailsDTO();\n    consumerGroup.setTopics(1); //for ui backward-compatibility, need to rm usage from ui\n    consumerGroup.setGroupId(c.getGroupId());\n    consumerGroup.setMembers(c.getMembers());\n    consumerGroup.setConsumerLag(c.getConsumerLag());\n    consumerGroup.setSimple(c.isSimple());\n    consumerGroup.setPartitionAssignor(c.getPartitionAssignor());\n    consumerGroup.setState(mapConsumerGroupState(c.getState()));\n    Optional.ofNullable(c.getCoordinator())\n        .ifPresent(cd -> consumerGroup.setCoordinator(mapCoordinator(cd)));\n    return consumerGroup;\n  }\n\n  public static ConsumerGroupDetailsDTO toDetailsDto(InternalConsumerGroup g) {\n    ConsumerGroupDetailsDTO details = convertToConsumerGroup(g, new ConsumerGroupDetailsDTO());\n    Map<TopicPartition, ConsumerGroupTopicPartitionDTO> partitionMap = new HashMap<>();\n\n    for (Map.Entry<TopicPartition, Long> entry : g.getOffsets().entrySet()) {\n      ConsumerGroupTopicPartitionDTO partition = new ConsumerGroupTopicPartitionDTO();\n      partition.setTopic(entry.getKey().topic());\n      partition.setPartition(entry.getKey().partition());\n      partition.setCurrentOffset(entry.getValue());\n\n      final Optional<Long> endOffset = Optional.ofNullable(g.getEndOffsets())\n          .map(o -> o.get(entry.getKey()));\n\n      final Long behind = endOffset.map(o -> o - entry.getValue())\n          .orElse(0L);\n\n      partition.setEndOffset(endOffset.orElse(0L));\n      partition.setConsumerLag(behind);\n\n      partitionMap.put(entry.getKey(), partition);\n    }\n\n    for (InternalConsumerGroup.InternalMember member : g.getMembers()) {\n      for (TopicPartition topicPartition : member.getAssignment()) {\n        final ConsumerGroupTopicPartitionDTO partition = partitionMap.computeIfAbsent(\n            topicPartition,\n            tp -> new ConsumerGroupTopicPartitionDTO()\n                .topic(tp.topic())\n                .partition(tp.partition())\n        );\n        partition.setHost(member.getHost());\n        partition.setConsumerId(member.getConsumerId());\n        partitionMap.put(topicPartition, partition);\n      }\n    }\n    details.setPartitions(new ArrayList<>(partitionMap.values()));\n    return details;\n  }\n\n  private static <T extends ConsumerGroupDTO> T convertToConsumerGroup(\n      InternalConsumerGroup c, T consumerGroup) {\n    consumerGroup.setGroupId(c.getGroupId());\n    consumerGroup.setMembers(c.getMembers().size());\n    consumerGroup.setConsumerLag(c.getConsumerLag());\n    consumerGroup.setTopics(c.getTopicNum());\n    consumerGroup.setSimple(c.isSimple());\n\n    Optional.ofNullable(c.getState())\n        .ifPresent(s -> consumerGroup.setState(mapConsumerGroupState(s)));\n    Optional.ofNullable(c.getCoordinator())\n        .ifPresent(cd -> consumerGroup.setCoordinator(mapCoordinator(cd)));\n\n    consumerGroup.setPartitionAssignor(c.getPartitionAssignor());\n    return consumerGroup;\n  }\n\n  private static BrokerDTO mapCoordinator(Node node) {\n    return new BrokerDTO().host(node.host()).id(node.id()).port(node.port());\n  }\n\n  private static ConsumerGroupStateDTO mapConsumerGroupState(\n      org.apache.kafka.common.ConsumerGroupState state) {\n    switch (state) {\n      case DEAD:\n        return ConsumerGroupStateDTO.DEAD;\n      case EMPTY:\n        return ConsumerGroupStateDTO.EMPTY;\n      case STABLE:\n        return ConsumerGroupStateDTO.STABLE;\n      case PREPARING_REBALANCE:\n        return ConsumerGroupStateDTO.PREPARING_REBALANCE;\n      case COMPLETING_REBALANCE:\n        return ConsumerGroupStateDTO.COMPLETING_REBALANCE;\n      default:\n        return ConsumerGroupStateDTO.UNKNOWN;\n    }\n  }\n\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/DescribeLogDirsMapper.java",
    "content": "package com.provectus.kafka.ui.mapper;\n\nimport com.provectus.kafka.ui.model.BrokerTopicLogdirsDTO;\nimport com.provectus.kafka.ui.model.BrokerTopicPartitionLogdirDTO;\nimport com.provectus.kafka.ui.model.BrokersLogdirsDTO;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport org.apache.kafka.common.TopicPartition;\nimport org.apache.kafka.common.protocol.Errors;\nimport org.apache.kafka.common.requests.DescribeLogDirsResponse;\nimport org.springframework.stereotype.Component;\n\n@Component\npublic class DescribeLogDirsMapper {\n\n  public List<BrokersLogdirsDTO> toBrokerLogDirsList(\n      Map<Integer, Map<String, DescribeLogDirsResponse.LogDirInfo>> logDirsInfo) {\n\n    return logDirsInfo.entrySet().stream().map(\n        mapEntry -> mapEntry.getValue().entrySet().stream()\n            .map(e -> toBrokerLogDirs(mapEntry.getKey(), e.getKey(), e.getValue()))\n            .toList()\n    ).flatMap(Collection::stream).collect(Collectors.toList());\n  }\n\n  private BrokersLogdirsDTO toBrokerLogDirs(Integer broker, String dirName,\n                                            DescribeLogDirsResponse.LogDirInfo logDirInfo) {\n    BrokersLogdirsDTO result = new BrokersLogdirsDTO();\n    result.setName(dirName);\n    if (logDirInfo.error != null && logDirInfo.error != Errors.NONE) {\n      result.setError(logDirInfo.error.message());\n    }\n    var topics = logDirInfo.replicaInfos.entrySet().stream()\n        .collect(Collectors.groupingBy(e -> e.getKey().topic())).entrySet().stream()\n        .map(e -> toTopicLogDirs(broker, e.getKey(), e.getValue()))\n        .toList();\n    result.setTopics(topics);\n    return result;\n  }\n\n  private BrokerTopicLogdirsDTO toTopicLogDirs(Integer broker, String name,\n                                               List<Map.Entry<TopicPartition,\n                                                   DescribeLogDirsResponse.ReplicaInfo>> partitions) {\n    BrokerTopicLogdirsDTO topic = new BrokerTopicLogdirsDTO();\n    topic.setName(name);\n    topic.setPartitions(\n        partitions.stream().map(\n            e -> topicPartitionLogDir(\n                broker, e.getKey().partition(), e.getValue())).toList()\n    );\n    return topic;\n  }\n\n  private BrokerTopicPartitionLogdirDTO topicPartitionLogDir(Integer broker, Integer partition,\n                                                             DescribeLogDirsResponse.ReplicaInfo\n                                                                 replicaInfo) {\n    BrokerTopicPartitionLogdirDTO logDir = new BrokerTopicPartitionLogdirDTO();\n    logDir.setBroker(broker);\n    logDir.setPartition(partition);\n    logDir.setSize(replicaInfo.size);\n    logDir.setOffsetLag(replicaInfo.offsetLag);\n    return logDir;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/KafkaConnectMapper.java",
    "content": "package com.provectus.kafka.ui.mapper;\n\nimport com.provectus.kafka.ui.connect.model.ConnectorStatusConnector;\nimport com.provectus.kafka.ui.connect.model.ConnectorTask;\nimport com.provectus.kafka.ui.connect.model.NewConnector;\nimport com.provectus.kafka.ui.model.ConnectorDTO;\nimport com.provectus.kafka.ui.model.ConnectorPluginConfigValidationResponseDTO;\nimport com.provectus.kafka.ui.model.ConnectorPluginDTO;\nimport com.provectus.kafka.ui.model.ConnectorStatusDTO;\nimport com.provectus.kafka.ui.model.ConnectorTaskStatusDTO;\nimport com.provectus.kafka.ui.model.FullConnectorInfoDTO;\nimport com.provectus.kafka.ui.model.TaskDTO;\nimport com.provectus.kafka.ui.model.TaskStatusDTO;\nimport com.provectus.kafka.ui.model.connect.InternalConnectInfo;\nimport java.util.List;\nimport org.mapstruct.Mapper;\n\n@Mapper(componentModel = \"spring\")\npublic interface KafkaConnectMapper {\n  NewConnector toClient(com.provectus.kafka.ui.model.NewConnectorDTO newConnector);\n\n  ConnectorDTO fromClient(com.provectus.kafka.ui.connect.model.Connector connector);\n\n  ConnectorStatusDTO fromClient(ConnectorStatusConnector connectorStatus);\n\n  TaskDTO fromClient(ConnectorTask connectorTask);\n\n  TaskStatusDTO fromClient(com.provectus.kafka.ui.connect.model.TaskStatus taskStatus);\n\n  ConnectorPluginDTO fromClient(\n      com.provectus.kafka.ui.connect.model.ConnectorPlugin connectorPlugin);\n\n  ConnectorPluginConfigValidationResponseDTO fromClient(\n      com.provectus.kafka.ui.connect.model.ConnectorPluginConfigValidationResponse\n          connectorPluginConfigValidationResponse);\n\n  default FullConnectorInfoDTO fullConnectorInfo(InternalConnectInfo connectInfo) {\n    ConnectorDTO connector = connectInfo.getConnector();\n    List<TaskDTO> tasks = connectInfo.getTasks();\n    int failedTasksCount = (int) tasks.stream()\n        .map(TaskDTO::getStatus)\n        .map(TaskStatusDTO::getState)\n        .filter(ConnectorTaskStatusDTO.FAILED::equals)\n        .count();\n    return new FullConnectorInfoDTO()\n        .connect(connector.getConnect())\n        .name(connector.getName())\n        .connectorClass((String) connectInfo.getConfig().get(\"connector.class\"))\n        .type(connector.getType())\n        .topics(connectInfo.getTopics())\n        .status(connector.getStatus())\n        .tasksCount(tasks.size())\n        .failedTasksCount(failedTasksCount);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/mapper/KafkaSrMapper.java",
    "content": "package com.provectus.kafka.ui.mapper;\n\nimport com.provectus.kafka.ui.model.CompatibilityCheckResponseDTO;\nimport com.provectus.kafka.ui.model.CompatibilityLevelDTO;\nimport com.provectus.kafka.ui.model.NewSchemaSubjectDTO;\nimport com.provectus.kafka.ui.model.SchemaReferenceDTO;\nimport com.provectus.kafka.ui.model.SchemaSubjectDTO;\nimport com.provectus.kafka.ui.model.SchemaTypeDTO;\nimport com.provectus.kafka.ui.service.SchemaRegistryService;\nimport com.provectus.kafka.ui.sr.model.Compatibility;\nimport com.provectus.kafka.ui.sr.model.CompatibilityCheckResponse;\nimport com.provectus.kafka.ui.sr.model.NewSubject;\nimport com.provectus.kafka.ui.sr.model.SchemaReference;\nimport com.provectus.kafka.ui.sr.model.SchemaType;\nimport java.util.List;\nimport java.util.Optional;\nimport org.mapstruct.Mapper;\n\n\n@Mapper\npublic interface KafkaSrMapper {\n\n  default SchemaSubjectDTO toDto(SchemaRegistryService.SubjectWithCompatibilityLevel s) {\n    return new SchemaSubjectDTO()\n        .id(s.getId())\n        .version(s.getVersion())\n        .subject(s.getSubject())\n        .schema(s.getSchema())\n        .schemaType(SchemaTypeDTO.fromValue(Optional.ofNullable(s.getSchemaType()).orElse(SchemaType.AVRO).getValue()))\n        .references(toDto(s.getReferences()))\n        .compatibilityLevel(s.getCompatibility().toString());\n  }\n\n  List<SchemaReferenceDTO> toDto(List<SchemaReference> references);\n\n  CompatibilityCheckResponseDTO toDto(CompatibilityCheckResponse ccr);\n\n  CompatibilityLevelDTO.CompatibilityEnum toDto(Compatibility compatibility);\n\n  NewSubject fromDto(NewSchemaSubjectDTO subjectDto);\n\n  Compatibility fromDto(CompatibilityLevelDTO.CompatibilityEnum dtoEnum);\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/BrokerMetrics.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport java.util.List;\nimport lombok.Builder;\nimport lombok.Data;\n\n@Data\n@Builder(toBuilder = true)\npublic class BrokerMetrics {\n  private final List<MetricDTO> metrics;\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/CleanupPolicy.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\npublic enum CleanupPolicy {\n  DELETE(\"delete\"),\n  COMPACT(\"compact\"),\n  COMPACT_DELETE(Arrays.asList(\"compact,delete\", \"delete,compact\")),\n  UNKNOWN(\"unknown\");\n\n  private final List<String> policies;\n\n  CleanupPolicy(String policy) {\n    this(Collections.singletonList(policy));\n  }\n\n  CleanupPolicy(List<String> policies) {\n    this.policies = policies;\n  }\n\n  public String getPolicy() {\n    return policies.get(0);\n  }\n\n  public static CleanupPolicy fromString(String string) {\n    return Arrays.stream(CleanupPolicy.values())\n        .filter(v ->\n            v.policies.stream().anyMatch(\n                s -> s.equals(string.replace(\" \", \"\")\n                )\n            )\n        ).findFirst()\n        .orElse(UNKNOWN);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/ClusterFeature.java",
    "content": "package com.provectus.kafka.ui.model;\n\npublic enum ClusterFeature {\n  KAFKA_CONNECT,\n  KSQL_DB,\n  SCHEMA_REGISTRY,\n  TOPIC_DELETION,\n  KAFKA_ACL_VIEW,\n  KAFKA_ACL_EDIT\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/ConsumerPosition.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport java.util.Map;\nimport javax.annotation.Nullable;\nimport lombok.Value;\nimport org.apache.kafka.common.TopicPartition;\n\n@Value\npublic class ConsumerPosition {\n  SeekTypeDTO seekType;\n  String topic;\n  @Nullable\n  Map<TopicPartition, Long> seekTo; // null if positioning should apply to all tps\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalBroker.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport java.math.BigDecimal;\nimport javax.annotation.Nullable;\nimport lombok.Data;\nimport org.apache.kafka.common.Node;\n\n@Data\npublic class InternalBroker {\n\n  private final Integer id;\n  private final String host;\n  private final Integer port;\n  private final @Nullable BigDecimal bytesInPerSec;\n  private final @Nullable BigDecimal bytesOutPerSec;\n  private final @Nullable Integer partitionsLeader;\n  private final @Nullable Integer partitions;\n  private final @Nullable Integer inSyncPartitions;\n  private final @Nullable BigDecimal leadersSkew;\n  private final @Nullable BigDecimal partitionsSkew;\n\n  public InternalBroker(Node node,\n                        PartitionDistributionStats partitionDistribution,\n                        Statistics statistics) {\n    this.id = node.id();\n    this.host = node.host();\n    this.port = node.port();\n    this.bytesInPerSec = statistics.getMetrics().getBrokerBytesInPerSec().get(node.id());\n    this.bytesOutPerSec = statistics.getMetrics().getBrokerBytesOutPerSec().get(node.id());\n    this.partitionsLeader = partitionDistribution.getPartitionLeaders().get(node);\n    this.partitions = partitionDistribution.getPartitionsCount().get(node);\n    this.inSyncPartitions = partitionDistribution.getInSyncPartitions().get(node);\n    this.leadersSkew = partitionDistribution.leadersSkew(node);\n    this.partitionsSkew = partitionDistribution.partitionsSkew(node);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalBrokerConfig.java",
    "content": "package com.provectus.kafka.ui.model;\n\n\nimport java.util.List;\nimport lombok.Builder;\nimport lombok.Data;\nimport org.apache.kafka.clients.admin.ConfigEntry;\n\n@Data\n@Builder\npublic class InternalBrokerConfig {\n  private final String name;\n  private final String value;\n  private final ConfigEntry.ConfigSource source;\n  private final boolean isSensitive;\n  private final boolean isReadOnly;\n  private final List<ConfigEntry.ConfigSynonym> synonyms;\n\n  public static InternalBrokerConfig from(ConfigEntry configEntry) {\n    InternalBrokerConfig.InternalBrokerConfigBuilder builder = InternalBrokerConfig.builder()\n        .name(configEntry.name())\n        .value(configEntry.value())\n        .source(configEntry.source())\n        .isReadOnly(configEntry.isReadOnly())\n        .isSensitive(configEntry.isSensitive())\n        .synonyms(configEntry.synonyms());\n    return builder.build();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalBrokerDiskUsage.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport lombok.Builder;\nimport lombok.Data;\n\n@Data\n@Builder(toBuilder = true)\npublic class InternalBrokerDiskUsage {\n  private final long segmentCount;\n  private final long segmentSize;\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalClusterMetrics.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport java.math.BigDecimal;\nimport java.util.List;\nimport java.util.Map;\nimport javax.annotation.Nullable;\nimport lombok.Builder;\nimport lombok.Data;\n\n\n@Data\n@Builder(toBuilder = true)\npublic class InternalClusterMetrics {\n\n  public static InternalClusterMetrics empty() {\n    return InternalClusterMetrics.builder()\n        .brokers(List.of())\n        .topics(Map.of())\n        .status(ServerStatusDTO.OFFLINE)\n        .internalBrokerMetrics(Map.of())\n        .metrics(List.of())\n        .version(\"unknown\")\n        .build();\n  }\n\n  private final String version;\n\n  private final ServerStatusDTO status;\n  private final Throwable lastKafkaException;\n\n  private final int brokerCount;\n  private final int activeControllers;\n  private final List<Integer> brokers;\n\n  private final int topicCount;\n  private final Map<String, InternalTopic> topics;\n\n  // partitions stats\n  private final int underReplicatedPartitionCount;\n  private final int onlinePartitionCount;\n  private final int offlinePartitionCount;\n  private final int inSyncReplicasCount;\n  private final int outOfSyncReplicasCount;\n\n  // log dir stats\n  @Nullable // will be null if log dir collection disabled\n  private final Map<Integer, InternalBrokerDiskUsage> internalBrokerDiskUsage;\n\n  // metrics from metrics collector\n  private final BigDecimal bytesInPerSec;\n  private final BigDecimal bytesOutPerSec;\n  private final Map<Integer, BrokerMetrics> internalBrokerMetrics;\n  private final List<MetricDTO> metrics;\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalClusterState.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport com.google.common.base.Throwables;\nimport java.math.BigDecimal;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\nimport lombok.Data;\nimport org.apache.kafka.common.Node;\n\n@Data\npublic class InternalClusterState {\n  private String name;\n  private ServerStatusDTO status;\n  private MetricsCollectionErrorDTO lastError;\n  private Integer topicCount;\n  private Integer brokerCount;\n  private Integer activeControllers;\n  private Integer onlinePartitionCount;\n  private Integer offlinePartitionCount;\n  private Integer inSyncReplicasCount;\n  private Integer outOfSyncReplicasCount;\n  private Integer underReplicatedPartitionCount;\n  private List<BrokerDiskUsageDTO> diskUsage;\n  private String version;\n  private List<ClusterFeature> features;\n  private BigDecimal bytesInPerSec;\n  private BigDecimal bytesOutPerSec;\n  private Boolean readOnly;\n\n  public InternalClusterState(KafkaCluster cluster, Statistics statistics) {\n    name = cluster.getName();\n    status = statistics.getStatus();\n    lastError = Optional.ofNullable(statistics.getLastKafkaException())\n        .map(e -> new MetricsCollectionErrorDTO()\n            .message(e.getMessage())\n            .stackTrace(Throwables.getStackTraceAsString(e)))\n        .orElse(null);\n    topicCount = statistics.getTopicDescriptions().size();\n    brokerCount = statistics.getClusterDescription().getNodes().size();\n    activeControllers = Optional.ofNullable(statistics.getClusterDescription().getController())\n        .map(Node::id)\n        .orElse(null);\n    version = statistics.getVersion();\n\n    if (statistics.getLogDirInfo() != null) {\n      diskUsage = statistics.getLogDirInfo().getBrokerStats().entrySet().stream()\n          .map(e -> new BrokerDiskUsageDTO()\n              .brokerId(e.getKey())\n              .segmentSize(e.getValue().getSegmentSize())\n              .segmentCount(e.getValue().getSegmentsCount()))\n          .collect(Collectors.toList());\n    }\n\n    features = statistics.getFeatures();\n\n    bytesInPerSec = statistics\n        .getMetrics()\n        .getBrokerBytesInPerSec()\n        .values().stream()\n        .reduce(BigDecimal::add)\n        .orElse(null);\n\n    bytesOutPerSec = statistics\n        .getMetrics()\n        .getBrokerBytesOutPerSec()\n        .values().stream()\n        .reduce(BigDecimal::add)\n        .orElse(null);\n\n    var partitionsStats = new PartitionsStats(statistics.getTopicDescriptions().values());\n    onlinePartitionCount = partitionsStats.getOnlinePartitionCount();\n    offlinePartitionCount = partitionsStats.getOfflinePartitionCount();\n    inSyncReplicasCount = partitionsStats.getInSyncReplicasCount();\n    outOfSyncReplicasCount = partitionsStats.getOutOfSyncReplicasCount();\n    underReplicatedPartitionCount = partitionsStats.getUnderReplicatedPartitionCount();\n    readOnly = cluster.isReadOnly();\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalConsumerGroup.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport java.util.Collection;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport lombok.Builder;\nimport lombok.Data;\nimport org.apache.kafka.clients.admin.ConsumerGroupDescription;\nimport org.apache.kafka.common.ConsumerGroupState;\nimport org.apache.kafka.common.Node;\nimport org.apache.kafka.common.TopicPartition;\n\n@Data\n@Builder(toBuilder = true)\npublic class InternalConsumerGroup {\n  private final String groupId;\n  private final boolean simple;\n  private final Collection<InternalMember> members;\n  private final Map<TopicPartition, Long> offsets;\n  private final Map<TopicPartition, Long> endOffsets;\n  private final Long consumerLag;\n  private final Integer topicNum;\n  private final String partitionAssignor;\n  private final ConsumerGroupState state;\n  private final Node coordinator;\n\n  @Data\n  @Builder(toBuilder = true)\n  public static class InternalMember {\n    private final String consumerId;\n    private final String groupInstanceId;\n    private final String clientId;\n    private final String host;\n    private final Set<TopicPartition> assignment;\n  }\n\n  public static InternalConsumerGroup create(\n      ConsumerGroupDescription description,\n      Map<TopicPartition, Long> groupOffsets,\n      Map<TopicPartition, Long> topicEndOffsets) {\n    var builder = InternalConsumerGroup.builder();\n    builder.groupId(description.groupId());\n    builder.simple(description.isSimpleConsumerGroup());\n    builder.state(description.state());\n    builder.partitionAssignor(description.partitionAssignor());\n    Collection<InternalMember> internalMembers = initInternalMembers(description);\n    builder.members(internalMembers);\n    builder.offsets(groupOffsets);\n    builder.endOffsets(topicEndOffsets);\n    builder.consumerLag(calculateConsumerLag(groupOffsets, topicEndOffsets));\n    builder.topicNum(calculateTopicNum(groupOffsets, internalMembers));\n    Optional.ofNullable(description.coordinator()).ifPresent(builder::coordinator);\n    return builder.build();\n  }\n\n  private static Long calculateConsumerLag(Map<TopicPartition, Long> offsets, Map<TopicPartition, Long> endOffsets) {\n    Long consumerLag = null;\n    // consumerLag should be undefined if no committed offsets found for topic\n    if (!offsets.isEmpty()) {\n      consumerLag = offsets.entrySet().stream()\n          .mapToLong(e ->\n              Optional.ofNullable(endOffsets)\n                  .map(o -> o.get(e.getKey()))\n                  .map(o -> o - e.getValue())\n                  .orElse(0L)\n          ).sum();\n    }\n\n    return consumerLag;\n  }\n\n  private static Integer calculateTopicNum(Map<TopicPartition, Long> offsets, Collection<InternalMember> members) {\n\n    return (int) Stream.concat(\n        offsets.keySet().stream().map(TopicPartition::topic),\n        members.stream()\n            .flatMap(m -> m.getAssignment().stream().map(TopicPartition::topic))\n    ).distinct().count();\n\n  }\n\n  private static Collection<InternalMember> initInternalMembers(ConsumerGroupDescription description) {\n    return description.members().stream()\n        .map(m ->\n            InternalConsumerGroup.InternalMember.builder()\n                .assignment(m.assignment().topicPartitions())\n                .clientId(m.clientId())\n                .groupInstanceId(m.groupInstanceId().orElse(\"\"))\n                .consumerId(m.consumerId())\n                .clientId(m.clientId())\n                .host(m.host())\n                .build()\n        ).collect(Collectors.toList());\n  }\n\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalLogDirStats.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport static java.util.stream.Collectors.collectingAndThen;\nimport static java.util.stream.Collectors.groupingBy;\nimport static java.util.stream.Collectors.summarizingLong;\nimport static java.util.stream.Collectors.toList;\n\nimport java.util.List;\nimport java.util.LongSummaryStatistics;\nimport java.util.Map;\nimport lombok.Value;\nimport org.apache.kafka.common.TopicPartition;\nimport org.apache.kafka.common.requests.DescribeLogDirsResponse;\nimport reactor.util.function.Tuple2;\nimport reactor.util.function.Tuple3;\nimport reactor.util.function.Tuples;\n\n@Value\npublic class InternalLogDirStats {\n\n  @Value\n  public static class SegmentStats {\n    long segmentSize;\n    int segmentsCount;\n\n    public SegmentStats(LongSummaryStatistics s) {\n      segmentSize = s.getSum();\n      segmentsCount = (int) s.getCount();\n    }\n  }\n\n  Map<TopicPartition, SegmentStats> partitionsStats;\n  Map<String, SegmentStats> topicStats;\n  Map<Integer, SegmentStats> brokerStats;\n\n  public static InternalLogDirStats empty() {\n    return new InternalLogDirStats(Map.of());\n  }\n\n  public InternalLogDirStats(Map<Integer, Map<String, DescribeLogDirsResponse.LogDirInfo>> log) {\n    final List<Tuple3<Integer, TopicPartition, Long>> topicPartitions =\n        log.entrySet().stream().flatMap(b ->\n            b.getValue().entrySet().stream().flatMap(topicMap ->\n                topicMap.getValue().replicaInfos.entrySet().stream()\n                    .map(e -> Tuples.of(b.getKey(), e.getKey(), e.getValue().size))\n            )\n        ).toList();\n\n    partitionsStats = topicPartitions.stream().collect(\n        groupingBy(\n            Tuple2::getT2,\n            collectingAndThen(\n                summarizingLong(Tuple3::getT3), SegmentStats::new)));\n\n    topicStats =\n        topicPartitions.stream().collect(\n            groupingBy(\n                t -> t.getT2().topic(),\n                collectingAndThen(\n                    summarizingLong(Tuple3::getT3), SegmentStats::new)));\n\n    brokerStats = topicPartitions.stream().collect(\n        groupingBy(\n            Tuple2::getT1,\n            collectingAndThen(\n                summarizingLong(Tuple3::getT3), SegmentStats::new)));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalPartition.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport java.util.List;\nimport lombok.Builder;\nimport lombok.Data;\n\n@Data\n@Builder(toBuilder = true)\npublic class InternalPartition {\n  private final int partition;\n  private final Integer leader;\n  private final List<InternalReplica> replicas;\n  private final int inSyncReplicasCount;\n  private final int replicasCount;\n\n  private final Long offsetMin;\n  private final Long offsetMax;\n\n  // from log dir\n  private final Long segmentSize;\n  private final Integer segmentCount;\n\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalPartitionsOffsets.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport com.google.common.collect.HashBasedTable;\nimport com.google.common.collect.Table;\nimport java.util.Map;\nimport java.util.Optional;\nimport lombok.Value;\nimport org.apache.kafka.common.TopicPartition;\n\n\npublic class InternalPartitionsOffsets {\n\n  @Value\n  public static class Offsets {\n    Long earliest;\n    Long latest;\n  }\n\n  private final Table<String, Integer, Offsets> offsets = HashBasedTable.create();\n\n  public InternalPartitionsOffsets(Map<TopicPartition, Offsets> offsetsMap) {\n    offsetsMap.forEach((tp, o) -> this.offsets.put(tp.topic(), tp.partition(), o));\n  }\n\n  public static InternalPartitionsOffsets empty() {\n    return new InternalPartitionsOffsets(Map.of());\n  }\n\n  public Optional<Offsets> get(String topic, int partition) {\n    return Optional.ofNullable(offsets.get(topic, partition));\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalReplica.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\n\n@Data\n@Builder\n@RequiredArgsConstructor\npublic class InternalReplica {\n  private final int broker;\n  private final boolean leader;\n  private final boolean inSync;\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalSegmentSizeDto.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport java.util.Map;\nimport lombok.Builder;\nimport lombok.Data;\n\n@Data\n@Builder(toBuilder = true)\npublic class InternalSegmentSizeDto {\n\n  private final Map<String, InternalTopic> internalTopicWithSegmentSize;\n  private final InternalClusterMetrics clusterMetricsWithSegmentSize;\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalTopic.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport java.math.BigDecimal;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport javax.annotation.Nullable;\nimport lombok.Builder;\nimport lombok.Data;\nimport org.apache.kafka.clients.admin.ConfigEntry;\nimport org.apache.kafka.clients.admin.TopicDescription;\nimport org.apache.kafka.common.TopicPartition;\n\n@Data\n@Builder(toBuilder = true)\npublic class InternalTopic {\n\n  ClustersProperties clustersProperties;\n\n  // from TopicDescription\n  private final String name;\n  private final boolean internal;\n  private final int replicas;\n  private final int partitionCount;\n  private final int inSyncReplicas;\n  private final int replicationFactor;\n  private final int underReplicatedPartitions;\n  private final Map<Integer, InternalPartition> partitions;\n\n  // topic configs\n  private final List<InternalTopicConfig> topicConfigs;\n  private final CleanupPolicy cleanUpPolicy;\n\n  // rates from metrics\n  private final BigDecimal bytesInPerSec;\n  private final BigDecimal bytesOutPerSec;\n\n  // from log dir data\n  private final long segmentSize;\n  private final long segmentCount;\n\n  public static InternalTopic from(TopicDescription topicDescription,\n                                   List<ConfigEntry> configs,\n                                   InternalPartitionsOffsets partitionsOffsets,\n                                   Metrics metrics,\n                                   InternalLogDirStats logDirInfo,\n                                   @Nullable String internalTopicPrefix) {\n    var topic = InternalTopic.builder();\n\n    internalTopicPrefix = internalTopicPrefix == null || internalTopicPrefix.isEmpty()\n        ? \"_\"\n        : internalTopicPrefix;\n\n    topic.internal(\n        topicDescription.isInternal() || topicDescription.name().startsWith(internalTopicPrefix)\n    );\n    topic.name(topicDescription.name());\n\n    List<InternalPartition> partitions = topicDescription.partitions().stream()\n        .map(partition -> {\n          var partitionDto = InternalPartition.builder();\n\n          partitionDto.leader(partition.leader() != null ? partition.leader().id() : null);\n          partitionDto.partition(partition.partition());\n          partitionDto.inSyncReplicasCount(partition.isr().size());\n          partitionDto.replicasCount(partition.replicas().size());\n          List<InternalReplica> replicas = partition.replicas().stream()\n              .map(r ->\n                  InternalReplica.builder()\n                      .broker(r.id())\n                      .inSync(partition.isr().contains(r))\n                      .leader(partition.leader() != null && partition.leader().id() == r.id())\n                      .build())\n              .collect(Collectors.toList());\n          partitionDto.replicas(replicas);\n\n          partitionsOffsets.get(topicDescription.name(), partition.partition())\n              .ifPresent(offsets -> {\n                partitionDto.offsetMin(offsets.getEarliest());\n                partitionDto.offsetMax(offsets.getLatest());\n              });\n\n          var segmentStats =\n              logDirInfo.getPartitionsStats().get(\n                  new TopicPartition(topicDescription.name(), partition.partition()));\n          if (segmentStats != null) {\n            partitionDto.segmentCount(segmentStats.getSegmentsCount());\n            partitionDto.segmentSize(segmentStats.getSegmentSize());\n          }\n\n          return partitionDto.build();\n        })\n        .toList();\n\n    topic.partitions(partitions.stream().collect(\n        Collectors.toMap(InternalPartition::getPartition, t -> t)));\n\n    var partitionsStats = new PartitionsStats(topicDescription);\n    topic.replicas(partitionsStats.getReplicasCount());\n    topic.partitionCount(partitionsStats.getPartitionsCount());\n    topic.inSyncReplicas(partitionsStats.getInSyncReplicasCount());\n    topic.underReplicatedPartitions(partitionsStats.getUnderReplicatedPartitionCount());\n\n    topic.replicationFactor(\n        topicDescription.partitions().isEmpty()\n            ? 0\n            : topicDescription.partitions().get(0).replicas().size()\n    );\n\n    var segmentStats = logDirInfo.getTopicStats().get(topicDescription.name());\n    if (segmentStats != null) {\n      topic.segmentCount(segmentStats.getSegmentsCount());\n      topic.segmentSize(segmentStats.getSegmentSize());\n    }\n\n    topic.bytesInPerSec(metrics.getTopicBytesInPerSec().get(topicDescription.name()));\n    topic.bytesOutPerSec(metrics.getTopicBytesOutPerSec().get(topicDescription.name()));\n\n    topic.topicConfigs(\n        configs.stream().map(InternalTopicConfig::from).collect(Collectors.toList()));\n\n    topic.cleanUpPolicy(\n        configs.stream()\n            .filter(config -> config.name().equals(\"cleanup.policy\"))\n            .findFirst()\n            .map(ConfigEntry::value)\n            .map(CleanupPolicy::fromString)\n            .orElse(CleanupPolicy.UNKNOWN)\n    );\n\n    return topic.build();\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalTopicConfig.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport java.util.List;\nimport lombok.Builder;\nimport lombok.Data;\nimport org.apache.kafka.clients.admin.ConfigEntry;\n\n\n@Data\n@Builder\npublic class InternalTopicConfig {\n  private final String name;\n  private final String value;\n  private final String defaultValue;\n  private final ConfigEntry.ConfigSource source;\n  private final boolean isSensitive;\n  private final boolean isReadOnly;\n  private final List<ConfigEntry.ConfigSynonym> synonyms;\n  private final String doc;\n\n  public static InternalTopicConfig from(ConfigEntry configEntry) {\n    InternalTopicConfig.InternalTopicConfigBuilder builder = InternalTopicConfig.builder()\n        .name(configEntry.name())\n        .value(configEntry.value())\n        .source(configEntry.source())\n        .isReadOnly(configEntry.isReadOnly())\n        .isSensitive(configEntry.isSensitive())\n        .synonyms(configEntry.synonyms())\n        .doc(configEntry.documentation());\n\n    if (configEntry.source() == ConfigEntry.ConfigSource.DEFAULT_CONFIG) {\n      // this is important case, because for some configs like \"confluent.*\" no synonyms returned, but\n      // they are set by default and \"source\" == DEFAULT_CONFIG\n      builder.defaultValue(configEntry.value());\n    } else {\n      // normally by default first entity of synonyms values will be used.\n      configEntry.synonyms().stream()\n          // skipping DYNAMIC_TOPIC_CONFIG value - which is explicitly set value when\n          // topic was created (not default), see ConfigEntry.synonyms() doc\n          .filter(s -> s.source() != ConfigEntry.ConfigSource.DYNAMIC_TOPIC_CONFIG)\n          .map(ConfigEntry.ConfigSynonym::value)\n          .findFirst()\n          .ifPresent(builder::defaultValue);\n    }\n    return builder.build();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalTopicConsumerGroup.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport java.util.Map;\nimport java.util.Optional;\nimport javax.annotation.Nullable;\nimport lombok.Builder;\nimport lombok.Value;\nimport org.apache.kafka.clients.admin.ConsumerGroupDescription;\nimport org.apache.kafka.common.ConsumerGroupState;\nimport org.apache.kafka.common.Node;\nimport org.apache.kafka.common.TopicPartition;\n\n@Value\n@Builder\npublic class InternalTopicConsumerGroup {\n\n  String groupId;\n  int members;\n  @Nullable\n  Long consumerLag; //null means no committed offsets found for this group\n  boolean isSimple;\n  String partitionAssignor;\n  ConsumerGroupState state;\n  @Nullable\n  Node coordinator;\n\n  public static InternalTopicConsumerGroup create(\n      String topic,\n      ConsumerGroupDescription g,\n      Map<TopicPartition, Long> committedOffsets,\n      Map<TopicPartition, Long> endOffsets) {\n    return InternalTopicConsumerGroup.builder()\n        .groupId(g.groupId())\n        .members(\n            (int) g.members().stream()\n                // counting only members with target topic assignment\n                .filter(m -> m.assignment().topicPartitions().stream().anyMatch(p -> p.topic().equals(topic)))\n                .count()\n        )\n        .consumerLag(calculateConsumerLag(committedOffsets, endOffsets))\n        .isSimple(g.isSimpleConsumerGroup())\n        .partitionAssignor(g.partitionAssignor())\n        .state(g.state())\n        .coordinator(g.coordinator())\n        .build();\n  }\n\n  @Nullable\n  private static Long calculateConsumerLag(Map<TopicPartition, Long> committedOffsets,\n                                           Map<TopicPartition, Long> endOffsets) {\n    if (committedOffsets.isEmpty()) {\n      return null;\n    }\n    return committedOffsets.entrySet().stream()\n        .mapToLong(e ->\n            Optional.ofNullable(endOffsets.get(e.getKey()))\n                .map(o -> o - e.getValue())\n                .orElse(0L)\n        ).sum();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/KafkaCluster.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.connect.api.KafkaConnectClientApi;\nimport com.provectus.kafka.ui.emitter.PollingSettings;\nimport com.provectus.kafka.ui.service.ksql.KsqlApiClient;\nimport com.provectus.kafka.ui.service.masking.DataMasking;\nimport com.provectus.kafka.ui.sr.api.KafkaSrClientApi;\nimport com.provectus.kafka.ui.util.ReactiveFailover;\nimport java.util.Map;\nimport java.util.Properties;\nimport lombok.AccessLevel;\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Data;\n\n@Data\n@Builder(toBuilder = true)\n@AllArgsConstructor(access = AccessLevel.PRIVATE)\npublic class KafkaCluster {\n  private final ClustersProperties.Cluster originalProperties;\n\n  private final String name;\n  private final String version;\n  private final String bootstrapServers;\n  private final Properties properties;\n  private final boolean readOnly;\n  private final MetricsConfig metricsConfig;\n  private final DataMasking masking;\n  private final PollingSettings pollingSettings;\n  private final ReactiveFailover<KafkaSrClientApi> schemaRegistryClient;\n  private final Map<String, ReactiveFailover<KafkaConnectClientApi>> connectsClients;\n  private final ReactiveFailover<KsqlApiClient> ksqlClient;\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/Metrics.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport static java.util.stream.Collectors.toMap;\n\nimport com.provectus.kafka.ui.service.metrics.RawMetric;\nimport java.math.BigDecimal;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Stream;\nimport lombok.Builder;\nimport lombok.Value;\n\n\n@Builder\n@Value\npublic class Metrics {\n\n  Map<Integer, BigDecimal> brokerBytesInPerSec;\n  Map<Integer, BigDecimal> brokerBytesOutPerSec;\n  Map<String, BigDecimal> topicBytesInPerSec;\n  Map<String, BigDecimal> topicBytesOutPerSec;\n  Map<Integer, List<RawMetric>> perBrokerMetrics;\n\n  public static Metrics empty() {\n    return Metrics.builder()\n        .brokerBytesInPerSec(Map.of())\n        .brokerBytesOutPerSec(Map.of())\n        .topicBytesInPerSec(Map.of())\n        .topicBytesOutPerSec(Map.of())\n        .perBrokerMetrics(Map.of())\n        .build();\n  }\n\n  public Stream<RawMetric> getSummarizedMetrics() {\n    return perBrokerMetrics.values().stream()\n        .flatMap(Collection::stream)\n        .collect(toMap(RawMetric::identityKey, m -> m, (m1, m2) -> m1.copyWithValue(m1.value().add(m2.value()))))\n        .values()\n        .stream();\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/MetricsConfig.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport lombok.AccessLevel;\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Data;\n\n@Data\n@Builder(toBuilder = true)\n@AllArgsConstructor(access = AccessLevel.PRIVATE)\npublic class MetricsConfig {\n  public static final String JMX_METRICS_TYPE = \"JMX\";\n  public static final String PROMETHEUS_METRICS_TYPE = \"PROMETHEUS\";\n\n  private final String type;\n  private final Integer port;\n  private final boolean ssl;\n  private final String username;\n  private final String password;\n  private final String keystoreLocation;\n  private final String keystorePassword;\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/PartitionDistributionStats.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport java.math.BigDecimal;\nimport java.math.RoundingMode;\nimport java.util.HashMap;\nimport java.util.Map;\nimport javax.annotation.Nullable;\nimport lombok.AccessLevel;\nimport lombok.Getter;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.kafka.clients.admin.TopicDescription;\nimport org.apache.kafka.common.Node;\nimport org.apache.kafka.common.TopicPartitionInfo;\n\n@RequiredArgsConstructor(access = AccessLevel.PRIVATE)\n@Getter\n@Slf4j\npublic class PartitionDistributionStats {\n\n  // avg skew will show unuseful results on low number of partitions\n  private static final int MIN_PARTITIONS_FOR_SKEW_CALCULATION = 50;\n\n  private final Map<Node, Integer> partitionLeaders;\n  private final Map<Node, Integer> partitionsCount;\n  private final Map<Node, Integer> inSyncPartitions;\n  private final double avgLeadersCntPerBroker;\n  private final double avgPartitionsPerBroker;\n  private final boolean skewCanBeCalculated;\n\n  public static PartitionDistributionStats create(Statistics stats) {\n    return create(stats, MIN_PARTITIONS_FOR_SKEW_CALCULATION);\n  }\n\n  static PartitionDistributionStats create(Statistics stats, int minPartitionsForSkewCalculation) {\n    var partitionLeaders = new HashMap<Node, Integer>();\n    var partitionsReplicated = new HashMap<Node, Integer>();\n    var isr = new HashMap<Node, Integer>();\n    int partitionsCnt = 0;\n    for (TopicDescription td : stats.getTopicDescriptions().values()) {\n      for (TopicPartitionInfo tp : td.partitions()) {\n        partitionsCnt++;\n        tp.replicas().forEach(r -> incr(partitionsReplicated, r));\n        tp.isr().forEach(r -> incr(isr, r));\n        if (tp.leader() != null) {\n          incr(partitionLeaders, tp.leader());\n        }\n      }\n    }\n    int nodesWithPartitions = partitionsReplicated.size();\n    int partitionReplications = partitionsReplicated.values().stream().mapToInt(i -> i).sum();\n    var avgPartitionsPerBroker = nodesWithPartitions == 0 ? 0 : ((double) partitionReplications) / nodesWithPartitions;\n\n    int nodesWithLeaders = partitionLeaders.size();\n    int leadersCnt = partitionLeaders.values().stream().mapToInt(i -> i).sum();\n    var avgLeadersCntPerBroker = nodesWithLeaders == 0 ? 0 : ((double) leadersCnt) / nodesWithLeaders;\n\n    return new PartitionDistributionStats(\n        partitionLeaders,\n        partitionsReplicated,\n        isr,\n        avgLeadersCntPerBroker,\n        avgPartitionsPerBroker,\n        partitionsCnt >= minPartitionsForSkewCalculation\n    );\n  }\n\n  private static void incr(Map<Node, Integer> map, Node n) {\n    map.compute(n, (k, c) -> c == null ? 1 : ++c);\n  }\n\n  @Nullable\n  public BigDecimal partitionsSkew(Node node) {\n    return calculateAvgSkew(partitionsCount.get(node), avgPartitionsPerBroker);\n  }\n\n  @Nullable\n  public BigDecimal leadersSkew(Node node) {\n    return calculateAvgSkew(partitionLeaders.get(node), avgLeadersCntPerBroker);\n  }\n\n  // Returns difference (in percents) from average value, null if it can't be calculated\n  @Nullable\n  private BigDecimal calculateAvgSkew(@Nullable Integer value, double avgValue) {\n    if (avgValue == 0 || !skewCanBeCalculated) {\n      return null;\n    }\n    value = value == null ? 0 : value;\n    return new BigDecimal((value - avgValue) / avgValue * 100.0)\n        .setScale(1, RoundingMode.HALF_UP);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/PartitionsStats.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport java.util.Collection;\nimport java.util.List;\nimport lombok.Data;\nimport org.apache.kafka.clients.admin.TopicDescription;\n\n@Data\npublic class PartitionsStats {\n\n  private int partitionsCount;\n  private int replicasCount;\n  private int onlinePartitionCount;\n  private int offlinePartitionCount;\n  private int inSyncReplicasCount;\n  private int outOfSyncReplicasCount;\n  private int underReplicatedPartitionCount;\n\n  public PartitionsStats(TopicDescription description) {\n    this(List.of(description));\n  }\n\n  public PartitionsStats(Collection<TopicDescription> topicDescriptions) {\n    topicDescriptions.stream()\n        .flatMap(t -> t.partitions().stream())\n        .forEach(p -> {\n          partitionsCount++;\n          replicasCount += p.replicas().size();\n          onlinePartitionCount += p.leader() != null ? 1 : 0;\n          offlinePartitionCount += p.leader() == null ? 1 : 0;\n          inSyncReplicasCount += p.isr().size();\n          outOfSyncReplicasCount += (p.replicas().size() - p.isr().size());\n          if (p.replicas().size() > p.isr().size()) {\n            underReplicatedPartitionCount++;\n          }\n        });\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/Statistics.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport com.provectus.kafka.ui.service.ReactiveAdminClient;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport lombok.Builder;\nimport lombok.Value;\nimport org.apache.kafka.clients.admin.ConfigEntry;\nimport org.apache.kafka.clients.admin.TopicDescription;\n\n@Value\n@Builder(toBuilder = true)\npublic class Statistics {\n  ServerStatusDTO status;\n  Throwable lastKafkaException;\n  String version;\n  List<ClusterFeature> features;\n  ReactiveAdminClient.ClusterDescription clusterDescription;\n  Metrics metrics;\n  InternalLogDirStats logDirInfo;\n  Map<String, TopicDescription> topicDescriptions;\n  Map<String, List<ConfigEntry>> topicConfigs;\n\n  public static Statistics empty() {\n    return builder()\n        .status(ServerStatusDTO.OFFLINE)\n        .version(\"Unknown\")\n        .features(List.of())\n        .clusterDescription(\n            new ReactiveAdminClient.ClusterDescription(null, null, List.of(), Set.of()))\n        .metrics(Metrics.empty())\n        .logDirInfo(InternalLogDirStats.empty())\n        .topicDescriptions(Map.of())\n        .topicConfigs(Map.of())\n        .build();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/connect/InternalConnectInfo.java",
    "content": "package com.provectus.kafka.ui.model.connect;\n\nimport com.provectus.kafka.ui.model.ConnectorDTO;\nimport com.provectus.kafka.ui.model.TaskDTO;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.Builder;\nimport lombok.Data;\n\n@Data\n@Builder(toBuilder = true)\npublic class InternalConnectInfo {\n  private final ConnectorDTO connector;\n  private final Map<String, Object> config;\n  private final List<TaskDTO> tasks;\n  private final List<String> topics;\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/AccessContext.java",
    "content": "package com.provectus.kafka.ui.model.rbac;\n\nimport com.provectus.kafka.ui.model.rbac.permission.AclAction;\nimport com.provectus.kafka.ui.model.rbac.permission.ApplicationConfigAction;\nimport com.provectus.kafka.ui.model.rbac.permission.AuditAction;\nimport com.provectus.kafka.ui.model.rbac.permission.ClusterConfigAction;\nimport com.provectus.kafka.ui.model.rbac.permission.ConnectAction;\nimport com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction;\nimport com.provectus.kafka.ui.model.rbac.permission.KsqlAction;\nimport com.provectus.kafka.ui.model.rbac.permission.SchemaAction;\nimport com.provectus.kafka.ui.model.rbac.permission.TopicAction;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.Value;\nimport org.springframework.util.Assert;\n\n@Value\npublic class AccessContext {\n\n  Collection<ApplicationConfigAction> applicationConfigActions;\n\n  String cluster;\n  Collection<ClusterConfigAction> clusterConfigActions;\n\n  String topic;\n  Collection<TopicAction> topicActions;\n\n  String consumerGroup;\n  Collection<ConsumerGroupAction> consumerGroupActions;\n\n  String connect;\n  Collection<ConnectAction> connectActions;\n\n  String connector;\n\n  String schema;\n  Collection<SchemaAction> schemaActions;\n\n  Collection<KsqlAction> ksqlActions;\n\n  Collection<AclAction> aclActions;\n\n  Collection<AuditAction> auditAction;\n\n  String operationName;\n  Object operationParams;\n\n  public static AccessContextBuilder builder() {\n    return new AccessContextBuilder();\n  }\n\n  public static final class AccessContextBuilder {\n    private static final String ACTIONS_NOT_PRESENT = \"actions not present\";\n\n    private Collection<ApplicationConfigAction> applicationConfigActions = Collections.emptySet();\n    private String cluster;\n    private Collection<ClusterConfigAction> clusterConfigActions = Collections.emptySet();\n    private String topic;\n    private Collection<TopicAction> topicActions = Collections.emptySet();\n    private String consumerGroup;\n    private Collection<ConsumerGroupAction> consumerGroupActions = Collections.emptySet();\n    private String connect;\n    private Collection<ConnectAction> connectActions = Collections.emptySet();\n    private String connector;\n    private String schema;\n    private Collection<SchemaAction> schemaActions = Collections.emptySet();\n    private Collection<KsqlAction> ksqlActions = Collections.emptySet();\n    private Collection<AclAction> aclActions = Collections.emptySet();\n    private Collection<AuditAction> auditActions = Collections.emptySet();\n\n    private String operationName;\n    private Object operationParams;\n\n    private AccessContextBuilder() {\n    }\n\n    public AccessContextBuilder applicationConfigActions(ApplicationConfigAction... actions) {\n      Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT);\n      this.applicationConfigActions = List.of(actions);\n      return this;\n    }\n\n    public AccessContextBuilder cluster(String cluster) {\n      this.cluster = cluster;\n      return this;\n    }\n\n    public AccessContextBuilder clusterConfigActions(ClusterConfigAction... actions) {\n      Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT);\n      this.clusterConfigActions = List.of(actions);\n      return this;\n    }\n\n    public AccessContextBuilder topic(String topic) {\n      this.topic = topic;\n      return this;\n    }\n\n    public AccessContextBuilder topicActions(TopicAction... actions) {\n      Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT);\n      this.topicActions = List.of(actions);\n      return this;\n    }\n\n    public AccessContextBuilder consumerGroup(String consumerGroup) {\n      this.consumerGroup = consumerGroup;\n      return this;\n    }\n\n    public AccessContextBuilder consumerGroupActions(ConsumerGroupAction... actions) {\n      Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT);\n      this.consumerGroupActions = List.of(actions);\n      return this;\n    }\n\n    public AccessContextBuilder connect(String connect) {\n      this.connect = connect;\n      return this;\n    }\n\n    public AccessContextBuilder connectActions(ConnectAction... actions) {\n      Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT);\n      this.connectActions = List.of(actions);\n      return this;\n    }\n\n    public AccessContextBuilder connector(String connector) {\n      this.connector = connector;\n      return this;\n    }\n\n    public AccessContextBuilder schema(String schema) {\n      this.schema = schema;\n      return this;\n    }\n\n    public AccessContextBuilder schemaActions(SchemaAction... actions) {\n      Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT);\n      this.schemaActions = List.of(actions);\n      return this;\n    }\n\n    public AccessContextBuilder ksqlActions(KsqlAction... actions) {\n      Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT);\n      this.ksqlActions = List.of(actions);\n      return this;\n    }\n\n    public AccessContextBuilder aclActions(AclAction... actions) {\n      Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT);\n      this.aclActions = List.of(actions);\n      return this;\n    }\n\n    public AccessContextBuilder auditActions(AuditAction... actions) {\n      Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT);\n      this.auditActions = List.of(actions);\n      return this;\n    }\n\n    public AccessContextBuilder operationName(String operationName) {\n      this.operationName = operationName;\n      return this;\n    }\n\n    public AccessContextBuilder operationParams(Object operationParams) {\n      this.operationParams = operationParams;\n      return this;\n    }\n\n    public AccessContextBuilder operationParams(Map<String, Object> paramsMap) {\n      this.operationParams = paramsMap;\n      return this;\n    }\n\n    public AccessContext build() {\n      return new AccessContext(\n          applicationConfigActions,\n          cluster, clusterConfigActions,\n          topic, topicActions,\n          consumerGroup, consumerGroupActions,\n          connect, connectActions,\n          connector,\n          schema, schemaActions,\n          ksqlActions, aclActions, auditActions,\n          operationName, operationParams);\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Permission.java",
    "content": "package com.provectus.kafka.ui.model.rbac;\n\nimport static com.provectus.kafka.ui.model.rbac.Resource.ACL;\nimport static com.provectus.kafka.ui.model.rbac.Resource.APPLICATIONCONFIG;\nimport static com.provectus.kafka.ui.model.rbac.Resource.AUDIT;\nimport static com.provectus.kafka.ui.model.rbac.Resource.CLUSTERCONFIG;\nimport static com.provectus.kafka.ui.model.rbac.Resource.KSQL;\n\nimport com.provectus.kafka.ui.model.rbac.permission.AclAction;\nimport com.provectus.kafka.ui.model.rbac.permission.ApplicationConfigAction;\nimport com.provectus.kafka.ui.model.rbac.permission.AuditAction;\nimport com.provectus.kafka.ui.model.rbac.permission.ClusterConfigAction;\nimport com.provectus.kafka.ui.model.rbac.permission.ConnectAction;\nimport com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction;\nimport com.provectus.kafka.ui.model.rbac.permission.KsqlAction;\nimport com.provectus.kafka.ui.model.rbac.permission.SchemaAction;\nimport com.provectus.kafka.ui.model.rbac.permission.TopicAction;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.regex.Pattern;\nimport javax.annotation.Nullable;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.ToString;\nimport org.apache.commons.collections4.CollectionUtils;\nimport org.springframework.util.Assert;\n\n@Getter\n@ToString\n@EqualsAndHashCode\npublic class Permission {\n\n  private static final List<Resource> RBAC_ACTION_EXEMPT_LIST =\n      List.of(KSQL, CLUSTERCONFIG, APPLICATIONCONFIG, ACL, AUDIT);\n\n  Resource resource;\n  List<String> actions;\n\n  @Nullable\n  String value;\n  @Nullable\n  transient Pattern compiledValuePattern;\n\n  @SuppressWarnings(\"unused\")\n  public void setResource(String resource) {\n    this.resource = Resource.fromString(resource.toUpperCase());\n  }\n\n  @SuppressWarnings(\"unused\")\n  public void setValue(@Nullable String value) {\n    this.value = value;\n  }\n\n  @SuppressWarnings(\"unused\")\n  public void setActions(List<String> actions) {\n    this.actions = actions;\n  }\n\n  public void validate() {\n    Assert.notNull(resource, \"resource cannot be null\");\n    if (!RBAC_ACTION_EXEMPT_LIST.contains(this.resource)) {\n      Assert.notNull(value, \"permission value can't be empty for resource \" + resource);\n    }\n  }\n\n  public void transform() {\n    if (value != null) {\n      this.compiledValuePattern = Pattern.compile(value);\n    }\n    if (CollectionUtils.isNotEmpty(actions) && actions.stream().anyMatch(\"ALL\"::equalsIgnoreCase)) {\n      this.actions = getAllActionValues();\n    }\n  }\n\n  private List<String> getAllActionValues() {\n    if (resource == null) {\n      return Collections.emptyList();\n    }\n\n    return switch (this.resource) {\n      case APPLICATIONCONFIG -> Arrays.stream(ApplicationConfigAction.values()).map(Enum::toString).toList();\n      case CLUSTERCONFIG -> Arrays.stream(ClusterConfigAction.values()).map(Enum::toString).toList();\n      case TOPIC -> Arrays.stream(TopicAction.values()).map(Enum::toString).toList();\n      case CONSUMER -> Arrays.stream(ConsumerGroupAction.values()).map(Enum::toString).toList();\n      case SCHEMA -> Arrays.stream(SchemaAction.values()).map(Enum::toString).toList();\n      case CONNECT -> Arrays.stream(ConnectAction.values()).map(Enum::toString).toList();\n      case KSQL -> Arrays.stream(KsqlAction.values()).map(Enum::toString).toList();\n      case ACL -> Arrays.stream(AclAction.values()).map(Enum::toString).toList();\n      case AUDIT -> Arrays.stream(AuditAction.values()).map(Enum::toString).toList();\n    };\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Resource.java",
    "content": "package com.provectus.kafka.ui.model.rbac;\n\nimport org.apache.commons.lang3.EnumUtils;\nimport org.jetbrains.annotations.Nullable;\n\npublic enum Resource {\n\n  APPLICATIONCONFIG,\n  CLUSTERCONFIG,\n  TOPIC,\n  CONSUMER,\n  SCHEMA,\n  CONNECT,\n  KSQL,\n  ACL,\n  AUDIT;\n\n  @Nullable\n  public static Resource fromString(String name) {\n    return EnumUtils.getEnum(Resource.class, name);\n  }\n\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Role.java",
    "content": "package com.provectus.kafka.ui.model.rbac;\n\nimport java.util.List;\nimport lombok.Data;\n\n@Data\npublic class Role {\n\n  String name;\n  List<String> clusters;\n  List<Subject> subjects;\n  List<Permission> permissions;\n\n  public void validate() {\n    permissions.forEach(Permission::transform);\n    permissions.forEach(Permission::validate);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Subject.java",
    "content": "package com.provectus.kafka.ui.model.rbac;\n\nimport com.provectus.kafka.ui.model.rbac.provider.Provider;\nimport lombok.Getter;\n\n@Getter\npublic class Subject {\n\n  Provider provider;\n  String type;\n  String value;\n\n  public void setProvider(String provider) {\n    this.provider = Provider.fromString(provider.toUpperCase());\n  }\n\n  public void setType(String type) {\n    this.type = type;\n  }\n\n  public void setValue(String value) {\n    this.value = value;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/AclAction.java",
    "content": "package com.provectus.kafka.ui.model.rbac.permission;\n\nimport java.util.Set;\nimport org.apache.commons.lang3.EnumUtils;\nimport org.jetbrains.annotations.Nullable;\n\npublic enum AclAction implements PermissibleAction {\n\n  VIEW,\n  EDIT\n\n  ;\n\n  public static final Set<AclAction> ALTER_ACTIONS = Set.of(EDIT);\n\n  @Nullable\n  public static AclAction fromString(String name) {\n    return EnumUtils.getEnum(AclAction.class, name);\n  }\n\n  @Override\n  public boolean isAlter() {\n    return ALTER_ACTIONS.contains(this);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ApplicationConfigAction.java",
    "content": "package com.provectus.kafka.ui.model.rbac.permission;\n\nimport java.util.Set;\nimport org.apache.commons.lang3.EnumUtils;\nimport org.jetbrains.annotations.Nullable;\n\npublic enum ApplicationConfigAction implements PermissibleAction {\n\n  VIEW,\n  EDIT\n\n  ;\n\n  public static final Set<ApplicationConfigAction> ALTER_ACTIONS = Set.of(EDIT);\n\n  @Nullable\n  public static ApplicationConfigAction fromString(String name) {\n    return EnumUtils.getEnum(ApplicationConfigAction.class, name);\n  }\n\n  @Override\n  public boolean isAlter() {\n    return ALTER_ACTIONS.contains(this);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/AuditAction.java",
    "content": "package com.provectus.kafka.ui.model.rbac.permission;\n\nimport java.util.Set;\nimport org.apache.commons.lang3.EnumUtils;\nimport org.jetbrains.annotations.Nullable;\n\npublic enum AuditAction implements PermissibleAction {\n\n  VIEW\n\n  ;\n\n  private static final Set<AuditAction> ALTER_ACTIONS = Set.of();\n\n  @Nullable\n  public static AuditAction fromString(String name) {\n    return EnumUtils.getEnum(AuditAction.class, name);\n  }\n\n  @Override\n  public boolean isAlter() {\n    return ALTER_ACTIONS.contains(this);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ClusterConfigAction.java",
    "content": "package com.provectus.kafka.ui.model.rbac.permission;\n\nimport java.util.Set;\nimport org.apache.commons.lang3.EnumUtils;\nimport org.jetbrains.annotations.Nullable;\n\npublic enum ClusterConfigAction implements PermissibleAction {\n\n  VIEW,\n  EDIT\n\n  ;\n\n  public static final Set<ClusterConfigAction> ALTER_ACTIONS = Set.of(EDIT);\n\n  @Nullable\n  public static ClusterConfigAction fromString(String name) {\n    return EnumUtils.getEnum(ClusterConfigAction.class, name);\n  }\n\n  @Override\n  public boolean isAlter() {\n    return ALTER_ACTIONS.contains(this);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ConnectAction.java",
    "content": "package com.provectus.kafka.ui.model.rbac.permission;\n\nimport java.util.Set;\nimport org.apache.commons.lang3.EnumUtils;\nimport org.jetbrains.annotations.Nullable;\n\npublic enum ConnectAction implements PermissibleAction {\n\n  VIEW,\n  EDIT,\n  CREATE,\n  RESTART\n\n  ;\n\n  public static final Set<ConnectAction> ALTER_ACTIONS = Set.of(CREATE, EDIT, RESTART);\n\n  @Nullable\n  public static ConnectAction fromString(String name) {\n    return EnumUtils.getEnum(ConnectAction.class, name);\n  }\n\n  @Override\n  public boolean isAlter() {\n    return ALTER_ACTIONS.contains(this);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ConsumerGroupAction.java",
    "content": "package com.provectus.kafka.ui.model.rbac.permission;\n\nimport java.util.Set;\nimport org.apache.commons.lang3.EnumUtils;\nimport org.jetbrains.annotations.Nullable;\n\npublic enum ConsumerGroupAction implements PermissibleAction {\n\n  VIEW,\n  DELETE,\n  RESET_OFFSETS\n\n  ;\n\n  public static final Set<ConsumerGroupAction> ALTER_ACTIONS = Set.of(DELETE, RESET_OFFSETS);\n\n  @Nullable\n  public static ConsumerGroupAction fromString(String name) {\n    return EnumUtils.getEnum(ConsumerGroupAction.class, name);\n  }\n\n  @Override\n  public boolean isAlter() {\n    return ALTER_ACTIONS.contains(this);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/KsqlAction.java",
    "content": "package com.provectus.kafka.ui.model.rbac.permission;\n\nimport java.util.Set;\nimport org.apache.commons.lang3.EnumUtils;\nimport org.jetbrains.annotations.Nullable;\n\npublic enum KsqlAction implements PermissibleAction {\n\n  EXECUTE\n\n  ;\n\n  public static final Set<KsqlAction> ALTER_ACTIONS = Set.of(EXECUTE);\n\n  @Nullable\n  public static KsqlAction fromString(String name) {\n    return EnumUtils.getEnum(KsqlAction.class, name);\n  }\n\n  @Override\n  public boolean isAlter() {\n    return ALTER_ACTIONS.contains(this);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/PermissibleAction.java",
    "content": "package com.provectus.kafka.ui.model.rbac.permission;\n\npublic sealed interface PermissibleAction permits\n    AclAction, ApplicationConfigAction,\n    ConsumerGroupAction, SchemaAction,\n    ConnectAction, ClusterConfigAction,\n    KsqlAction, TopicAction, AuditAction {\n\n  String name();\n\n  boolean isAlter();\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/SchemaAction.java",
    "content": "package com.provectus.kafka.ui.model.rbac.permission;\n\nimport java.util.Set;\nimport org.apache.commons.lang3.EnumUtils;\nimport org.jetbrains.annotations.Nullable;\n\npublic enum SchemaAction implements PermissibleAction {\n\n  VIEW,\n  CREATE,\n  DELETE,\n  EDIT,\n  MODIFY_GLOBAL_COMPATIBILITY\n\n  ;\n\n  public static final Set<SchemaAction> ALTER_ACTIONS = Set.of(CREATE, DELETE, EDIT, MODIFY_GLOBAL_COMPATIBILITY);\n\n  @Nullable\n  public static SchemaAction fromString(String name) {\n    return EnumUtils.getEnum(SchemaAction.class, name);\n  }\n\n  @Override\n  public boolean isAlter() {\n    return ALTER_ACTIONS.contains(this);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/TopicAction.java",
    "content": "package com.provectus.kafka.ui.model.rbac.permission;\n\nimport java.util.Set;\nimport org.apache.commons.lang3.EnumUtils;\nimport org.jetbrains.annotations.Nullable;\n\npublic enum TopicAction implements PermissibleAction {\n\n  VIEW,\n  CREATE,\n  EDIT,\n  DELETE,\n  MESSAGES_READ,\n  MESSAGES_PRODUCE,\n  MESSAGES_DELETE,\n\n  ;\n\n  public static final Set<TopicAction> ALTER_ACTIONS = Set.of(CREATE, EDIT, DELETE, MESSAGES_PRODUCE, MESSAGES_DELETE);\n\n  @Nullable\n  public static TopicAction fromString(String name) {\n    return EnumUtils.getEnum(TopicAction.class, name);\n  }\n\n  @Override\n  public boolean isAlter() {\n    return ALTER_ACTIONS.contains(this);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/provider/Provider.java",
    "content": "package com.provectus.kafka.ui.model.rbac.provider;\n\nimport org.apache.commons.lang3.EnumUtils;\nimport org.jetbrains.annotations.Nullable;\n\npublic enum Provider {\n\n  OAUTH_GOOGLE,\n  OAUTH_GITHUB,\n\n  OAUTH_COGNITO,\n\n  OAUTH,\n\n  LDAP,\n  LDAP_AD;\n\n  @Nullable\n  public static Provider fromString(String name) {\n    return EnumUtils.getEnum(Provider.class, name);\n  }\n\n  public static class Name {\n    public static String GOOGLE = \"google\";\n    public static String GITHUB = \"github\";\n    public static String COGNITO = \"cognito\";\n\n    public static String OAUTH = \"oauth\";\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/schemaregistry/ErrorResponse.java",
    "content": "package com.provectus.kafka.ui.model.schemaregistry;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport lombok.Data;\n\n@Data\npublic class ErrorResponse {\n\n  @JsonProperty(\"error_code\")\n  private int errorCode;\n\n  private String message;\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/schemaregistry/InternalCompatibilityCheck.java",
    "content": "package com.provectus.kafka.ui.model.schemaregistry;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport lombok.Data;\n\n@Data\npublic class InternalCompatibilityCheck {\n  @JsonProperty(\"is_compatible\")\n  private boolean isCompatible;\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/schemaregistry/InternalCompatibilityLevel.java",
    "content": "package com.provectus.kafka.ui.model.schemaregistry;\n\nimport lombok.Data;\n\n@Data\npublic class InternalCompatibilityLevel {\n  private String compatibilityLevel;\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/schemaregistry/InternalNewSchema.java",
    "content": "package com.provectus.kafka.ui.model.schemaregistry;\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.provectus.kafka.ui.model.SchemaTypeDTO;\nimport lombok.Data;\n\n@Data\npublic class InternalNewSchema {\n  private String schema;\n  @JsonInclude(JsonInclude.Include.NON_NULL)\n  private SchemaTypeDTO schemaType;\n\n  public InternalNewSchema(String schema, SchemaTypeDTO schemaType) {\n    this.schema = schema;\n    this.schemaType = schemaType;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/schemaregistry/SubjectIdResponse.java",
    "content": "package com.provectus.kafka.ui.model.schemaregistry;\n\nimport lombok.Data;\n\n@Data\npublic class SubjectIdResponse {\n  private Integer id;\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/BuiltInSerde.java",
    "content": "package com.provectus.kafka.ui.serdes;\n\nimport com.provectus.kafka.ui.serde.api.PropertyResolver;\nimport com.provectus.kafka.ui.serde.api.Serde;\n\npublic interface BuiltInSerde extends Serde {\n\n  // returns true is serde has enough properties set on cluster&global levels to\n  // be configured without explicit config provide\n  default boolean canBeAutoConfigured(PropertyResolver kafkaClusterProperties,\n                                      PropertyResolver globalProperties) {\n    return true;\n  }\n\n  // will be called for build-in serdes that were not explicitly registered\n  // and that returned true on canBeAutoConfigured(..) call.\n  // NOTE: Serde.configure() method won't be called if serde is auto-configured!\n  default void autoConfigure(PropertyResolver kafkaClusterProperties,\n                             PropertyResolver globalProperties) {\n  }\n\n  @Override\n  default void configure(PropertyResolver serdeProperties,\n                         PropertyResolver kafkaClusterProperties,\n                         PropertyResolver globalProperties) {\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ClassloaderUtil.java",
    "content": "package com.provectus.kafka.ui.serdes;\n\nclass ClassloaderUtil {\n\n  static ClassLoader compareAndSwapLoaders(ClassLoader loader) {\n    ClassLoader current = Thread.currentThread().getContextClassLoader();\n    if (!current.equals(loader)) {\n      Thread.currentThread().setContextClassLoader(loader);\n    }\n    return current;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ClusterSerdes.java",
    "content": "package com.provectus.kafka.ui.serdes;\n\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport com.provectus.kafka.ui.serdes.builtin.StringSerde;\nimport java.io.Closeable;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.function.Predicate;\nimport java.util.stream.Stream;\nimport javax.annotation.Nullable;\nimport lombok.Getter;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\n\n@Slf4j\n@RequiredArgsConstructor\npublic class ClusterSerdes implements Closeable {\n\n  final Map<String, SerdeInstance> serdes;\n\n  @Nullable\n  final SerdeInstance defaultKeySerde;\n\n  @Nullable\n  final SerdeInstance defaultValueSerde;\n\n  @Getter\n  final SerdeInstance fallbackSerde;\n\n  private Optional<SerdeInstance> findSerdeByPatternsOrDefault(String topic,\n                                                               Serde.Target type,\n                                                               Predicate<SerdeInstance> additionalCheck) {\n    // iterating over serdes in the same order they were added in config\n    for (SerdeInstance serdeInstance : serdes.values()) {\n      var pattern = type == Serde.Target.KEY\n          ? serdeInstance.topicKeyPattern\n          : serdeInstance.topicValuePattern;\n      if (pattern != null\n          && pattern.matcher(topic).matches()\n          && additionalCheck.test(serdeInstance)) {\n        return Optional.of(serdeInstance);\n      }\n    }\n    if (type == Serde.Target.KEY\n        && defaultKeySerde != null\n        && additionalCheck.test(defaultKeySerde)) {\n      return Optional.of(defaultKeySerde);\n    }\n    if (type == Serde.Target.VALUE\n        && defaultValueSerde != null\n        && additionalCheck.test(defaultValueSerde)) {\n      return Optional.of(defaultValueSerde);\n    }\n    return Optional.empty();\n  }\n\n  public Optional<SerdeInstance> serdeForName(String name) {\n    return Optional.ofNullable(serdes.get(name));\n  }\n\n  public Stream<SerdeInstance> all() {\n    return serdes.values().stream();\n  }\n\n  public SerdeInstance suggestSerdeForSerialize(String topic, Serde.Target type) {\n    return findSerdeByPatternsOrDefault(topic, type, s -> s.canSerialize(topic, type))\n        .orElse(serdes.get(StringSerde.name()));\n  }\n\n  public SerdeInstance suggestSerdeForDeserialize(String topic, Serde.Target type) {\n    return findSerdeByPatternsOrDefault(topic, type, s -> s.canDeserialize(topic, type))\n        .orElse(serdes.get(StringSerde.name()));\n  }\n\n  @Override\n  public void close() {\n    serdes.values().forEach(SerdeInstance::close);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ConsumerRecordDeserializer.java",
    "content": "package com.provectus.kafka.ui.serdes;\n\nimport com.provectus.kafka.ui.model.TopicMessageDTO;\nimport com.provectus.kafka.ui.model.TopicMessageDTO.TimestampTypeEnum;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport java.time.Instant;\nimport java.time.OffsetDateTime;\nimport java.time.ZoneId;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.UnaryOperator;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.kafka.clients.consumer.ConsumerRecord;\nimport org.apache.kafka.common.header.Header;\nimport org.apache.kafka.common.header.Headers;\nimport org.apache.kafka.common.record.TimestampType;\nimport org.apache.kafka.common.utils.Bytes;\n\n@Slf4j\n@RequiredArgsConstructor\npublic class ConsumerRecordDeserializer {\n\n  private static final ZoneId UTC_ZONE_ID = ZoneId.of(\"UTC\");\n\n  private final String keySerdeName;\n  private final Serde.Deserializer keyDeserializer;\n\n  private final String valueSerdeName;\n  private final Serde.Deserializer valueDeserializer;\n\n  private final String fallbackSerdeName;\n  private final Serde.Deserializer fallbackKeyDeserializer;\n  private final Serde.Deserializer fallbackValueDeserializer;\n\n  private final UnaryOperator<TopicMessageDTO> masker;\n\n  public TopicMessageDTO deserialize(ConsumerRecord<Bytes, Bytes> rec) {\n    var message = new TopicMessageDTO();\n    fillKey(message, rec);\n    fillValue(message, rec);\n    fillHeaders(message, rec);\n\n    message.setPartition(rec.partition());\n    message.setOffset(rec.offset());\n    message.setTimestampType(mapToTimestampType(rec.timestampType()));\n    message.setTimestamp(OffsetDateTime.ofInstant(Instant.ofEpochMilli(rec.timestamp()), UTC_ZONE_ID));\n\n    message.setKeySize(getKeySize(rec));\n    message.setValueSize(getValueSize(rec));\n    message.setHeadersSize(getHeadersSize(rec));\n\n    return masker.apply(message);\n  }\n\n  private static TimestampTypeEnum mapToTimestampType(TimestampType timestampType) {\n    return switch (timestampType) {\n      case CREATE_TIME -> TimestampTypeEnum.CREATE_TIME;\n      case LOG_APPEND_TIME -> TimestampTypeEnum.LOG_APPEND_TIME;\n      case NO_TIMESTAMP_TYPE -> TimestampTypeEnum.NO_TIMESTAMP_TYPE;\n    };\n  }\n\n  private void fillHeaders(TopicMessageDTO message, ConsumerRecord<Bytes, Bytes> rec) {\n    Map<String, String> headers = new HashMap<>();\n    rec.headers().iterator()\n        .forEachRemaining(header ->\n            headers.put(\n                header.key(),\n                header.value() != null ? new String(header.value()) : null\n            ));\n    message.setHeaders(headers);\n  }\n\n  private void fillKey(TopicMessageDTO message, ConsumerRecord<Bytes, Bytes> rec) {\n    if (rec.key() == null) {\n      return;\n    }\n    try {\n      var deserResult = keyDeserializer.deserialize(new RecordHeadersImpl(), rec.key().get());\n      message.setKey(deserResult.getResult());\n      message.setKeySerde(keySerdeName);\n      message.setKeyDeserializeProperties(deserResult.getAdditionalProperties());\n    } catch (Exception e) {\n      log.trace(\"Error deserializing key for key topic: {}, partition {}, offset {}, with serde {}\",\n          rec.topic(), rec.partition(), rec.offset(), keySerdeName, e);\n      var deserResult = fallbackKeyDeserializer.deserialize(new RecordHeadersImpl(), rec.key().get());\n      message.setKey(deserResult.getResult());\n      message.setKeySerde(fallbackSerdeName);\n    }\n  }\n\n  private void fillValue(TopicMessageDTO message, ConsumerRecord<Bytes, Bytes> rec) {\n    if (rec.value() == null) {\n      return;\n    }\n    try {\n      var deserResult = valueDeserializer.deserialize(\n          new RecordHeadersImpl(rec.headers()), rec.value().get());\n      message.setContent(deserResult.getResult());\n      message.setValueSerde(valueSerdeName);\n      message.setValueDeserializeProperties(deserResult.getAdditionalProperties());\n    } catch (Exception e) {\n      log.trace(\"Error deserializing key for value topic: {}, partition {}, offset {}, with serde {}\",\n          rec.topic(), rec.partition(), rec.offset(), valueSerdeName, e);\n      var deserResult = fallbackValueDeserializer.deserialize(\n          new RecordHeadersImpl(rec.headers()), rec.value().get());\n      message.setContent(deserResult.getResult());\n      message.setValueSerde(fallbackSerdeName);\n    }\n  }\n\n  private static Long getHeadersSize(ConsumerRecord<Bytes, Bytes> consumerRecord) {\n    Headers headers = consumerRecord.headers();\n    if (headers != null) {\n      return Arrays.stream(headers.toArray())\n          .mapToLong(ConsumerRecordDeserializer::headerSize)\n          .sum();\n    }\n    return 0L;\n  }\n\n  private static Long getKeySize(ConsumerRecord<Bytes, Bytes> consumerRecord) {\n    return consumerRecord.key() != null ? (long) consumerRecord.serializedKeySize() : null;\n  }\n\n  private static Long getValueSize(ConsumerRecord<Bytes, Bytes> consumerRecord) {\n    return consumerRecord.value() != null ? (long) consumerRecord.serializedValueSize() : null;\n  }\n\n  private static int headerSize(Header header) {\n    int key = header.key() != null ? header.key().getBytes().length : 0;\n    int val = header.value() != null ? header.value().length : 0;\n    return key + val;\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/CustomSerdeLoader.java",
    "content": "package com.provectus.kafka.ui.serdes;\n\nimport com.provectus.kafka.ui.serde.api.PropertyResolver;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport java.io.IOException;\nimport java.net.URL;\nimport java.net.URLClassLoader;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.security.AccessController;\nimport java.security.PrivilegedAction;\nimport java.util.ArrayList;\nimport java.util.Enumeration;\nimport java.util.Iterator;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.stream.Collectors;\nimport lombok.SneakyThrows;\nimport lombok.Value;\n\n\nclass CustomSerdeLoader {\n\n  @Value\n  static class CustomSerde {\n    Serde serde;\n    ClassLoader classLoader;\n  }\n\n  // serde location -> classloader\n  private final Map<Path, ClassLoader> classloaders = new ConcurrentHashMap<>();\n\n  @SneakyThrows\n  CustomSerde loadAndConfigure(String className,\n                               String filePath,\n                               PropertyResolver serdeProps,\n                               PropertyResolver clusterProps,\n                               PropertyResolver globalProps) {\n    Path locationPath = Path.of(filePath);\n    var serdeClassloader = createClassloader(locationPath);\n    var origCL = ClassloaderUtil.compareAndSwapLoaders(serdeClassloader);\n    try {\n      var serdeClass = serdeClassloader.loadClass(className);\n      var serde = (Serde) serdeClass.getDeclaredConstructor().newInstance();\n      serde.configure(serdeProps, clusterProps, globalProps);\n      return new CustomSerde(serde, serdeClassloader);\n    } finally {\n      ClassloaderUtil.compareAndSwapLoaders(origCL);\n    }\n  }\n\n  private static boolean isArchive(Path path) {\n    String archivePath = path.toString().toLowerCase();\n    return Files.isReadable(path)\n        && Files.isRegularFile(path)\n        && (archivePath.endsWith(\".jar\") || archivePath.endsWith(\".zip\"));\n  }\n\n  @SneakyThrows\n  private static List<URL> findArchiveFiles(Path location) {\n    if (isArchive(location)) {\n      return List.of(location.toUri().toURL());\n    }\n    if (Files.isDirectory(location)) {\n      List<URL> archiveFiles = new ArrayList<>();\n      try (var files = Files.walk(location)) {\n        var paths = files.filter(CustomSerdeLoader::isArchive).collect(Collectors.toList());\n        for (Path path : paths) {\n          archiveFiles.add(path.toUri().toURL());\n        }\n      }\n      return archiveFiles;\n    }\n    return List.of();\n  }\n\n  private ClassLoader createClassloader(Path location) {\n    if (!Files.exists(location)) {\n      throw new IllegalStateException(\"Location does not exist\");\n    }\n    var archives = findArchiveFiles(location);\n    if (archives.isEmpty()) {\n      throw new IllegalStateException(\"No archive files were found\");\n    }\n    // we assume that location's content does not change during serdes creation\n    // so, we can reuse already created classloaders\n    return classloaders.computeIfAbsent(location, l ->\n        AccessController.doPrivileged(\n            (PrivilegedAction<URLClassLoader>) () ->\n                new ChildFirstClassloader(\n                    archives.toArray(URL[]::new),\n                    CustomSerdeLoader.class.getClassLoader())));\n  }\n\n  //---------------------------------------------------------------------------------\n\n  // This Classloader first tries to load classes by itself. If class not fount\n  // search is propagated to parent (this is opposite to how usual classloaders work)\n  private static class ChildFirstClassloader extends URLClassLoader {\n\n    private static final String JAVA_PACKAGE_PREFIX = \"java.\";\n\n    ChildFirstClassloader(URL[] urls, ClassLoader parent) {\n      super(urls, parent);\n    }\n\n    @Override\n    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {\n      // first check whether it's a system class, delegate to the system loader\n      if (name.startsWith(JAVA_PACKAGE_PREFIX)) {\n        return findSystemClass(name);\n      }\n      Class<?> loadedClass = findLoadedClass(name);\n      if (loadedClass == null) {\n        try {\n          // start searching from current classloader\n          loadedClass = findClass(name);\n        } catch (ClassNotFoundException e) {\n          // if not found - going to parent\n          loadedClass = super.loadClass(name, resolve);\n        }\n      }\n      if (resolve) {\n        resolveClass(loadedClass);\n      }\n      return loadedClass;\n    }\n\n    @Override\n    public Enumeration<URL> getResources(String name) throws IOException {\n      List<URL> allRes = new LinkedList<>();\n      Enumeration<URL> thisRes = findResources(name);\n      if (thisRes != null) {\n        while (thisRes.hasMoreElements()) {\n          allRes.add(thisRes.nextElement());\n        }\n      }\n      // then try finding resources from parent classloaders\n      Enumeration<URL> parentRes = super.findResources(name);\n      if (parentRes != null) {\n        while (parentRes.hasMoreElements()) {\n          allRes.add(parentRes.nextElement());\n        }\n      }\n      return new Enumeration<>() {\n        final Iterator<URL> it = allRes.iterator();\n\n        @Override\n        public boolean hasMoreElements() {\n          return it.hasNext();\n        }\n\n        @Override\n        public URL nextElement() {\n          return it.next();\n        }\n      };\n    }\n\n    @Override\n    public URL getResource(String name) {\n      URL res = findResource(name);\n      if (res == null) {\n        res = super.getResource(name);\n      }\n      return res;\n    }\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ProducerRecordCreator.java",
    "content": "package com.provectus.kafka.ui.serdes;\n\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport java.util.Map;\nimport javax.annotation.Nullable;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.kafka.clients.producer.ProducerRecord;\nimport org.apache.kafka.common.header.Header;\nimport org.apache.kafka.common.header.internals.RecordHeader;\nimport org.apache.kafka.common.header.internals.RecordHeaders;\n\n@RequiredArgsConstructor\npublic class ProducerRecordCreator {\n\n  private final Serde.Serializer keySerializer;\n  private final Serde.Serializer valuesSerializer;\n\n  public ProducerRecord<byte[], byte[]> create(String topic,\n                                               @Nullable Integer partition,\n                                               @Nullable String key,\n                                               @Nullable String value,\n                                               @Nullable Map<String, String> headers) {\n    return new ProducerRecord<>(\n        topic,\n        partition,\n        key == null ? null : keySerializer.serialize(key),\n        value == null ? null : valuesSerializer.serialize(value),\n        headers == null ? null : createHeaders(headers)\n    );\n  }\n\n  private Iterable<Header> createHeaders(Map<String, String> clientHeaders) {\n    RecordHeaders headers = new RecordHeaders();\n    clientHeaders.forEach((k, v) -> headers.add(new RecordHeader(k, v.getBytes())));\n    return headers;\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/PropertyResolverImpl.java",
    "content": "package com.provectus.kafka.ui.serdes;\n\nimport com.google.common.base.Preconditions;\nimport com.provectus.kafka.ui.serde.api.PropertyResolver;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport javax.annotation.Nullable;\nimport org.springframework.boot.context.properties.bind.Bindable;\nimport org.springframework.boot.context.properties.bind.Binder;\nimport org.springframework.boot.context.properties.source.ConfigurationPropertyName;\nimport org.springframework.core.env.Environment;\nimport org.springframework.core.env.StandardEnvironment;\n\n\npublic class PropertyResolverImpl implements PropertyResolver {\n\n  private final Binder binder;\n\n  @Nullable\n  private final String prefix;\n\n  public static PropertyResolverImpl empty() {\n    return new PropertyResolverImpl(new StandardEnvironment(), null);\n  }\n\n  public PropertyResolverImpl(Environment env) {\n    this(env, null);\n  }\n\n  public PropertyResolverImpl(Environment env, @Nullable String prefix) {\n    this.binder = Binder.get(env);\n    this.prefix = prefix;\n  }\n\n  private ConfigurationPropertyName targetPropertyName(String key) {\n    Preconditions.checkNotNull(key);\n    Preconditions.checkState(!key.isBlank());\n    String propertyName = prefix == null ? key : prefix + \".\" + key;\n    return ConfigurationPropertyName.adapt(propertyName, '.');\n  }\n\n  @Override\n  public <T> Optional<T> getProperty(String key, Class<T> targetType) {\n    var targetKey = targetPropertyName(key);\n    var result = binder.bind(targetKey, Bindable.of(targetType));\n    return result.isBound() ? Optional.of(result.get()) : Optional.empty();\n  }\n\n  @Override\n  public <T> Optional<List<T>> getListProperty(String key, Class<T> itemType) {\n    var targetKey = targetPropertyName(key);\n    var listResult = binder.bind(targetKey, Bindable.listOf(itemType));\n    return listResult.isBound() ? Optional.of(listResult.get()) : Optional.empty();\n  }\n\n  @Override\n  public <K, V> Optional<Map<K, V>> getMapProperty(String key, Class<K> keyType, Class<V> valueType) {\n    var targetKey = targetPropertyName(key);\n    var mapResult = binder.bind(targetKey, Bindable.mapOf(keyType, valueType));\n    return mapResult.isBound() ? Optional.of(mapResult.get()) : Optional.empty();\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/RecordHeaderImpl.java",
    "content": "package com.provectus.kafka.ui.serdes;\n\nimport com.provectus.kafka.ui.serde.api.RecordHeader;\nimport org.apache.kafka.common.header.Header;\n\npublic class RecordHeaderImpl implements RecordHeader  {\n\n  private final Header header;\n\n  public RecordHeaderImpl(Header header) {\n    this.header = header;\n  }\n\n  @Override\n  public String key() {\n    return header.key();\n  }\n\n  @Override\n  public byte[] value() {\n    return header.value();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/RecordHeadersImpl.java",
    "content": "package com.provectus.kafka.ui.serdes;\n\nimport com.google.common.collect.Iterators;\nimport com.provectus.kafka.ui.serde.api.RecordHeader;\nimport com.provectus.kafka.ui.serde.api.RecordHeaders;\nimport java.util.Iterator;\nimport org.apache.kafka.common.header.Headers;\n\n\npublic class RecordHeadersImpl implements RecordHeaders {\n\n  private final Headers headers;\n\n  public RecordHeadersImpl() {\n    this(new org.apache.kafka.common.header.internals.RecordHeaders());\n  }\n\n  public RecordHeadersImpl(Headers headers) {\n    this.headers = headers;\n  }\n\n  @Override\n  public Iterator<RecordHeader> iterator() {\n    return Iterators.transform(headers.iterator(), RecordHeaderImpl::new);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/SerdeInstance.java",
    "content": "package com.provectus.kafka.ui.serdes;\n\nimport com.provectus.kafka.ui.serde.api.SchemaDescription;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport java.io.Closeable;\nimport java.util.Optional;\nimport java.util.function.Supplier;\nimport java.util.regex.Pattern;\nimport javax.annotation.Nullable;\nimport lombok.Getter;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\n\n@Slf4j\n@RequiredArgsConstructor\npublic class SerdeInstance implements Closeable {\n\n  @Getter\n  final String name;\n\n  final Serde serde;\n\n  @Nullable\n  final Pattern topicKeyPattern;\n\n  @Nullable\n  final Pattern topicValuePattern;\n\n  @Nullable // will be set for custom serdes\n  final ClassLoader classLoader;\n\n  private <T> T wrapWithClassloader(Supplier<T> call) {\n    if (classLoader == null) {\n      return call.get();\n    }\n    var origCl = ClassloaderUtil.compareAndSwapLoaders(classLoader);\n    try {\n      return call.get();\n    } finally {\n      ClassloaderUtil.compareAndSwapLoaders(origCl);\n    }\n  }\n\n  public Optional<SchemaDescription> getSchema(String topic, Serde.Target type) {\n    try {\n      return wrapWithClassloader(() -> serde.getSchema(topic, type));\n    } catch (Exception e) {\n      log.warn(\"Error getting schema for '{}'({}) with serde '{}'\", topic, type, name, e);\n      return Optional.empty();\n    }\n  }\n\n  public Optional<String> description() {\n    try {\n      return wrapWithClassloader(serde::getDescription);\n    } catch (Exception e) {\n      log.warn(\"Error getting description serde '{}'\", name, e);\n      return Optional.empty();\n    }\n  }\n\n  public boolean canSerialize(String topic, Serde.Target type) {\n    try {\n      return wrapWithClassloader(() -> serde.canSerialize(topic, type));\n    } catch (Exception e) {\n      log.warn(\"Error calling canSerialize for '{}'({}) with serde '{}'\", topic, type, name, e);\n      return false;\n    }\n  }\n\n  public boolean canDeserialize(String topic, Serde.Target type) {\n    try {\n      return wrapWithClassloader(() -> serde.canDeserialize(topic, type));\n    } catch (Exception e) {\n      log.warn(\"Error calling canDeserialize for '{}'({}) with serde '{}'\", topic, type, name, e);\n      return false;\n    }\n  }\n\n  public Serde.Serializer serializer(String topic, Serde.Target type) {\n    return wrapWithClassloader(() -> {\n      var serializer = serde.serializer(topic, type);\n      return input -> wrapWithClassloader(() -> serializer.serialize(input));\n    });\n  }\n\n  public Serde.Deserializer deserializer(String topic, Serde.Target type) {\n    return wrapWithClassloader(() -> {\n      var deserializer = serde.deserializer(topic, type);\n      return (headers, data) -> wrapWithClassloader(() -> deserializer.deserialize(headers, data));\n    });\n  }\n\n  @Override\n  public void close() {\n    wrapWithClassloader(() -> {\n      try {\n        serde.close();\n      } catch (Exception e) {\n        log.error(\"Error closing serde \" + name, e);\n      }\n      return null;\n    });\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/SerdesInitializer.java",
    "content": "package com.provectus.kafka.ui.serdes;\n\nimport com.google.common.annotations.VisibleForTesting;\nimport com.google.common.base.Preconditions;\nimport com.google.common.base.Strings;\nimport com.google.common.collect.ImmutableMap;\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.config.ClustersProperties.SerdeConfig;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.serde.api.PropertyResolver;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport com.provectus.kafka.ui.serdes.builtin.AvroEmbeddedSerde;\nimport com.provectus.kafka.ui.serdes.builtin.Base64Serde;\nimport com.provectus.kafka.ui.serdes.builtin.ConsumerOffsetsSerde;\nimport com.provectus.kafka.ui.serdes.builtin.HexSerde;\nimport com.provectus.kafka.ui.serdes.builtin.Int32Serde;\nimport com.provectus.kafka.ui.serdes.builtin.Int64Serde;\nimport com.provectus.kafka.ui.serdes.builtin.ProtobufFileSerde;\nimport com.provectus.kafka.ui.serdes.builtin.ProtobufRawSerde;\nimport com.provectus.kafka.ui.serdes.builtin.StringSerde;\nimport com.provectus.kafka.ui.serdes.builtin.UInt32Serde;\nimport com.provectus.kafka.ui.serdes.builtin.UInt64Serde;\nimport com.provectus.kafka.ui.serdes.builtin.UuidBinarySerde;\nimport com.provectus.kafka.ui.serdes.builtin.sr.SchemaRegistrySerde;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.regex.Pattern;\nimport javax.annotation.Nullable;\nimport lombok.SneakyThrows;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.core.env.Environment;\n\n@Slf4j\npublic class SerdesInitializer {\n\n  private final Map<String, Class<? extends BuiltInSerde>> builtInSerdeClasses;\n  private final CustomSerdeLoader customSerdeLoader;\n\n  public SerdesInitializer() {\n    this(\n        ImmutableMap.<String, Class<? extends BuiltInSerde>>builder()\n            .put(StringSerde.name(), StringSerde.class)\n            .put(SchemaRegistrySerde.name(), SchemaRegistrySerde.class)\n            .put(ProtobufFileSerde.name(), ProtobufFileSerde.class)\n            .put(Int32Serde.name(), Int32Serde.class)\n            .put(Int64Serde.name(), Int64Serde.class)\n            .put(UInt32Serde.name(), UInt32Serde.class)\n            .put(UInt64Serde.name(), UInt64Serde.class)\n            .put(AvroEmbeddedSerde.name(), AvroEmbeddedSerde.class)\n            .put(Base64Serde.name(), Base64Serde.class)\n            .put(HexSerde.name(), HexSerde.class)\n            .put(UuidBinarySerde.name(), UuidBinarySerde.class)\n            .put(ProtobufRawSerde.name(), ProtobufRawSerde.class)\n            .build(),\n        new CustomSerdeLoader()\n    );\n  }\n\n  @VisibleForTesting\n  SerdesInitializer(Map<String, Class<? extends BuiltInSerde>> builtInSerdeClasses,\n                    CustomSerdeLoader customSerdeLoader) {\n    this.builtInSerdeClasses = builtInSerdeClasses;\n    this.customSerdeLoader = customSerdeLoader;\n  }\n\n  /**\n   * Initialization algorithm:\n   * First, we iterate over explicitly configured serdes from cluster config:\n   * > if serde has name = one of built-in serde's names:\n   * - if serde's properties are empty, we treat it as serde should be\n   * auto-configured - we try to do that\n   * - if serde's properties not empty, we treat it as an intention to\n   * override default configuration, so we configuring it with specific config (calling configure(..))\n   * <p/>\n   * > if serde has className = one of built-in serde's classes:\n   * - initializing it with specific config and with default classloader\n   * <p/>\n   * > if serde has custom className != one of built-in serde's classes:\n   * - initializing it with specific config and with custom classloader (see CustomSerdeLoader)\n   * <p/>\n   * Second, we iterate over remaining built-in serdes (that we NOT explicitly configured by config)\n   * trying to auto-configure them and  registering with empty patterns - they will be present\n   * in Serde selection in UI, but not assigned to any topic k/v.\n   */\n  public ClusterSerdes init(Environment env,\n                            ClustersProperties clustersProperties,\n                            int clusterIndex) {\n    ClustersProperties.Cluster clusterProperties = clustersProperties.getClusters().get(clusterIndex);\n    log.debug(\"Configuring serdes for cluster {}\", clusterProperties.getName());\n\n    var globalPropertiesResolver = new PropertyResolverImpl(env);\n    var clusterPropertiesResolver = new PropertyResolverImpl(env, \"kafka.clusters.\" + clusterIndex);\n\n    Map<String, SerdeInstance> registeredSerdes = new LinkedHashMap<>();\n    // initializing serdes from config\n    if (clusterProperties.getSerde() != null) {\n      for (int i = 0; i < clusterProperties.getSerde().size(); i++) {\n        SerdeConfig serdeConfig = clusterProperties.getSerde().get(i);\n        if (Strings.isNullOrEmpty(serdeConfig.getName())) {\n          throw new ValidationException(\"'name' property not set for serde: \" + serdeConfig);\n        }\n        if (registeredSerdes.containsKey(serdeConfig.getName())) {\n          throw new ValidationException(\"Multiple serdes with same name: \" + serdeConfig.getName());\n        }\n        var instance = createSerdeFromConfig(\n            serdeConfig,\n            new PropertyResolverImpl(env, \"kafka.clusters.\" + clusterIndex + \".serde.\" + i + \".properties\"),\n            clusterPropertiesResolver,\n            globalPropertiesResolver\n        );\n        registeredSerdes.put(serdeConfig.getName(), instance);\n      }\n    }\n\n    // initializing remaining built-in serdes with empty selection patters\n    builtInSerdeClasses.forEach((name, clazz) -> {\n      if (!registeredSerdes.containsKey(name)) {\n        BuiltInSerde serde = createSerdeInstance(clazz);\n        if (autoConfigureSerde(serde, clusterPropertiesResolver, globalPropertiesResolver)) {\n          registeredSerdes.put(name, new SerdeInstance(name, serde, null, null, null));\n        }\n      }\n    });\n\n    registerTopicRelatedSerde(registeredSerdes);\n\n    return new ClusterSerdes(\n        registeredSerdes,\n        Optional.ofNullable(clusterProperties.getDefaultKeySerde())\n            .map(name -> Preconditions.checkNotNull(registeredSerdes.get(name), \"Default key serde not found\"))\n            .orElse(null),\n        Optional.ofNullable(clusterProperties.getDefaultValueSerde())\n            .map(name -> Preconditions.checkNotNull(registeredSerdes.get(name), \"Default value serde not found\"))\n            .or(() -> Optional.ofNullable(registeredSerdes.get(SchemaRegistrySerde.name())))\n            .or(() -> Optional.ofNullable(registeredSerdes.get(ProtobufFileSerde.name())))\n            .orElse(null),\n        createFallbackSerde()\n    );\n  }\n\n  /**\n   * Registers serdse that should only be used for specific (hard-coded) topics, like ConsumerOffsetsSerde.\n   */\n  private void registerTopicRelatedSerde(Map<String, SerdeInstance> serdes) {\n    registerConsumerOffsetsSerde(serdes);\n  }\n\n  private void registerConsumerOffsetsSerde(Map<String, SerdeInstance> serdes) {\n    var pattern = Pattern.compile(ConsumerOffsetsSerde.TOPIC);\n    serdes.put(\n        ConsumerOffsetsSerde.name(),\n        new SerdeInstance(\n            ConsumerOffsetsSerde.name(),\n            new ConsumerOffsetsSerde(),\n            pattern,\n            pattern,\n            null\n        )\n    );\n  }\n\n  private SerdeInstance createFallbackSerde() {\n    StringSerde serde = new StringSerde();\n    serde.configure(PropertyResolverImpl.empty(), PropertyResolverImpl.empty(), PropertyResolverImpl.empty());\n    return new SerdeInstance(\"Fallback\", serde, null, null, null);\n  }\n\n  @SneakyThrows\n  private SerdeInstance createSerdeFromConfig(SerdeConfig serdeConfig,\n                                              PropertyResolver serdeProps,\n                                              PropertyResolver clusterProps,\n                                              PropertyResolver globalProps) {\n    if (builtInSerdeClasses.containsKey(serdeConfig.getName())) {\n      return createSerdeWithBuiltInSerdeName(serdeConfig, serdeProps, clusterProps, globalProps);\n    }\n    if (serdeConfig.getClassName() != null) {\n      var builtInSerdeClass = builtInSerdeClasses.values().stream()\n          .filter(c -> c.getName().equals(serdeConfig.getClassName()))\n          .findAny();\n      // built-in serde type with custom name\n      if (builtInSerdeClass.isPresent()) {\n        return createSerdeWithBuiltInClass(builtInSerdeClass.get(), serdeConfig, serdeProps, clusterProps, globalProps);\n      }\n    }\n    log.info(\"Loading custom serde {}\", serdeConfig.getName());\n    return loadAndInitCustomSerde(serdeConfig, serdeProps, clusterProps, globalProps);\n  }\n\n  private SerdeInstance createSerdeWithBuiltInSerdeName(SerdeConfig serdeConfig,\n                                                        PropertyResolver serdeProps,\n                                                        PropertyResolver clusterProps,\n                                                        PropertyResolver globalProps) {\n    String name = serdeConfig.getName();\n    if (serdeConfig.getClassName() != null) {\n      throw new ValidationException(\"className can't be set for built-in serde\");\n    }\n    if (serdeConfig.getFilePath() != null) {\n      throw new ValidationException(\"filePath can't be set for built-in serde types\");\n    }\n    var clazz = builtInSerdeClasses.get(name);\n    BuiltInSerde serde = createSerdeInstance(clazz);\n    if (serdeConfig.getProperties() == null || serdeConfig.getProperties().isEmpty()) {\n      if (!autoConfigureSerde(serde, clusterProps, globalProps)) {\n        // no properties provided and serde does not support auto-configuration\n        throw new ValidationException(name + \" serde is not configured\");\n      }\n    } else {\n      // configuring serde with explicitly set properties\n      serde.configure(serdeProps, clusterProps, globalProps);\n    }\n    return new SerdeInstance(\n        name,\n        serde,\n        nullablePattern(serdeConfig.getTopicKeysPattern()),\n        nullablePattern(serdeConfig.getTopicValuesPattern()),\n        null\n    );\n  }\n\n  private boolean autoConfigureSerde(BuiltInSerde serde, PropertyResolver clusterProps, PropertyResolver globalProps) {\n    if (serde.canBeAutoConfigured(clusterProps, globalProps)) {\n      serde.autoConfigure(clusterProps, globalProps);\n      return true;\n    }\n    return false;\n  }\n\n  @SneakyThrows\n  private SerdeInstance createSerdeWithBuiltInClass(Class<? extends BuiltInSerde> clazz,\n                                                    SerdeConfig serdeConfig,\n                                                    PropertyResolver serdeProps,\n                                                    PropertyResolver clusterProps,\n                                                    PropertyResolver globalProps) {\n    if (serdeConfig.getFilePath() != null) {\n      throw new ValidationException(\"filePath can't be set for built-in serde type\");\n    }\n    BuiltInSerde serde = createSerdeInstance(clazz);\n    serde.configure(serdeProps, clusterProps, globalProps);\n    return new SerdeInstance(\n        serdeConfig.getName(),\n        serde,\n        nullablePattern(serdeConfig.getTopicKeysPattern()),\n        nullablePattern(serdeConfig.getTopicValuesPattern()),\n        null\n    );\n  }\n\n  @SneakyThrows\n  private <T extends Serde> T createSerdeInstance(Class<T> clazz) {\n    return clazz.getDeclaredConstructor().newInstance();\n  }\n\n  private SerdeInstance loadAndInitCustomSerde(SerdeConfig serdeConfig,\n                                               PropertyResolver serdeProps,\n                                               PropertyResolver clusterProps,\n                                               PropertyResolver globalProps) {\n    if (Strings.isNullOrEmpty(serdeConfig.getClassName())) {\n      throw new ValidationException(\n          \"'className' property not set for custom serde \" + serdeConfig.getName());\n    }\n    if (Strings.isNullOrEmpty(serdeConfig.getFilePath())) {\n      throw new ValidationException(\n          \"'filePath' property not set for custom serde \" + serdeConfig.getName());\n    }\n    var loaded = customSerdeLoader.loadAndConfigure(\n        serdeConfig.getClassName(), serdeConfig.getFilePath(), serdeProps, clusterProps, globalProps);\n    return new SerdeInstance(\n        serdeConfig.getName(),\n        loaded.getSerde(),\n        nullablePattern(serdeConfig.getTopicKeysPattern()),\n        nullablePattern(serdeConfig.getTopicValuesPattern()),\n        loaded.getClassLoader()\n    );\n  }\n\n  @Nullable\n  private Pattern nullablePattern(@Nullable String pattern) {\n    return pattern == null ? null : Pattern.compile(pattern);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/AvroEmbeddedSerde.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.PropertyResolver;\nimport com.provectus.kafka.ui.serde.api.RecordHeaders;\nimport com.provectus.kafka.ui.serde.api.SchemaDescription;\nimport com.provectus.kafka.ui.serdes.BuiltInSerde;\nimport io.confluent.kafka.schemaregistry.avro.AvroSchemaUtils;\nimport java.util.Map;\nimport java.util.Optional;\nimport lombok.SneakyThrows;\nimport org.apache.avro.file.DataFileReader;\nimport org.apache.avro.file.SeekableByteArrayInput;\nimport org.apache.avro.generic.GenericDatumReader;\n\npublic class AvroEmbeddedSerde implements BuiltInSerde {\n\n  public static String name() {\n    return \"Avro (Embedded)\";\n  }\n\n  @Override\n  public Optional<String> getDescription() {\n    return Optional.empty();\n  }\n\n  @Override\n  public Optional<SchemaDescription> getSchema(String topic, Target type) {\n    return Optional.empty();\n  }\n\n  @Override\n  public boolean canDeserialize(String topic, Target type) {\n    return true;\n  }\n\n  @Override\n  public boolean canSerialize(String topic, Target type) {\n    return false;\n  }\n\n  @Override\n  public Serializer serializer(String topic, Target type) {\n    throw new IllegalStateException();\n  }\n\n  @Override\n  public Deserializer deserializer(String topic, Target type) {\n    return new Deserializer() {\n      @SneakyThrows\n      @Override\n      public DeserializeResult deserialize(RecordHeaders headers, byte[] data) {\n        try (var reader = new DataFileReader<>(new SeekableByteArrayInput(data), new GenericDatumReader<>())) {\n          if (!reader.hasNext()) {\n            // this is very strange situation, when only header present in payload\n            // returning null in this case\n            return new DeserializeResult(null, DeserializeResult.Type.JSON, Map.of());\n          }\n          Object avroObj = reader.next();\n          String jsonValue = new String(AvroSchemaUtils.toJson(avroObj));\n          return new DeserializeResult(jsonValue, DeserializeResult.Type.JSON, Map.of());\n        }\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/Base64Serde.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.SchemaDescription;\nimport com.provectus.kafka.ui.serdes.BuiltInSerde;\nimport java.util.Base64;\nimport java.util.Map;\nimport java.util.Optional;\n\npublic class Base64Serde implements BuiltInSerde {\n\n  public static String name() {\n    return \"Base64\";\n  }\n\n  @Override\n  public Optional<String> getDescription() {\n    return Optional.empty();\n  }\n\n  @Override\n  public Optional<SchemaDescription> getSchema(String topic, Target type) {\n    return Optional.empty();\n  }\n\n  @Override\n  public boolean canDeserialize(String topic, Target type) {\n    return true;\n  }\n\n  @Override\n  public boolean canSerialize(String topic, Target type) {\n    return true;\n  }\n\n  @Override\n  public Serializer serializer(String topic, Target type) {\n    var decoder = Base64.getDecoder();\n    return inputString -> {\n      inputString = inputString.trim();\n      // it is actually a hack to provide ability to sent empty array as a key/value\n      if (inputString.length() == 0) {\n        return new byte[] {};\n      }\n      return decoder.decode(inputString);\n    };\n  }\n\n  @Override\n  public Deserializer deserializer(String topic, Target type) {\n    var encoder = Base64.getEncoder();\n    return (headers, data) ->\n        new DeserializeResult(\n            encoder.encodeToString(data),\n            DeserializeResult.Type.STRING,\n            Map.of()\n        );\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/ConsumerOffsetsSerde.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.JsonSerializer;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport com.fasterxml.jackson.databind.module.SimpleModule;\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.SchemaDescription;\nimport com.provectus.kafka.ui.serdes.BuiltInSerde;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.util.Map;\nimport java.util.Optional;\nimport lombok.SneakyThrows;\nimport org.apache.kafka.common.protocol.types.ArrayOf;\nimport org.apache.kafka.common.protocol.types.BoundField;\nimport org.apache.kafka.common.protocol.types.CompactArrayOf;\nimport org.apache.kafka.common.protocol.types.Field;\nimport org.apache.kafka.common.protocol.types.Schema;\nimport org.apache.kafka.common.protocol.types.Struct;\nimport org.apache.kafka.common.protocol.types.Type;\n\n// Deserialization logic and message's schemas can be found in\n// kafka.coordinator.group.GroupMetadataManager (readMessageKey, readOffsetMessageValue, readGroupMessageValue)\npublic class ConsumerOffsetsSerde implements BuiltInSerde {\n\n  private static final JsonMapper JSON_MAPPER = createMapper();\n\n  private static final String ASSIGNMENT = \"assignment\";\n  private static final String CLIENT_HOST = \"client_host\";\n  private static final String CLIENT_ID = \"client_id\";\n  private static final String COMMIT_TIMESTAMP = \"commit_timestamp\";\n  private static final String CURRENT_STATE_TIMESTAMP = \"current_state_timestamp\";\n  private static final String GENERATION = \"generation\";\n  private static final String LEADER = \"leader\";\n  private static final String MEMBERS = \"members\";\n  private static final String MEMBER_ID = \"member_id\";\n  private static final String METADATA = \"metadata\";\n  private static final String OFFSET = \"offset\";\n  private static final String PROTOCOL = \"protocol\";\n  private static final String PROTOCOL_TYPE = \"protocol_type\";\n  private static final String REBALANCE_TIMEOUT = \"rebalance_timeout\";\n  private static final String SESSION_TIMEOUT = \"session_timeout\";\n  private static final String SUBSCRIPTION = \"subscription\";\n\n  public static final String TOPIC = \"__consumer_offsets\";\n\n  public static String name() {\n    return \"__consumer_offsets\";\n  }\n\n  private static JsonMapper createMapper() {\n    var module = new SimpleModule();\n    module.addSerializer(Struct.class, new JsonSerializer<>() {\n      @Override\n      public void serialize(Struct value, JsonGenerator gen, SerializerProvider serializers) throws IOException {\n        gen.writeStartObject();\n        for (BoundField field : value.schema().fields()) {\n          var fieldVal = value.get(field);\n          gen.writeObjectField(field.def.name, fieldVal);\n        }\n        gen.writeEndObject();\n      }\n    });\n    var mapper = new JsonMapper();\n    mapper.registerModule(module);\n    return mapper;\n  }\n\n  @Override\n  public Optional<String> getDescription() {\n    return Optional.empty();\n  }\n\n  @Override\n  public Optional<SchemaDescription> getSchema(String topic, Target type) {\n    return Optional.empty();\n  }\n\n  @Override\n  public boolean canDeserialize(String topic, Target type) {\n    return topic.equals(TOPIC);\n  }\n\n  @Override\n  public boolean canSerialize(String topic, Target type) {\n    return false;\n  }\n\n  @Override\n  public Serializer serializer(String topic, Target type) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public Deserializer deserializer(String topic, Target type) {\n    return switch (type) {\n      case KEY -> keyDeserializer();\n      case VALUE -> valueDeserializer();\n    };\n  }\n\n  private Deserializer keyDeserializer() {\n    final Schema commitKeySchema = new Schema(\n        new Field(\"group\", Type.STRING, \"\"),\n        new Field(\"topic\", Type.STRING, \"\"),\n        new Field(\"partition\", Type.INT32, \"\")\n    );\n\n    final Schema groupMetadataSchema = new Schema(\n        new Field(\"group\", Type.STRING, \"\")\n    );\n\n    return (headers, data) -> {\n      var bb = ByteBuffer.wrap(data);\n      short version = bb.getShort();\n      return new DeserializeResult(\n          toJson(\n              switch (version) {\n                case 0, 1 -> commitKeySchema.read(bb);\n                case 2 -> groupMetadataSchema.read(bb);\n                default -> throw new IllegalStateException(\"Unknown group metadata message version: \" + version);\n              }\n          ),\n          DeserializeResult.Type.JSON,\n          Map.of()\n      );\n    };\n  }\n\n  private Deserializer valueDeserializer() {\n    final Schema commitOffsetSchemaV0 =\n        new Schema(\n            new Field(OFFSET, Type.INT64, \"\"),\n            new Field(METADATA, Type.STRING, \"\"),\n            new Field(COMMIT_TIMESTAMP, Type.INT64, \"\")\n        );\n\n    final Schema commitOffsetSchemaV1 =\n        new Schema(\n            new Field(OFFSET, Type.INT64, \"\"),\n            new Field(METADATA, Type.STRING, \"\"),\n            new Field(COMMIT_TIMESTAMP, Type.INT64, \"\"),\n            new Field(\"expire_timestamp\", Type.INT64, \"\")\n        );\n\n    final Schema commitOffsetSchemaV2 =\n        new Schema(\n            new Field(OFFSET, Type.INT64, \"\"),\n            new Field(METADATA, Type.STRING, \"\"),\n            new Field(COMMIT_TIMESTAMP, Type.INT64, \"\")\n        );\n\n    final Schema commitOffsetSchemaV3 =\n        new Schema(\n            new Field(OFFSET, Type.INT64, \"\"),\n            new Field(\"leader_epoch\", Type.INT32, \"\"),\n            new Field(METADATA, Type.STRING, \"\"),\n            new Field(COMMIT_TIMESTAMP, Type.INT64, \"\")\n        );\n\n    final Schema commitOffsetSchemaV4 = new Schema(\n        new Field(OFFSET, Type.INT64, \"\"),\n        new Field(\"leader_epoch\", Type.INT32, \"\"),\n        new Field(METADATA, Type.COMPACT_STRING, \"\"),\n        new Field(COMMIT_TIMESTAMP, Type.INT64, \"\"),\n        Field.TaggedFieldsSection.of()\n    );\n\n    final Schema metadataSchema0 =\n        new Schema(\n            new Field(PROTOCOL_TYPE, Type.STRING, \"\"),\n            new Field(GENERATION, Type.INT32, \"\"),\n            new Field(PROTOCOL, Type.NULLABLE_STRING, \"\"),\n            new Field(LEADER, Type.NULLABLE_STRING, \"\"),\n            new Field(MEMBERS, new ArrayOf(new Schema(\n                new Field(MEMBER_ID, Type.STRING, \"\"),\n                new Field(CLIENT_ID, Type.STRING, \"\"),\n                new Field(CLIENT_HOST, Type.STRING, \"\"),\n                new Field(SESSION_TIMEOUT, Type.INT32, \"\"),\n                new Field(SUBSCRIPTION, Type.BYTES, \"\"),\n                new Field(ASSIGNMENT, Type.BYTES, \"\")\n            )), \"\")\n        );\n\n    final Schema metadataSchema1 =\n        new Schema(\n            new Field(PROTOCOL_TYPE, Type.STRING, \"\"),\n            new Field(GENERATION, Type.INT32, \"\"),\n            new Field(PROTOCOL, Type.NULLABLE_STRING, \"\"),\n            new Field(LEADER, Type.NULLABLE_STRING, \"\"),\n            new Field(MEMBERS, new ArrayOf(new Schema(\n                new Field(MEMBER_ID, Type.STRING, \"\"),\n                new Field(CLIENT_ID, Type.STRING, \"\"),\n                new Field(CLIENT_HOST, Type.STRING, \"\"),\n                new Field(REBALANCE_TIMEOUT, Type.INT32, \"\"),\n                new Field(SESSION_TIMEOUT, Type.INT32, \"\"),\n                new Field(SUBSCRIPTION, Type.BYTES, \"\"),\n                new Field(ASSIGNMENT, Type.BYTES, \"\")\n            )), \"\")\n        );\n\n    final Schema metadataSchema2 =\n        new Schema(\n            new Field(PROTOCOL_TYPE, Type.STRING, \"\"),\n            new Field(GENERATION, Type.INT32, \"\"),\n            new Field(PROTOCOL, Type.NULLABLE_STRING, \"\"),\n            new Field(LEADER, Type.NULLABLE_STRING, \"\"),\n            new Field(CURRENT_STATE_TIMESTAMP, Type.INT64, \"\"),\n            new Field(MEMBERS, new ArrayOf(new Schema(\n                new Field(MEMBER_ID, Type.STRING, \"\"),\n                new Field(CLIENT_ID, Type.STRING, \"\"),\n                new Field(CLIENT_HOST, Type.STRING, \"\"),\n                new Field(REBALANCE_TIMEOUT, Type.INT32, \"\"),\n                new Field(SESSION_TIMEOUT, Type.INT32, \"\"),\n                new Field(SUBSCRIPTION, Type.BYTES, \"\"),\n                new Field(ASSIGNMENT, Type.BYTES, \"\")\n            )), \"\")\n        );\n\n    final Schema metadataSchema3 =\n        new Schema(\n            new Field(PROTOCOL_TYPE, Type.STRING, \"\"),\n            new Field(GENERATION, Type.INT32, \"\"),\n            new Field(PROTOCOL, Type.NULLABLE_STRING, \"\"),\n            new Field(LEADER, Type.NULLABLE_STRING, \"\"),\n            new Field(CURRENT_STATE_TIMESTAMP, Type.INT64, \"\"),\n            new Field(MEMBERS, new ArrayOf(new Schema(\n                new Field(MEMBER_ID, Type.STRING, \"\"),\n                new Field(\"group_instance_id\", Type.NULLABLE_STRING, \"\"),\n                new Field(CLIENT_ID, Type.STRING, \"\"),\n                new Field(CLIENT_HOST, Type.STRING, \"\"),\n                new Field(REBALANCE_TIMEOUT, Type.INT32, \"\"),\n                new Field(SESSION_TIMEOUT, Type.INT32, \"\"),\n                new Field(SUBSCRIPTION, Type.BYTES, \"\"),\n                new Field(ASSIGNMENT, Type.BYTES, \"\")\n            )), \"\")\n        );\n\n    final Schema metadataSchema4 =\n        new Schema(\n            new Field(PROTOCOL_TYPE, Type.COMPACT_STRING, \"\"),\n            new Field(GENERATION, Type.INT32, \"\"),\n            new Field(PROTOCOL, Type.COMPACT_NULLABLE_STRING, \"\"),\n            new Field(LEADER, Type.COMPACT_NULLABLE_STRING, \"\"),\n            new Field(CURRENT_STATE_TIMESTAMP, Type.INT64, \"\"),\n            new Field(MEMBERS, new CompactArrayOf(new Schema(\n                new Field(MEMBER_ID, Type.COMPACT_STRING, \"\"),\n                new Field(\"group_instance_id\", Type.COMPACT_NULLABLE_STRING, \"\"),\n                new Field(CLIENT_ID, Type.COMPACT_STRING, \"\"),\n                new Field(CLIENT_HOST, Type.COMPACT_STRING, \"\"),\n                new Field(REBALANCE_TIMEOUT, Type.INT32, \"\"),\n                new Field(SESSION_TIMEOUT, Type.INT32, \"\"),\n                new Field(SUBSCRIPTION, Type.COMPACT_BYTES, \"\"),\n                new Field(ASSIGNMENT, Type.COMPACT_BYTES, \"\"),\n                Field.TaggedFieldsSection.of()\n            )), \"\"),\n            Field.TaggedFieldsSection.of()\n        );\n\n    return (headers, data) -> {\n      String result;\n      var bb = ByteBuffer.wrap(data);\n      short version = bb.getShort();\n      // ideally, we should distinguish if value is commit or metadata\n      // by checking record's key, but our current serde structure doesn't allow that.\n      // so, we are trying to parse into metadata first and after into commit msg\n      try {\n        result = toJson(\n            switch (version) {\n              case 0 -> metadataSchema0.read(bb);\n              case 1 -> metadataSchema1.read(bb);\n              case 2 -> metadataSchema2.read(bb);\n              case 3 -> metadataSchema3.read(bb);\n              case 4 -> metadataSchema4.read(bb);\n              default -> throw new IllegalArgumentException(\"Unrecognized version: \" + version);\n            }\n        );\n      } catch (Throwable e) {\n        bb = bb.rewind();\n        bb.getShort(); // skipping version\n        result = toJson(\n            switch (version) {\n              case 0 -> commitOffsetSchemaV0.read(bb);\n              case 1 -> commitOffsetSchemaV1.read(bb);\n              case 2 -> commitOffsetSchemaV2.read(bb);\n              case 3 -> commitOffsetSchemaV3.read(bb);\n              case 4 -> commitOffsetSchemaV4.read(bb);\n              default -> throw new IllegalArgumentException(\"Unrecognized version: \" + version);\n            }\n        );\n      }\n\n      if (bb.remaining() != 0) {\n        throw new IllegalArgumentException(\n            \"Message buffer is not read to the end, which is likely means message is unrecognized\");\n      }\n      return new DeserializeResult(\n          result,\n          DeserializeResult.Type.JSON,\n          Map.of()\n      );\n    };\n  }\n\n  @SneakyThrows\n  private String toJson(Struct s) {\n    return JSON_MAPPER.writeValueAsString(s);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/HexSerde.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.PropertyResolver;\nimport com.provectus.kafka.ui.serde.api.SchemaDescription;\nimport com.provectus.kafka.ui.serdes.BuiltInSerde;\nimport java.util.HexFormat;\nimport java.util.Map;\nimport java.util.Optional;\n\npublic class HexSerde implements BuiltInSerde {\n\n  private HexFormat deserializeHexFormat;\n\n  public static String name() {\n    return \"Hex\";\n  }\n\n  @Override\n  public void autoConfigure(PropertyResolver kafkaClusterProperties, PropertyResolver globalProperties) {\n    configure(\" \", true);\n  }\n\n  @Override\n  public void configure(PropertyResolver serdeProperties,\n                        PropertyResolver kafkaClusterProperties,\n                        PropertyResolver globalProperties) {\n    String delim = serdeProperties.getProperty(\"delimiter\", String.class).orElse(\" \");\n    boolean uppercase = serdeProperties.getProperty(\"uppercase\", Boolean.class).orElse(true);\n    configure(delim, uppercase);\n  }\n\n  private void configure(String delim, boolean uppercase) {\n    deserializeHexFormat = HexFormat.ofDelimiter(delim);\n    if (uppercase) {\n      deserializeHexFormat = deserializeHexFormat.withUpperCase();\n    }\n  }\n\n  @Override\n  public Optional<String> getDescription() {\n    return Optional.empty();\n  }\n\n  @Override\n  public Optional<SchemaDescription> getSchema(String topic, Target type) {\n    return Optional.empty();\n  }\n\n  @Override\n  public boolean canDeserialize(String topic, Target type) {\n    return true;\n  }\n\n  @Override\n  public boolean canSerialize(String topic, Target type) {\n    return true;\n  }\n\n  @Override\n  public Serializer serializer(String topic, Target type) {\n    return input -> {\n      input = input.trim();\n      // it is a hack to provide ability to sent empty array as a key/value\n      if (input.length() == 0) {\n        return new byte[] {};\n      }\n      return HexFormat.of().parseHex(prepareInputForParse(input));\n    };\n  }\n\n  // removing most-common delimiters and prefixes\n  private static String prepareInputForParse(String input) {\n    return input\n        .replaceAll(\" \", \"\")\n        .replaceAll(\"#\", \"\")\n        .replaceAll(\":\", \"\");\n  }\n\n  @Override\n  public Deserializer deserializer(String topic, Target type) {\n    return (headers, data) ->\n        new DeserializeResult(\n            deserializeHexFormat.formatHex(data),\n            DeserializeResult.Type.STRING,\n            Map.of()\n        );\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/Int32Serde.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport com.google.common.primitives.Ints;\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.SchemaDescription;\nimport com.provectus.kafka.ui.serdes.BuiltInSerde;\nimport java.util.Map;\nimport java.util.Optional;\n\npublic class Int32Serde implements BuiltInSerde {\n\n  public static String name() {\n    return \"Int32\";\n  }\n\n  @Override\n  public Optional<String> getDescription() {\n    return Optional.empty();\n  }\n\n  @Override\n  public Optional<SchemaDescription> getSchema(String topic, Target type) {\n    return Optional.of(\n        new SchemaDescription(\n            String.format(\n                \"{ \"\n                    + \"  \\\"type\\\" : \\\"integer\\\", \"\n                    + \"  \\\"minimum\\\" : %s, \"\n                    + \"  \\\"maximum\\\" : %s \"\n                    + \"}\",\n                Integer.MIN_VALUE,\n                Integer.MAX_VALUE\n            ),\n            Map.of()\n        )\n    );\n  }\n\n  @Override\n  public boolean canDeserialize(String topic, Target type) {\n    return true;\n  }\n\n  @Override\n  public boolean canSerialize(String topic, Target type) {\n    return true;\n  }\n\n  @Override\n  public Serializer serializer(String topic, Target type) {\n    return input -> Ints.toByteArray(Integer.parseInt(input));\n  }\n\n  @Override\n  public Deserializer deserializer(String topic, Target type) {\n    return (headers, data) ->\n        new DeserializeResult(\n            String.valueOf(Ints.fromByteArray(data)),\n            DeserializeResult.Type.JSON,\n            Map.of()\n        );\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/Int64Serde.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport com.google.common.primitives.Longs;\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.PropertyResolver;\nimport com.provectus.kafka.ui.serde.api.RecordHeaders;\nimport com.provectus.kafka.ui.serde.api.SchemaDescription;\nimport com.provectus.kafka.ui.serdes.BuiltInSerde;\nimport java.util.Map;\nimport java.util.Optional;\n\npublic class Int64Serde implements BuiltInSerde {\n\n  public static String name() {\n    return \"Int64\";\n  }\n\n  @Override\n  public Optional<String> getDescription() {\n    return Optional.empty();\n  }\n\n  @Override\n  public Optional<SchemaDescription> getSchema(String topic, Target type) {\n    return Optional.of(\n        new SchemaDescription(\n            String.format(\n                \"{ \"\n                    + \"  \\\"type\\\" : \\\"integer\\\", \"\n                    + \"  \\\"minimum\\\" : %s, \"\n                    + \"  \\\"maximum\\\" : %s \"\n                    + \"}\",\n                Long.MIN_VALUE,\n                Long.MAX_VALUE\n            ),\n            Map.of()\n        )\n    );\n  }\n\n  @Override\n  public boolean canDeserialize(String topic, Target type) {\n    return true;\n  }\n\n  @Override\n  public boolean canSerialize(String topic, Target type) {\n    return true;\n  }\n\n  @Override\n  public Serializer serializer(String topic, Target type) {\n    return input -> Longs.toByteArray(Long.parseLong(input));\n  }\n\n  @Override\n  public Deserializer deserializer(String topic, Target type) {\n    return (headers, data) ->\n        new DeserializeResult(\n            String.valueOf(Longs.fromByteArray(data)),\n            DeserializeResult.Type.JSON,\n            Map.of()\n        );\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/ProtobufFileSerde.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport com.google.common.annotations.VisibleForTesting;\nimport com.google.common.base.Preconditions;\nimport com.google.protobuf.AnyProto;\nimport com.google.protobuf.ApiProto;\nimport com.google.protobuf.DescriptorProtos;\nimport com.google.protobuf.Descriptors;\nimport com.google.protobuf.Descriptors.Descriptor;\nimport com.google.protobuf.DurationProto;\nimport com.google.protobuf.DynamicMessage;\nimport com.google.protobuf.EmptyProto;\nimport com.google.protobuf.FieldMaskProto;\nimport com.google.protobuf.SourceContextProto;\nimport com.google.protobuf.StructProto;\nimport com.google.protobuf.TimestampProto;\nimport com.google.protobuf.TypeProto;\nimport com.google.protobuf.WrappersProto;\nimport com.google.protobuf.util.JsonFormat;\nimport com.google.type.ColorProto;\nimport com.google.type.DateProto;\nimport com.google.type.DateTimeProto;\nimport com.google.type.DayOfWeekProto;\nimport com.google.type.ExprProto;\nimport com.google.type.FractionProto;\nimport com.google.type.IntervalProto;\nimport com.google.type.LatLngProto;\nimport com.google.type.MoneyProto;\nimport com.google.type.MonthProto;\nimport com.google.type.PhoneNumberProto;\nimport com.google.type.PostalAddressProto;\nimport com.google.type.QuaternionProto;\nimport com.google.type.TimeOfDayProto;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.PropertyResolver;\nimport com.provectus.kafka.ui.serde.api.RecordHeaders;\nimport com.provectus.kafka.ui.serde.api.SchemaDescription;\nimport com.provectus.kafka.ui.serdes.BuiltInSerde;\nimport com.provectus.kafka.ui.util.jsonschema.ProtobufSchemaConverter;\nimport com.squareup.wire.schema.ErrorCollector;\nimport com.squareup.wire.schema.Linker;\nimport com.squareup.wire.schema.Loader;\nimport com.squareup.wire.schema.Location;\nimport com.squareup.wire.schema.ProtoFile;\nimport com.squareup.wire.schema.internal.parser.ProtoFileElement;\nimport com.squareup.wire.schema.internal.parser.ProtoParser;\nimport io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema;\nimport io.confluent.kafka.schemaregistry.protobuf.ProtobufSchemaUtils;\nimport java.io.ByteArrayInputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport javax.annotation.Nullable;\nimport lombok.SneakyThrows;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jetbrains.annotations.NotNull;\n\n@Slf4j\npublic class ProtobufFileSerde implements BuiltInSerde {\n\n  public static String name() {\n    return \"ProtobufFile\";\n  }\n\n  private static final ProtobufSchemaConverter SCHEMA_CONVERTER = new ProtobufSchemaConverter();\n\n  private Map<String, Descriptor> messageDescriptorMap = new HashMap<>();\n  private Map<String, Descriptor> keyMessageDescriptorMap = new HashMap<>();\n\n  private Map<Descriptor, Path> descriptorPaths = new HashMap<>();\n\n  @Nullable\n  private Descriptor defaultMessageDescriptor;\n\n  @Nullable\n  private Descriptor defaultKeyMessageDescriptor;\n\n  @Override\n  public boolean canBeAutoConfigured(PropertyResolver kafkaClusterProperties,\n                                     PropertyResolver globalProperties) {\n    return Configuration.canBeAutoConfigured(kafkaClusterProperties);\n  }\n\n  @Override\n  public void autoConfigure(PropertyResolver kafkaClusterProperties,\n                            PropertyResolver globalProperties) {\n    configure(Configuration.create(kafkaClusterProperties));\n  }\n\n  @Override\n  public void configure(PropertyResolver serdeProperties,\n                        PropertyResolver kafkaClusterProperties,\n                        PropertyResolver globalProperties) {\n    configure(Configuration.create(serdeProperties));\n  }\n\n  @VisibleForTesting\n  void configure(Configuration configuration) {\n    if (configuration.defaultMessageDescriptor() == null\n        && configuration.defaultKeyMessageDescriptor() == null\n        && configuration.messageDescriptorMap().isEmpty()\n        && configuration.keyMessageDescriptorMap().isEmpty()) {\n      throw new ValidationException(\"Neither default, not per-topic descriptors defined for \" + name() + \" serde\");\n    }\n    this.defaultMessageDescriptor = configuration.defaultMessageDescriptor();\n    this.defaultKeyMessageDescriptor = configuration.defaultKeyMessageDescriptor();\n    this.descriptorPaths = configuration.descriptorPaths();\n    this.messageDescriptorMap = configuration.messageDescriptorMap();\n    this.keyMessageDescriptorMap = configuration.keyMessageDescriptorMap();\n  }\n\n  @Override\n  public Optional<String> getDescription() {\n    return Optional.empty();\n  }\n\n  private Optional<Descriptor> descriptorFor(String topic, Target type) {\n    return type == Target.KEY\n        ?\n        Optional.ofNullable(keyMessageDescriptorMap.get(topic))\n            .or(() -> Optional.ofNullable(defaultKeyMessageDescriptor))\n        :\n        Optional.ofNullable(messageDescriptorMap.get(topic))\n            .or(() -> Optional.ofNullable(defaultMessageDescriptor));\n  }\n\n  @Override\n  public boolean canDeserialize(String topic, Target type) {\n    return descriptorFor(topic, type).isPresent();\n  }\n\n  @Override\n  public boolean canSerialize(String topic, Target type) {\n    return descriptorFor(topic, type).isPresent();\n  }\n\n  @Override\n  public Serializer serializer(String topic, Target type) {\n    var descriptor = descriptorFor(topic, type).orElseThrow();\n    return new Serializer() {\n      @SneakyThrows\n      @Override\n      public byte[] serialize(String input) {\n        DynamicMessage.Builder builder = DynamicMessage.newBuilder(descriptor);\n        JsonFormat.parser().merge(input, builder);\n        return builder.build().toByteArray();\n      }\n    };\n  }\n\n  @Override\n  public Deserializer deserializer(String topic, Target type) {\n    var descriptor = descriptorFor(topic, type).orElseThrow();\n    return new Deserializer() {\n      @SneakyThrows\n      @Override\n      public DeserializeResult deserialize(RecordHeaders headers, byte[] data) {\n        var protoMsg = DynamicMessage.parseFrom(descriptor, new ByteArrayInputStream(data));\n        byte[] jsonFromProto = ProtobufSchemaUtils.toJson(protoMsg);\n        var result = new String(jsonFromProto);\n        return new DeserializeResult(\n            result,\n            DeserializeResult.Type.JSON,\n            Map.of()\n        );\n      }\n    };\n  }\n\n  @Override\n  public Optional<SchemaDescription> getSchema(String topic, Target type) {\n    return descriptorFor(topic, type).map(this::toSchemaDescription);\n  }\n\n  private SchemaDescription toSchemaDescription(Descriptor descriptor) {\n    Path path = descriptorPaths.get(descriptor);\n    return new SchemaDescription(\n        SCHEMA_CONVERTER.convert(path.toUri(), descriptor).toJson(),\n        Map.of(\"messageName\", descriptor.getFullName())\n    );\n  }\n\n  @SneakyThrows\n  private static String readFileAsString(Path path) {\n    return Files.readString(path);\n  }\n\n  //----------------------------------------------------------------------------------------------------------------\n\n  @VisibleForTesting\n  record Configuration(@Nullable Descriptor defaultMessageDescriptor,\n                       @Nullable Descriptor defaultKeyMessageDescriptor,\n                       Map<Descriptor, Path> descriptorPaths,\n                       Map<String, Descriptor> messageDescriptorMap,\n                       Map<String, Descriptor> keyMessageDescriptorMap) {\n\n    static boolean canBeAutoConfigured(PropertyResolver kafkaClusterProperties) {\n      Optional<List<String>> protobufFiles = kafkaClusterProperties.getListProperty(\"protobufFiles\", String.class);\n      Optional<String> protobufFilesDir = kafkaClusterProperties.getProperty(\"protobufFilesDir\", String.class);\n      return protobufFilesDir.isPresent() || protobufFiles.filter(files -> !files.isEmpty()).isPresent();\n    }\n\n    static Configuration create(PropertyResolver properties) {\n      var protobufSchemas = loadSchemas(\n          properties.getListProperty(\"protobufFiles\", String.class),\n          properties.getProperty(\"protobufFilesDir\", String.class)\n      );\n\n      // Load all referenced message schemas and store their source proto file with the descriptors\n      Map<Descriptor, Path> descriptorPaths = new HashMap<>();\n      Optional<String> protobufMessageName = properties.getProperty(\"protobufMessageName\", String.class);\n      protobufMessageName.ifPresent(messageName -> addProtobufSchema(descriptorPaths, protobufSchemas, messageName));\n\n      Optional<String> protobufMessageNameForKey =\n          properties.getProperty(\"protobufMessageNameForKey\", String.class);\n      protobufMessageNameForKey\n          .ifPresent(messageName -> addProtobufSchema(descriptorPaths, protobufSchemas, messageName));\n\n      Optional<Map<String, String>> protobufMessageNameByTopic =\n          properties.getMapProperty(\"protobufMessageNameByTopic\", String.class, String.class);\n      protobufMessageNameByTopic\n          .ifPresent(messageNamesByTopic -> addProtobufSchemas(descriptorPaths, protobufSchemas, messageNamesByTopic));\n\n      Optional<Map<String, String>> protobufMessageNameForKeyByTopic =\n          properties.getMapProperty(\"protobufMessageNameForKeyByTopic\", String.class, String.class);\n      protobufMessageNameForKeyByTopic\n          .ifPresent(messageNamesByTopic -> addProtobufSchemas(descriptorPaths, protobufSchemas, messageNamesByTopic));\n\n      // Fill dictionary for descriptor lookup by full message name\n      Map<String, Descriptor> descriptorMap = descriptorPaths.keySet().stream()\n          .collect(Collectors.toMap(Descriptor::getFullName, Function.identity()));\n\n      return new Configuration(\n          protobufMessageName.map(descriptorMap::get).orElse(null),\n          protobufMessageNameForKey.map(descriptorMap::get).orElse(null),\n          descriptorPaths,\n          protobufMessageNameByTopic.map(map -> populateDescriptors(descriptorMap, map)).orElse(Map.of()),\n          protobufMessageNameForKeyByTopic.map(map -> populateDescriptors(descriptorMap, map)).orElse(Map.of())\n      );\n    }\n\n    private static Map.Entry<Descriptor, Path> getDescriptorAndPath(Map<Path, ProtobufSchema> protobufSchemas,\n                                                                    String msgName) {\n      return protobufSchemas.entrySet().stream()\n          .filter(schema -> schema.getValue().toDescriptor(msgName) != null)\n          .map(schema -> Map.entry(schema.getValue().toDescriptor(msgName), schema.getKey()))\n          .findFirst()\n          .orElseThrow(() -> new NullPointerException(\n              \"The given message type not found in protobuf definition: \" + msgName));\n    }\n\n    private static Map<String, Descriptor> populateDescriptors(Map<String, Descriptor> descriptorMap,\n                                                               Map<String, String> messageNameMap) {\n      Map<String, Descriptor> descriptors = new HashMap<>();\n      for (Map.Entry<String, String> entry : messageNameMap.entrySet()) {\n        descriptors.put(entry.getKey(), descriptorMap.get(entry.getValue()));\n      }\n      return descriptors;\n    }\n\n    @VisibleForTesting\n    static Map<Path, ProtobufSchema> loadSchemas(Optional<List<String>> protobufFiles,\n                                                 Optional<String> protobufFilesDir) {\n      if (protobufFilesDir.isPresent()) {\n        if (protobufFiles.isPresent()) {\n          log.warn(\"protobufFiles properties will be ignored, since protobufFilesDir provided\");\n        }\n        List<ProtoFile> loadedFiles = new ProtoSchemaLoader(protobufFilesDir.get()).load();\n        Map<String, ProtoFileElement> allPaths = loadedFiles.stream()\n            .collect(Collectors.toMap(f -> f.getLocation().getPath(), ProtoFile::toElement));\n        return loadedFiles.stream()\n            .collect(Collectors.toMap(\n                f -> Path.of(f.getLocation().getBase(), f.getLocation().getPath()),\n                f -> new ProtobufSchema(f.toElement(), List.of(), allPaths)));\n      }\n      //Supporting for backward-compatibility. Normally, protobufFilesDir setting should be used\n      return protobufFiles.stream()\n          .flatMap(Collection::stream)\n          .distinct()\n          .map(Path::of)\n          .collect(Collectors.toMap(path -> path, path -> new ProtobufSchema(readFileAsString(path))));\n    }\n\n    private static void addProtobufSchema(Map<Descriptor, Path> descriptorPaths,\n                                          Map<Path, ProtobufSchema> protobufSchemas,\n                                          String messageName) {\n      var descriptorAndPath = getDescriptorAndPath(protobufSchemas, messageName);\n      descriptorPaths.put(descriptorAndPath.getKey(), descriptorAndPath.getValue());\n    }\n\n    private static void addProtobufSchemas(Map<Descriptor, Path> descriptorPaths,\n                                           Map<Path, ProtobufSchema> protobufSchemas,\n                                           Map<String, String> messageNamesByTopic) {\n      messageNamesByTopic.values().stream()\n          .map(msgName -> getDescriptorAndPath(protobufSchemas, msgName))\n          .forEach(entry -> descriptorPaths.put(entry.getKey(), entry.getValue()));\n    }\n  }\n\n  static class ProtoSchemaLoader {\n\n    private final Path baseLocation;\n\n    ProtoSchemaLoader(String baseLocationStr) {\n      this.baseLocation = Path.of(baseLocationStr);\n      if (!Files.isReadable(baseLocation)) {\n        throw new ValidationException(\"proto files directory not readable\");\n      }\n    }\n\n    List<ProtoFile> load() {\n      Map<String, ProtoFile> knownTypes = knownProtoFiles();\n\n      Map<String, ProtoFile> filesByLocations = new HashMap<>();\n      filesByLocations.putAll(knownTypes);\n      filesByLocations.putAll(loadFilesWithLocations());\n\n      Linker linker = new Linker(\n          createFilesLoader(filesByLocations),\n          new ErrorCollector(),\n          true,\n          true\n      );\n      var schema = linker.link(filesByLocations.values());\n      linker.getErrors().throwIfNonEmpty();\n      return schema.getProtoFiles()\n          .stream()\n          .filter(p -> !knownTypes.containsKey(p.getLocation().getPath())) //filtering known types\n          .toList();\n    }\n\n    private Map<String, ProtoFile> knownProtoFiles() {\n      return Stream.of(\n          loadKnownProtoFile(\"google/type/color.proto\", ColorProto.getDescriptor()),\n          loadKnownProtoFile(\"google/type/date.proto\", DateProto.getDescriptor()),\n          loadKnownProtoFile(\"google/type/datetime.proto\", DateTimeProto.getDescriptor()),\n          loadKnownProtoFile(\"google/type/dayofweek.proto\", DayOfWeekProto.getDescriptor()),\n          loadKnownProtoFile(\"google/type/decimal.proto\", com.google.type.DecimalProto.getDescriptor()),\n          loadKnownProtoFile(\"google/type/expr.proto\", ExprProto.getDescriptor()),\n          loadKnownProtoFile(\"google/type/fraction.proto\", FractionProto.getDescriptor()),\n          loadKnownProtoFile(\"google/type/interval.proto\", IntervalProto.getDescriptor()),\n          loadKnownProtoFile(\"google/type/latlng.proto\", LatLngProto.getDescriptor()),\n          loadKnownProtoFile(\"google/type/money.proto\", MoneyProto.getDescriptor()),\n          loadKnownProtoFile(\"google/type/month.proto\", MonthProto.getDescriptor()),\n          loadKnownProtoFile(\"google/type/phone_number.proto\", PhoneNumberProto.getDescriptor()),\n          loadKnownProtoFile(\"google/type/postal_address.proto\", PostalAddressProto.getDescriptor()),\n          loadKnownProtoFile(\"google/type/quaternion.prot\", QuaternionProto.getDescriptor()),\n          loadKnownProtoFile(\"google/type/timeofday.proto\", TimeOfDayProto.getDescriptor()),\n          loadKnownProtoFile(\"google/protobuf/any.proto\", AnyProto.getDescriptor()),\n          loadKnownProtoFile(\"google/protobuf/api.proto\", ApiProto.getDescriptor()),\n          loadKnownProtoFile(\"google/protobuf/descriptor.proto\", DescriptorProtos.getDescriptor()),\n          loadKnownProtoFile(\"google/protobuf/duration.proto\", DurationProto.getDescriptor()),\n          loadKnownProtoFile(\"google/protobuf/empty.proto\", EmptyProto.getDescriptor()),\n          loadKnownProtoFile(\"google/protobuf/field_mask.proto\", FieldMaskProto.getDescriptor()),\n          loadKnownProtoFile(\"google/protobuf/source_context.proto\", SourceContextProto.getDescriptor()),\n          loadKnownProtoFile(\"google/protobuf/struct.proto\", StructProto.getDescriptor()),\n          loadKnownProtoFile(\"google/protobuf/timestamp.proto\", TimestampProto.getDescriptor()),\n          loadKnownProtoFile(\"google/protobuf/type.proto\", TypeProto.getDescriptor()),\n          loadKnownProtoFile(\"google/protobuf/wrappers.proto\", WrappersProto.getDescriptor())\n      ).collect(Collectors.toMap(p -> p.getLocation().getPath(), p -> p));\n    }\n\n    private ProtoFile loadKnownProtoFile(String path, Descriptors.FileDescriptor fileDescriptor) {\n      String protoFileString = null;\n      // know type file contains either message or enum\n      if (!fileDescriptor.getMessageTypes().isEmpty()) {\n        protoFileString = new ProtobufSchema(fileDescriptor.getMessageTypes().get(0)).canonicalString();\n      } else if (!fileDescriptor.getEnumTypes().isEmpty()) {\n        protoFileString = new ProtobufSchema(fileDescriptor.getEnumTypes().get(0)).canonicalString();\n      } else {\n        throw new IllegalStateException();\n      }\n      return ProtoFile.Companion.get(ProtoParser.Companion.parse(Location.get(path), protoFileString));\n    }\n\n    private Loader createFilesLoader(Map<String, ProtoFile> files) {\n      return new Loader() {\n        @Override\n        public @NotNull ProtoFile load(@NotNull String path) {\n          return Preconditions.checkNotNull(files.get(path), \"ProtoFile not found for import '%s'\", path);\n        }\n\n        @Override\n        public @NotNull Loader withErrors(@NotNull ErrorCollector errorCollector) {\n          return this;\n        }\n      };\n    }\n\n    @SneakyThrows\n    private Map<String, ProtoFile> loadFilesWithLocations() {\n      Map<String, ProtoFile> filesByLocations = new HashMap<>();\n      try (var files = Files.walk(baseLocation)) {\n        files.filter(p -> !Files.isDirectory(p) && p.toString().endsWith(\".proto\"))\n            .forEach(path -> {\n              // relative path will be used as \"import\" statement\n              String relativePath = baseLocation.relativize(path).toString();\n              var protoFileElement = ProtoParser.Companion.parse(\n                  Location.get(baseLocation.toString(), relativePath),\n                  readFileAsString(path)\n              );\n              filesByLocations.put(relativePath, ProtoFile.Companion.get(protoFileElement));\n            });\n      }\n      return filesByLocations;\n    }\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/ProtobufRawSerde.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport com.google.protobuf.UnknownFieldSet;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.RecordHeaders;\nimport com.provectus.kafka.ui.serde.api.SchemaDescription;\nimport com.provectus.kafka.ui.serdes.BuiltInSerde;\nimport java.util.Map;\nimport java.util.Optional;\nimport lombok.SneakyThrows;\n\npublic class ProtobufRawSerde implements BuiltInSerde {\n\n  public static String name() {\n    return \"ProtobufDecodeRaw\";\n  }\n\n  @Override\n  public Optional<String> getDescription() {\n    return Optional.empty();\n  }\n\n  @Override\n  public Optional<SchemaDescription> getSchema(String topic, Target type) {\n    return Optional.empty();\n  }\n\n  @Override\n  public boolean canSerialize(String topic, Target type) {\n    return false;\n  }\n\n  @Override\n  public boolean canDeserialize(String topic, Target type) {\n    return true;\n  }\n\n  @Override\n  public Serializer serializer(String topic, Target type) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public Deserializer deserializer(String topic, Target type) {\n    return new Deserializer() {\n        @SneakyThrows\n        @Override\n        public DeserializeResult deserialize(RecordHeaders headers, byte[] data) {\n            try {\n              UnknownFieldSet unknownFields = UnknownFieldSet.parseFrom(data);\n              return new DeserializeResult(unknownFields.toString(), DeserializeResult.Type.STRING, Map.of());\n            } catch (Exception e) {\n              throw new ValidationException(e.getMessage());\n            }\n        }\n    };\n  }\n}"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/StringSerde.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.PropertyResolver;\nimport com.provectus.kafka.ui.serde.api.SchemaDescription;\nimport com.provectus.kafka.ui.serdes.BuiltInSerde;\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Map;\nimport java.util.Optional;\n\npublic class StringSerde implements BuiltInSerde {\n\n  public static String name() {\n    return \"String\";\n  }\n\n  private Charset encoding = StandardCharsets.UTF_8;\n\n  @Override\n  public void configure(PropertyResolver serdeProperties,\n                        PropertyResolver kafkaClusterProperties,\n                        PropertyResolver globalProperties) {\n    serdeProperties.getProperty(\"encoding\", String.class)\n        .map(Charset::forName)\n        .ifPresent(e -> StringSerde.this.encoding = e);\n  }\n\n  @Override\n  public Optional<String> getDescription() {\n    return Optional.empty();\n  }\n\n  @Override\n  public Optional<SchemaDescription> getSchema(String topic, Target type) {\n    return Optional.empty();\n  }\n\n  @Override\n  public boolean canDeserialize(String topic, Target type) {\n    return true;\n  }\n\n  @Override\n  public boolean canSerialize(String topic, Target type) {\n    return true;\n  }\n\n  @Override\n  public Serializer serializer(String topic, Target type) {\n    return input -> input.getBytes(encoding);\n  }\n\n  @Override\n  public Deserializer deserializer(String topic, Target type) {\n    return (headers, data) ->\n        new DeserializeResult(\n            new String(data, encoding),\n            DeserializeResult.Type.STRING,\n            Map.of()\n        );\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/UInt32Serde.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport com.google.common.primitives.Ints;\nimport com.google.common.primitives.UnsignedInteger;\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.SchemaDescription;\nimport com.provectus.kafka.ui.serdes.BuiltInSerde;\nimport java.util.Map;\nimport java.util.Optional;\n\npublic class UInt32Serde implements BuiltInSerde {\n\n  public static String name() {\n    return \"UInt32\";\n  }\n\n  @Override\n  public Optional<String> getDescription() {\n    return Optional.empty();\n  }\n\n  @Override\n  public Optional<SchemaDescription> getSchema(String topic, Target type) {\n    return Optional.of(\n        new SchemaDescription(\n            String.format(\n                \"{ \"\n                    + \"  \\\"type\\\" : \\\"integer\\\", \"\n                    + \"  \\\"minimum\\\" : 0, \"\n                    + \"  \\\"maximum\\\" : %s\"\n                    + \"}\",\n                UnsignedInteger.MAX_VALUE\n            ),\n            Map.of()\n        )\n    );\n  }\n\n  @Override\n  public boolean canDeserialize(String topic, Target type) {\n    return true;\n  }\n\n  @Override\n  public boolean canSerialize(String topic, Target type) {\n    return true;\n  }\n\n  @Override\n  public Serializer serializer(String topic, Target type) {\n    return input -> Ints.toByteArray(Integer.parseUnsignedInt(input));\n  }\n\n  @Override\n  public Deserializer deserializer(String topic, Target type) {\n    return (headers, data) ->\n        new DeserializeResult(\n            UnsignedInteger.fromIntBits(Ints.fromByteArray(data)).toString(),\n            DeserializeResult.Type.JSON,\n            Map.of()\n        );\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/UInt64Serde.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport com.google.common.primitives.Longs;\nimport com.google.common.primitives.UnsignedLong;\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.SchemaDescription;\nimport com.provectus.kafka.ui.serdes.BuiltInSerde;\nimport java.util.Map;\nimport java.util.Optional;\n\n\npublic class UInt64Serde implements BuiltInSerde {\n\n  public static String name() {\n    return \"UInt64\";\n  }\n\n  @Override\n  public Optional<String> getDescription() {\n    return Optional.empty();\n  }\n\n  @Override\n  public Optional<SchemaDescription> getSchema(String topic, Target type) {\n    return Optional.of(\n        new SchemaDescription(\n            String.format(\n                \"{ \"\n                    + \"  \\\"type\\\" : \\\"integer\\\", \"\n                    + \"  \\\"minimum\\\" : 0, \"\n                    + \"  \\\"maximum\\\" : %s \"\n                    + \"}\",\n                UnsignedLong.MAX_VALUE\n            ),\n            Map.of()\n        )\n    );\n  }\n\n  @Override\n  public boolean canDeserialize(String topic, Target type) {\n    return true;\n  }\n\n  @Override\n  public boolean canSerialize(String topic, Target type) {\n    return true;\n  }\n\n  @Override\n  public Serializer serializer(String topic, Target type) {\n    return input -> Longs.toByteArray(Long.parseUnsignedLong(input));\n  }\n\n  @Override\n  public Deserializer deserializer(String topic, Target type) {\n    return (headers, data) ->\n        new DeserializeResult(\n            UnsignedLong.fromLongBits(Longs.fromByteArray(data)).toString(),\n            DeserializeResult.Type.JSON,\n            Map.of()\n        );\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/UuidBinarySerde.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.PropertyResolver;\nimport com.provectus.kafka.ui.serde.api.RecordHeaders;\nimport com.provectus.kafka.ui.serde.api.SchemaDescription;\nimport com.provectus.kafka.ui.serdes.BuiltInSerde;\nimport java.nio.ByteBuffer;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.UUID;\n\n\npublic class UuidBinarySerde implements BuiltInSerde {\n\n  public static String name() {\n    return \"UUIDBinary\";\n  }\n\n  private boolean mostSignificantBitsFirst = true;\n\n  @Override\n  public void configure(PropertyResolver serdeProperties,\n                        PropertyResolver kafkaClusterProperties,\n                        PropertyResolver globalProperties) {\n    serdeProperties.getProperty(\"mostSignificantBitsFirst\", Boolean.class)\n        .ifPresent(msb -> UuidBinarySerde.this.mostSignificantBitsFirst = msb);\n  }\n\n  @Override\n  public Optional<String> getDescription() {\n    return Optional.empty();\n  }\n\n  @Override\n  public Optional<SchemaDescription> getSchema(String topic, Target type) {\n    return Optional.empty();\n  }\n\n  @Override\n  public boolean canDeserialize(String topic, Target type) {\n    return true;\n  }\n\n  @Override\n  public boolean canSerialize(String topic, Target type) {\n    return true;\n  }\n\n  @Override\n  public Serializer serializer(String topic, Target type) {\n    return input -> {\n      UUID uuid = UUID.fromString(input);\n      ByteBuffer bb = ByteBuffer.wrap(new byte[16]);\n      if (mostSignificantBitsFirst) {\n        bb.putLong(uuid.getMostSignificantBits());\n        bb.putLong(uuid.getLeastSignificantBits());\n      } else {\n        bb.putLong(uuid.getLeastSignificantBits());\n        bb.putLong(uuid.getMostSignificantBits());\n      }\n      return bb.array();\n    };\n  }\n\n  @Override\n  public Deserializer deserializer(String topic, Target type) {\n    return (headers, data) -> {\n      if (data.length != 16) {\n        throw new ValidationException(\"UUID data should be 16 bytes, but it is \" + data.length);\n      }\n      ByteBuffer bb = ByteBuffer.wrap(data);\n      long msb = bb.getLong();\n      long lsb = bb.getLong();\n      UUID uuid = mostSignificantBitsFirst ? new UUID(msb, lsb) : new UUID(lsb, msb);\n      return new DeserializeResult(\n          uuid.toString(),\n          DeserializeResult.Type.STRING,\n          Map.of()\n      );\n    };\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/MessageFormatter.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin.sr;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.google.protobuf.Message;\nimport com.google.protobuf.util.JsonFormat;\nimport com.provectus.kafka.ui.util.jsonschema.JsonAvroConversion;\nimport io.confluent.kafka.schemaregistry.avro.AvroSchemaUtils;\nimport io.confluent.kafka.schemaregistry.client.SchemaRegistryClient;\nimport io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig;\nimport io.confluent.kafka.serializers.KafkaAvroDeserializer;\nimport io.confluent.kafka.serializers.KafkaAvroDeserializerConfig;\nimport io.confluent.kafka.serializers.json.KafkaJsonSchemaDeserializer;\nimport io.confluent.kafka.serializers.protobuf.KafkaProtobufDeserializer;\nimport java.util.Map;\nimport lombok.SneakyThrows;\n\ninterface MessageFormatter {\n\n  String format(String topic, byte[] value);\n\n  static Map<SchemaType, MessageFormatter> createMap(SchemaRegistryClient schemaRegistryClient) {\n    return Map.of(\n        SchemaType.AVRO, new AvroMessageFormatter(schemaRegistryClient),\n        SchemaType.JSON, new JsonSchemaMessageFormatter(schemaRegistryClient),\n        SchemaType.PROTOBUF, new ProtobufMessageFormatter(schemaRegistryClient)\n    );\n  }\n\n  class AvroMessageFormatter implements MessageFormatter {\n    private final KafkaAvroDeserializer avroDeserializer;\n\n    AvroMessageFormatter(SchemaRegistryClient client) {\n      this.avroDeserializer = new KafkaAvroDeserializer(client);\n      this.avroDeserializer.configure(\n          Map.of(\n              AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG, \"wontbeused\",\n              KafkaAvroDeserializerConfig.SPECIFIC_AVRO_READER_CONFIG, false,\n              KafkaAvroDeserializerConfig.SCHEMA_REFLECTION_CONFIG, false,\n              KafkaAvroDeserializerConfig.AVRO_USE_LOGICAL_TYPE_CONVERTERS_CONFIG, true\n          ),\n          false\n      );\n    }\n\n    @Override\n    public String format(String topic, byte[] value) {\n      Object deserialized = avroDeserializer.deserialize(topic, value);\n      var schema = AvroSchemaUtils.getSchema(deserialized);\n      return JsonAvroConversion.convertAvroToJson(deserialized, schema).toString();\n    }\n  }\n\n  class ProtobufMessageFormatter implements MessageFormatter {\n    private final KafkaProtobufDeserializer<?> protobufDeserializer;\n\n    ProtobufMessageFormatter(SchemaRegistryClient client) {\n      this.protobufDeserializer = new KafkaProtobufDeserializer<>(client);\n    }\n\n    @Override\n    @SneakyThrows\n    public String format(String topic, byte[] value) {\n      final Message message = protobufDeserializer.deserialize(topic, value);\n      return JsonFormat.printer()\n          .includingDefaultValueFields()\n          .omittingInsignificantWhitespace()\n          .preservingProtoFieldNames()\n          .print(message);\n    }\n  }\n\n  class JsonSchemaMessageFormatter implements MessageFormatter {\n    private final KafkaJsonSchemaDeserializer<JsonNode> jsonSchemaDeserializer;\n\n    JsonSchemaMessageFormatter(SchemaRegistryClient client) {\n      this.jsonSchemaDeserializer = new KafkaJsonSchemaDeserializer<>(client);\n    }\n\n    @Override\n    public String format(String topic, byte[] value) {\n      JsonNode json = jsonSchemaDeserializer.deserialize(topic, value);\n      return json.toString();\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/SchemaRegistrySerde.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin.sr;\n\nimport static com.provectus.kafka.ui.serdes.builtin.sr.Serialize.serializeAvro;\nimport static com.provectus.kafka.ui.serdes.builtin.sr.Serialize.serializeJson;\nimport static com.provectus.kafka.ui.serdes.builtin.sr.Serialize.serializeProto;\nimport static io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig.BASIC_AUTH_CREDENTIALS_SOURCE;\nimport static io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig.USER_INFO_CONFIG;\n\nimport com.google.common.annotations.VisibleForTesting;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.PropertyResolver;\nimport com.provectus.kafka.ui.serde.api.SchemaDescription;\nimport com.provectus.kafka.ui.serdes.BuiltInSerde;\nimport com.provectus.kafka.ui.util.jsonschema.AvroJsonSchemaConverter;\nimport com.provectus.kafka.ui.util.jsonschema.ProtobufSchemaConverter;\nimport io.confluent.kafka.schemaregistry.ParsedSchema;\nimport io.confluent.kafka.schemaregistry.avro.AvroSchema;\nimport io.confluent.kafka.schemaregistry.avro.AvroSchemaProvider;\nimport io.confluent.kafka.schemaregistry.client.CachedSchemaRegistryClient;\nimport io.confluent.kafka.schemaregistry.client.SchemaMetadata;\nimport io.confluent.kafka.schemaregistry.client.SchemaRegistryClient;\nimport io.confluent.kafka.schemaregistry.client.SchemaRegistryClientConfig;\nimport io.confluent.kafka.schemaregistry.client.rest.exceptions.RestClientException;\nimport io.confluent.kafka.schemaregistry.json.JsonSchema;\nimport io.confluent.kafka.schemaregistry.json.JsonSchemaProvider;\nimport io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema;\nimport io.confluent.kafka.schemaregistry.protobuf.ProtobufSchemaProvider;\nimport java.net.URI;\nimport java.nio.ByteBuffer;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.concurrent.Callable;\nimport javax.annotation.Nullable;\nimport lombok.SneakyThrows;\nimport org.apache.kafka.common.config.SslConfigs;\n\n\npublic class SchemaRegistrySerde implements BuiltInSerde {\n\n  private static final byte SR_PAYLOAD_MAGIC_BYTE = 0x0;\n  private static final int SR_PAYLOAD_PREFIX_LENGTH = 5;\n\n  public static String name() {\n    return \"SchemaRegistry\";\n  }\n\n  private static final String SCHEMA_REGISTRY = \"schemaRegistry\";\n\n  private SchemaRegistryClient schemaRegistryClient;\n  private List<String> schemaRegistryUrls;\n  private String valueSchemaNameTemplate;\n  private String keySchemaNameTemplate;\n  private boolean checkSchemaExistenceForDeserialize;\n\n  private Map<SchemaType, MessageFormatter> schemaRegistryFormatters;\n\n  @Override\n  public boolean canBeAutoConfigured(PropertyResolver kafkaClusterProperties,\n                                     PropertyResolver globalProperties) {\n    return kafkaClusterProperties.getListProperty(SCHEMA_REGISTRY, String.class)\n        .filter(lst -> !lst.isEmpty())\n        .isPresent();\n  }\n\n  @Override\n  public void autoConfigure(PropertyResolver kafkaClusterProperties,\n                            PropertyResolver globalProperties) {\n    var urls = kafkaClusterProperties.getListProperty(SCHEMA_REGISTRY, String.class)\n        .filter(lst -> !lst.isEmpty())\n        .orElseThrow(() -> new ValidationException(\"No urls provided for schema registry\"));\n    configure(\n        urls,\n        createSchemaRegistryClient(\n            urls,\n            kafkaClusterProperties.getProperty(\"schemaRegistryAuth.username\", String.class).orElse(null),\n            kafkaClusterProperties.getProperty(\"schemaRegistryAuth.password\", String.class).orElse(null),\n            kafkaClusterProperties.getProperty(\"schemaRegistrySsl.keystoreLocation\", String.class).orElse(null),\n            kafkaClusterProperties.getProperty(\"schemaRegistrySsl.keystorePassword\", String.class).orElse(null),\n            kafkaClusterProperties.getProperty(\"ssl.truststoreLocation\", String.class).orElse(null),\n            kafkaClusterProperties.getProperty(\"ssl.truststorePassword\", String.class).orElse(null)\n        ),\n        kafkaClusterProperties.getProperty(\"schemaRegistryKeySchemaNameTemplate\", String.class).orElse(\"%s-key\"),\n        kafkaClusterProperties.getProperty(\"schemaRegistrySchemaNameTemplate\", String.class).orElse(\"%s-value\"),\n        kafkaClusterProperties.getProperty(\"schemaRegistryCheckSchemaExistenceForDeserialize\", Boolean.class)\n            .orElse(false)\n    );\n  }\n\n  @Override\n  public void configure(PropertyResolver serdeProperties,\n                        PropertyResolver kafkaClusterProperties,\n                        PropertyResolver globalProperties) {\n    var urls = serdeProperties.getListProperty(\"url\", String.class)\n        .or(() -> kafkaClusterProperties.getListProperty(SCHEMA_REGISTRY, String.class))\n        .filter(lst -> !lst.isEmpty())\n        .orElseThrow(() -> new ValidationException(\"No urls provided for schema registry\"));\n    configure(\n        urls,\n        createSchemaRegistryClient(\n            urls,\n            serdeProperties.getProperty(\"username\", String.class).orElse(null),\n            serdeProperties.getProperty(\"password\", String.class).orElse(null),\n            serdeProperties.getProperty(\"keystoreLocation\", String.class).orElse(null),\n            serdeProperties.getProperty(\"keystorePassword\", String.class).orElse(null),\n            kafkaClusterProperties.getProperty(\"ssl.truststoreLocation\", String.class).orElse(null),\n            kafkaClusterProperties.getProperty(\"ssl.truststorePassword\", String.class).orElse(null)\n        ),\n        serdeProperties.getProperty(\"keySchemaNameTemplate\", String.class).orElse(\"%s-key\"),\n        serdeProperties.getProperty(\"schemaNameTemplate\", String.class).orElse(\"%s-value\"),\n        serdeProperties.getProperty(\"checkSchemaExistenceForDeserialize\", Boolean.class)\n            .orElse(false)\n    );\n  }\n\n  @VisibleForTesting\n  void configure(\n      List<String> schemaRegistryUrls,\n      SchemaRegistryClient schemaRegistryClient,\n      String keySchemaNameTemplate,\n      String valueSchemaNameTemplate,\n      boolean checkTopicSchemaExistenceForDeserialize) {\n    this.schemaRegistryUrls = schemaRegistryUrls;\n    this.schemaRegistryClient = schemaRegistryClient;\n    this.keySchemaNameTemplate = keySchemaNameTemplate;\n    this.valueSchemaNameTemplate = valueSchemaNameTemplate;\n    this.schemaRegistryFormatters = MessageFormatter.createMap(schemaRegistryClient);\n    this.checkSchemaExistenceForDeserialize = checkTopicSchemaExistenceForDeserialize;\n  }\n\n  private static SchemaRegistryClient createSchemaRegistryClient(List<String> urls,\n                                                                 @Nullable String username,\n                                                                 @Nullable String password,\n                                                                 @Nullable String keyStoreLocation,\n                                                                 @Nullable String keyStorePassword,\n                                                                 @Nullable String trustStoreLocation,\n                                                                 @Nullable String trustStorePassword) {\n    Map<String, String> configs = new HashMap<>();\n    if (username != null && password != null) {\n      configs.put(BASIC_AUTH_CREDENTIALS_SOURCE, \"USER_INFO\");\n      configs.put(USER_INFO_CONFIG, username + \":\" + password);\n    } else if (username != null) {\n      throw new ValidationException(\n          \"You specified username but do not specified password\");\n    } else if (password != null) {\n      throw new ValidationException(\n          \"You specified password but do not specified username\");\n    }\n\n    // We require at least a truststore. The logic is done similar to SchemaRegistryService.securedWebClientOnTLS\n    if (trustStoreLocation != null && trustStorePassword != null) {\n      configs.put(SchemaRegistryClientConfig.CLIENT_NAMESPACE + SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG,\n          trustStoreLocation);\n      configs.put(SchemaRegistryClientConfig.CLIENT_NAMESPACE + SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG,\n          trustStorePassword);\n    }\n\n    if (keyStoreLocation != null && keyStorePassword != null) {\n      configs.put(SchemaRegistryClientConfig.CLIENT_NAMESPACE + SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG,\n          keyStoreLocation);\n      configs.put(SchemaRegistryClientConfig.CLIENT_NAMESPACE + SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG,\n          keyStorePassword);\n      configs.put(SchemaRegistryClientConfig.CLIENT_NAMESPACE + SslConfigs.SSL_KEY_PASSWORD_CONFIG,\n          keyStorePassword);\n    }\n\n    return new CachedSchemaRegistryClient(\n        urls,\n        1_000,\n        List.of(new AvroSchemaProvider(), new ProtobufSchemaProvider(), new JsonSchemaProvider()),\n        configs\n    );\n  }\n\n  @Override\n  public Optional<String> getDescription() {\n    return Optional.empty();\n  }\n\n  @Override\n  public boolean canDeserialize(String topic, Target type) {\n    String subject = schemaSubject(topic, type);\n    return !checkSchemaExistenceForDeserialize\n        || getSchemaBySubject(subject).isPresent();\n  }\n\n  @Override\n  public boolean canSerialize(String topic, Target type) {\n    String subject = schemaSubject(topic, type);\n    return getSchemaBySubject(subject).isPresent();\n  }\n\n  @Override\n  public Optional<SchemaDescription> getSchema(String topic, Target type) {\n    String subject = schemaSubject(topic, type);\n    return getSchemaBySubject(subject)\n        .flatMap(schemaMetadata ->\n            //schema can be not-found, when schema contexts configured improperly\n            getSchemaById(schemaMetadata.getId())\n                .map(parsedSchema ->\n                    new SchemaDescription(\n                        convertSchema(schemaMetadata, parsedSchema),\n                        Map.of(\n                            \"subject\", subject,\n                            \"schemaId\", schemaMetadata.getId(),\n                            \"latestVersion\", schemaMetadata.getVersion(),\n                            \"type\", schemaMetadata.getSchemaType() // AVRO / PROTOBUF / JSON\n                        )\n                    )));\n  }\n\n  @SneakyThrows\n  private String convertSchema(SchemaMetadata schema, ParsedSchema parsedSchema) {\n    URI basePath = new URI(schemaRegistryUrls.get(0))\n        .resolve(Integer.toString(schema.getId()));\n    SchemaType schemaType = SchemaType.fromString(schema.getSchemaType())\n        .orElseThrow(() -> new IllegalStateException(\"Unknown schema type: \" + schema.getSchemaType()));\n    return switch (schemaType) {\n      case PROTOBUF -> new ProtobufSchemaConverter()\n          .convert(basePath, ((ProtobufSchema) parsedSchema).toDescriptor())\n          .toJson();\n      case AVRO -> new AvroJsonSchemaConverter()\n          .convert(basePath, ((AvroSchema) parsedSchema).rawSchema())\n          .toJson();\n      case JSON ->\n          //need to use confluent JsonSchema since it includes resolved references\n          ((JsonSchema) parsedSchema).rawSchema().toString();\n    };\n  }\n\n  private Optional<ParsedSchema> getSchemaById(int id) {\n    return wrapWith404Handler(() -> schemaRegistryClient.getSchemaById(id));\n  }\n\n  private Optional<SchemaMetadata> getSchemaBySubject(String subject) {\n    return wrapWith404Handler(() -> schemaRegistryClient.getLatestSchemaMetadata(subject));\n  }\n\n  @SneakyThrows\n  private <T> Optional<T> wrapWith404Handler(Callable<T> call) {\n    try {\n      return Optional.ofNullable(call.call());\n    } catch (RestClientException restClientException) {\n      if (restClientException.getStatus() == 404) {\n        return Optional.empty();\n      } else {\n        throw new RuntimeException(\"Error calling SchemaRegistryClient\", restClientException);\n      }\n    }\n  }\n\n  private String schemaSubject(String topic, Target type) {\n    return String.format(type == Target.KEY ? keySchemaNameTemplate : valueSchemaNameTemplate, topic);\n  }\n\n  @Override\n  public Serializer serializer(String topic, Target type) {\n    String subject = schemaSubject(topic, type);\n    SchemaMetadata meta = getSchemaBySubject(subject)\n        .orElseThrow(() -> new ValidationException(\n            String.format(\"No schema for subject '%s' found\", subject)));\n    ParsedSchema schema = getSchemaById(meta.getId())\n        .orElseThrow(() -> new IllegalStateException(\n            String.format(\"Schema found for id %s, subject '%s'\", meta.getId(), subject)));\n    SchemaType schemaType = SchemaType.fromString(meta.getSchemaType())\n        .orElseThrow(() -> new IllegalStateException(\"Unknown schema type: \" + meta.getSchemaType()));\n    return switch (schemaType) {\n      case PROTOBUF -> input ->\n          serializeProto(schemaRegistryClient, topic, type, (ProtobufSchema) schema, meta.getId(), input);\n      case AVRO -> input ->\n          serializeAvro((AvroSchema) schema, meta.getId(), input);\n      case JSON -> input ->\n          serializeJson((JsonSchema) schema, meta.getId(), input);\n    };\n  }\n\n  @Override\n  public Deserializer deserializer(String topic, Target type) {\n    return (headers, data) -> {\n      var schemaId = extractSchemaIdFromMsg(data);\n      SchemaType format = getMessageFormatBySchemaId(schemaId);\n      MessageFormatter formatter = schemaRegistryFormatters.get(format);\n      return new DeserializeResult(\n          formatter.format(topic, data),\n          DeserializeResult.Type.JSON,\n          Map.of(\n              \"schemaId\", schemaId,\n              \"type\", format.name()\n          )\n      );\n    };\n  }\n\n  private SchemaType getMessageFormatBySchemaId(int schemaId) {\n    return getSchemaById(schemaId)\n        .map(ParsedSchema::schemaType)\n        .flatMap(SchemaType::fromString)\n        .orElseThrow(() -> new ValidationException(String.format(\"Schema for id '%d' not found \", schemaId)));\n  }\n\n  private int extractSchemaIdFromMsg(byte[] data) {\n    ByteBuffer buffer = ByteBuffer.wrap(data);\n    if (buffer.remaining() >= SR_PAYLOAD_PREFIX_LENGTH && buffer.get() == SR_PAYLOAD_MAGIC_BYTE) {\n      return buffer.getInt();\n    }\n    throw new ValidationException(\n        String.format(\n            \"Data doesn't contain magic byte and schema id prefix, so it can't be deserialized with %s serde\",\n            name())\n    );\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/SchemaType.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin.sr;\n\nimport java.util.Optional;\nimport org.apache.commons.lang3.EnumUtils;\n\nenum SchemaType {\n  AVRO,\n  JSON,\n  PROTOBUF;\n\n  public static Optional<SchemaType> fromString(String typeString) {\n    return Optional.ofNullable(EnumUtils.getEnum(SchemaType.class, typeString));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/Serialize.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin.sr;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.google.common.base.Preconditions;\nimport com.google.protobuf.DynamicMessage;\nimport com.google.protobuf.Message;\nimport com.google.protobuf.util.JsonFormat;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport com.provectus.kafka.ui.util.annotation.KafkaClientInternalsDependant;\nimport com.provectus.kafka.ui.util.jsonschema.JsonAvroConversion;\nimport io.confluent.kafka.schemaregistry.avro.AvroSchema;\nimport io.confluent.kafka.schemaregistry.avro.AvroSchemaUtils;\nimport io.confluent.kafka.schemaregistry.client.SchemaRegistryClient;\nimport io.confluent.kafka.schemaregistry.json.JsonSchema;\nimport io.confluent.kafka.schemaregistry.json.jackson.Jackson;\nimport io.confluent.kafka.schemaregistry.protobuf.MessageIndexes;\nimport io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema;\nimport io.confluent.kafka.serializers.protobuf.AbstractKafkaProtobufSerializer;\nimport io.confluent.kafka.serializers.subject.DefaultReferenceSubjectNameStrategy;\nimport java.io.ByteArrayOutputStream;\nimport java.nio.ByteBuffer;\nimport java.util.HashMap;\nimport lombok.SneakyThrows;\nimport org.apache.avro.Schema;\nimport org.apache.avro.io.BinaryEncoder;\nimport org.apache.avro.io.DatumWriter;\nimport org.apache.avro.io.EncoderFactory;\n\nfinal class Serialize {\n\n  private static final byte MAGIC = 0x0;\n  private static final ObjectMapper JSON_SERIALIZE_MAPPER = Jackson.newObjectMapper(); //from confluent package\n\n  private Serialize() {\n  }\n\n  @KafkaClientInternalsDependant(\"AbstractKafkaJsonSchemaSerializer::serializeImpl\")\n  @SneakyThrows\n  static byte[] serializeJson(JsonSchema schema, int schemaId, String value) {\n    JsonNode json;\n    try {\n      json = JSON_SERIALIZE_MAPPER.readTree(value);\n    } catch (JsonProcessingException e) {\n      throw new ValidationException(String.format(\"'%s' is not valid json\", value));\n    }\n    try {\n      schema.validate(json);\n    } catch (org.everit.json.schema.ValidationException e) {\n      throw new ValidationException(\n          String.format(\"'%s' does not fit schema: %s\", value, e.getAllMessages()));\n    }\n    try (var out = new ByteArrayOutputStream()) {\n      out.write(MAGIC);\n      out.write(schemaId(schemaId));\n      out.write(JSON_SERIALIZE_MAPPER.writeValueAsBytes(json));\n      return out.toByteArray();\n    }\n  }\n\n  @KafkaClientInternalsDependant(\"AbstractKafkaProtobufSerializer::serializeImpl\")\n  @SneakyThrows\n  static byte[] serializeProto(SchemaRegistryClient srClient,\n                               String topic,\n                               Serde.Target target,\n                               ProtobufSchema schema,\n                               int schemaId,\n                               String input) {\n    // flags are tuned like in ProtobufSerializer by default\n    boolean normalizeSchema = false;\n    boolean autoRegisterSchema = false;\n    boolean useLatestVersion = true;\n    boolean latestCompatStrict = true;\n    boolean skipKnownTypes = true;\n\n    schema = AbstractKafkaProtobufSerializer.resolveDependencies(\n        srClient, normalizeSchema, autoRegisterSchema, useLatestVersion, latestCompatStrict,\n        new HashMap<>(), skipKnownTypes, new DefaultReferenceSubjectNameStrategy(),\n        topic, target == Serde.Target.KEY, schema\n    );\n\n    DynamicMessage.Builder builder = schema.newMessageBuilder();\n    JsonFormat.parser().merge(input, builder);\n    Message message = builder.build();\n    MessageIndexes indexes = schema.toMessageIndexes(message.getDescriptorForType().getFullName(), normalizeSchema);\n    try (var out = new ByteArrayOutputStream()) {\n      out.write(MAGIC);\n      out.write(schemaId(schemaId));\n      out.write(indexes.toByteArray());\n      message.writeTo(out);\n      return out.toByteArray();\n    }\n  }\n\n  @KafkaClientInternalsDependant(\"AbstractKafkaAvroSerializer::serializeImpl\")\n  @SneakyThrows\n  static byte[] serializeAvro(AvroSchema schema, int schemaId, String input) {\n    var avroObject = JsonAvroConversion.convertJsonToAvro(input, schema.rawSchema());\n    try (var out = new ByteArrayOutputStream()) {\n      out.write(MAGIC);\n      out.write(schemaId(schemaId));\n      Schema rawSchema = schema.rawSchema();\n      if (rawSchema.getType().equals(Schema.Type.BYTES)) {\n        Preconditions.checkState(\n            avroObject instanceof ByteBuffer,\n            \"Unrecognized bytes object of type: \" + avroObject.getClass().getName()\n        );\n        out.write(((ByteBuffer) avroObject).array());\n      } else {\n        boolean useLogicalTypeConverters = true;\n        BinaryEncoder encoder = EncoderFactory.get().directBinaryEncoder(out, null);\n        DatumWriter<Object> writer =\n            (DatumWriter<Object>) AvroSchemaUtils.getDatumWriter(avroObject, rawSchema, useLogicalTypeConverters);\n        writer.write(avroObject, encoder);\n        encoder.flush();\n      }\n      return out.toByteArray();\n    }\n  }\n\n  private static byte[] schemaId(int id) {\n    return ByteBuffer.allocate(Integer.BYTES).putInt(id).array();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/AdminClientService.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport reactor.core.publisher.Mono;\n\npublic interface AdminClientService {\n\n  Mono<ReactiveAdminClient> get(KafkaCluster cluster);\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/AdminClientServiceImpl.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.util.SslPropertiesUtil;\nimport java.io.Closeable;\nimport java.time.Instant;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Properties;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.kafka.clients.admin.AdminClient;\nimport org.apache.kafka.clients.admin.AdminClientConfig;\nimport org.springframework.stereotype.Service;\nimport reactor.core.publisher.Mono;\n\n@Service\n@Slf4j\npublic class AdminClientServiceImpl implements AdminClientService, Closeable {\n\n  private static final int DEFAULT_CLIENT_TIMEOUT_MS = 30_000;\n\n  private static final AtomicLong CLIENT_ID_SEQ = new AtomicLong();\n\n  private final Map<String, ReactiveAdminClient> adminClientCache = new ConcurrentHashMap<>();\n  private final int clientTimeout;\n\n  public AdminClientServiceImpl(ClustersProperties clustersProperties) {\n    this.clientTimeout = Optional.ofNullable(clustersProperties.getAdminClientTimeout())\n        .orElse(DEFAULT_CLIENT_TIMEOUT_MS);\n  }\n\n  @Override\n  public Mono<ReactiveAdminClient> get(KafkaCluster cluster) {\n    return Mono.justOrEmpty(adminClientCache.get(cluster.getName()))\n        .switchIfEmpty(createAdminClient(cluster))\n        .map(e -> adminClientCache.computeIfAbsent(cluster.getName(), key -> e));\n  }\n\n  private Mono<ReactiveAdminClient> createAdminClient(KafkaCluster cluster) {\n    return Mono.fromSupplier(() -> {\n      Properties properties = new Properties();\n      SslPropertiesUtil.addKafkaSslProperties(cluster.getOriginalProperties().getSsl(), properties);\n      properties.putAll(cluster.getProperties());\n      properties.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers());\n      properties.putIfAbsent(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, clientTimeout);\n      properties.putIfAbsent(\n          AdminClientConfig.CLIENT_ID_CONFIG,\n          \"kafka-ui-admin-\" + Instant.now().getEpochSecond() + \"-\" + CLIENT_ID_SEQ.incrementAndGet()\n      );\n      return AdminClient.create(properties);\n    }).flatMap(ac -> ReactiveAdminClient.create(ac).doOnError(th -> ac.close()))\n        .onErrorMap(th -> new IllegalStateException(\n            \"Error while creating AdminClient for Cluster \" + cluster.getName(), th));\n  }\n\n  @Override\n  public void close() {\n    adminClientCache.values().forEach(ReactiveAdminClient::close);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ApplicationInfoService.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport static com.provectus.kafka.ui.model.ApplicationInfoDTO.EnabledFeaturesEnum;\n\nimport com.provectus.kafka.ui.model.ApplicationInfoBuildDTO;\nimport com.provectus.kafka.ui.model.ApplicationInfoDTO;\nimport com.provectus.kafka.ui.model.ApplicationInfoLatestReleaseDTO;\nimport com.provectus.kafka.ui.util.DynamicConfigOperations;\nimport com.provectus.kafka.ui.util.GithubReleaseInfo;\nimport java.time.format.DateTimeFormatter;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Properties;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.info.BuildProperties;\nimport org.springframework.boot.info.GitProperties;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport org.springframework.stereotype.Service;\n\n@Service\npublic class ApplicationInfoService {\n\n  private final GithubReleaseInfo githubReleaseInfo = new GithubReleaseInfo();\n\n  private final DynamicConfigOperations dynamicConfigOperations;\n  private final BuildProperties buildProperties;\n  private final GitProperties gitProperties;\n\n  public ApplicationInfoService(DynamicConfigOperations dynamicConfigOperations,\n                                @Autowired(required = false) BuildProperties buildProperties,\n                                @Autowired(required = false) GitProperties gitProperties) {\n    this.dynamicConfigOperations = dynamicConfigOperations;\n    this.buildProperties = Optional.ofNullable(buildProperties).orElse(new BuildProperties(new Properties()));\n    this.gitProperties = Optional.ofNullable(gitProperties).orElse(new GitProperties(new Properties()));\n  }\n\n  public ApplicationInfoDTO getApplicationInfo() {\n    var releaseInfo = githubReleaseInfo.get();\n    return new ApplicationInfoDTO()\n        .build(getBuildInfo(releaseInfo))\n        .enabledFeatures(getEnabledFeatures())\n        .latestRelease(convert(releaseInfo));\n  }\n\n  private ApplicationInfoLatestReleaseDTO convert(GithubReleaseInfo.GithubReleaseDto releaseInfo) {\n    return new ApplicationInfoLatestReleaseDTO()\n        .htmlUrl(releaseInfo.html_url())\n        .publishedAt(releaseInfo.published_at())\n        .versionTag(releaseInfo.tag_name());\n  }\n\n  private ApplicationInfoBuildDTO getBuildInfo(GithubReleaseInfo.GithubReleaseDto release) {\n    return new ApplicationInfoBuildDTO()\n        .isLatestRelease(release.tag_name() != null && release.tag_name().equals(buildProperties.getVersion()))\n        .commitId(gitProperties.getShortCommitId())\n        .version(buildProperties.getVersion())\n        .buildTime(buildProperties.getTime() != null\n            ? DateTimeFormatter.ISO_INSTANT.format(buildProperties.getTime()) : null);\n  }\n\n  private List<EnabledFeaturesEnum> getEnabledFeatures() {\n    var enabledFeatures = new ArrayList<EnabledFeaturesEnum>();\n    if (dynamicConfigOperations.dynamicConfigEnabled()) {\n      enabledFeatures.add(EnabledFeaturesEnum.DYNAMIC_CONFIG);\n    }\n    return enabledFeatures;\n  }\n\n  // updating on startup and every hour\n  @Scheduled(fixedRateString = \"${github-release-info-update-rate:3600000}\")\n  public void updateGithubReleaseInfo() {\n    githubReleaseInfo.refresh().block();\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/BrokerService.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport com.provectus.kafka.ui.exception.InvalidRequestApiException;\nimport com.provectus.kafka.ui.exception.LogDirNotFoundApiException;\nimport com.provectus.kafka.ui.exception.NotFoundException;\nimport com.provectus.kafka.ui.exception.TopicOrPartitionNotFoundException;\nimport com.provectus.kafka.ui.mapper.DescribeLogDirsMapper;\nimport com.provectus.kafka.ui.model.BrokerLogdirUpdateDTO;\nimport com.provectus.kafka.ui.model.BrokersLogdirsDTO;\nimport com.provectus.kafka.ui.model.InternalBroker;\nimport com.provectus.kafka.ui.model.InternalBrokerConfig;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.PartitionDistributionStats;\nimport com.provectus.kafka.ui.service.metrics.RawMetric;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport javax.annotation.Nullable;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.kafka.clients.admin.ConfigEntry;\nimport org.apache.kafka.common.Node;\nimport org.apache.kafka.common.TopicPartitionReplica;\nimport org.apache.kafka.common.errors.InvalidRequestException;\nimport org.apache.kafka.common.errors.LogDirNotFoundException;\nimport org.apache.kafka.common.errors.TimeoutException;\nimport org.apache.kafka.common.errors.UnknownTopicOrPartitionException;\nimport org.apache.kafka.common.requests.DescribeLogDirsResponse;\nimport org.springframework.stereotype.Service;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\n@Service\n@RequiredArgsConstructor\n@Slf4j\npublic class BrokerService {\n\n  private final StatisticsCache statisticsCache;\n  private final AdminClientService adminClientService;\n  private final DescribeLogDirsMapper describeLogDirsMapper;\n\n  private Mono<Map<Integer, List<ConfigEntry>>> loadBrokersConfig(\n      KafkaCluster cluster, List<Integer> brokersIds) {\n    return adminClientService.get(cluster).flatMap(ac -> ac.loadBrokersConfig(brokersIds));\n  }\n\n  private Mono<List<ConfigEntry>> loadBrokersConfig(\n      KafkaCluster cluster, Integer brokerId) {\n    return loadBrokersConfig(cluster, Collections.singletonList(brokerId))\n        .map(map -> map.values().stream().findFirst().orElse(List.of()));\n  }\n\n  private Flux<InternalBrokerConfig> getBrokersConfig(KafkaCluster cluster, Integer brokerId) {\n    if (statisticsCache.get(cluster).getClusterDescription().getNodes()\n        .stream().noneMatch(node -> node.id() == brokerId)) {\n      return Flux.error(\n          new NotFoundException(String.format(\"Broker with id %s not found\", brokerId)));\n    }\n    return loadBrokersConfig(cluster, brokerId)\n        .map(list -> list.stream()\n            .map(InternalBrokerConfig::from)\n            .collect(Collectors.toList()))\n        .flatMapMany(Flux::fromIterable);\n  }\n\n  public Flux<InternalBroker> getBrokers(KafkaCluster cluster) {\n    var stats = statisticsCache.get(cluster);\n    var partitionsDistribution = PartitionDistributionStats.create(stats);\n    return adminClientService\n        .get(cluster)\n        .flatMap(ReactiveAdminClient::describeCluster)\n        .map(description -> description.getNodes().stream()\n            .map(node -> new InternalBroker(node, partitionsDistribution, stats))\n            .collect(Collectors.toList()))\n        .flatMapMany(Flux::fromIterable);\n  }\n\n  public Mono<Void> updateBrokerLogDir(KafkaCluster cluster,\n                                       Integer broker,\n                                       BrokerLogdirUpdateDTO brokerLogDir) {\n    return adminClientService.get(cluster)\n        .flatMap(ac -> updateBrokerLogDir(ac, brokerLogDir, broker));\n  }\n\n  private Mono<Void> updateBrokerLogDir(ReactiveAdminClient admin,\n                                        BrokerLogdirUpdateDTO b,\n                                        Integer broker) {\n\n    Map<TopicPartitionReplica, String> req = Map.of(\n        new TopicPartitionReplica(b.getTopic(), b.getPartition(), broker),\n        b.getLogDir());\n    return admin.alterReplicaLogDirs(req)\n        .onErrorResume(UnknownTopicOrPartitionException.class,\n            e -> Mono.error(new TopicOrPartitionNotFoundException()))\n        .onErrorResume(LogDirNotFoundException.class,\n            e -> Mono.error(new LogDirNotFoundApiException()))\n        .doOnError(e -> log.error(\"Unexpected error\", e));\n  }\n\n  public Mono<Void> updateBrokerConfigByName(KafkaCluster cluster,\n                                             Integer broker,\n                                             String name,\n                                             String value) {\n    return adminClientService.get(cluster)\n        .flatMap(ac -> ac.updateBrokerConfigByName(broker, name, value))\n        .onErrorResume(InvalidRequestException.class,\n            e -> Mono.error(new InvalidRequestApiException(e.getMessage())))\n        .doOnError(e -> log.error(\"Unexpected error\", e));\n  }\n\n  private Mono<Map<Integer, Map<String, DescribeLogDirsResponse.LogDirInfo>>> getClusterLogDirs(\n      KafkaCluster cluster, List<Integer> reqBrokers) {\n    return adminClientService.get(cluster)\n        .flatMap(admin -> {\n          List<Integer> brokers = statisticsCache.get(cluster).getClusterDescription().getNodes()\n              .stream()\n              .map(Node::id)\n              .collect(Collectors.toList());\n          if (!reqBrokers.isEmpty()) {\n            brokers.retainAll(reqBrokers);\n          }\n          return admin.describeLogDirs(brokers);\n        })\n        .onErrorResume(TimeoutException.class, (TimeoutException e) -> {\n          log.error(\"Error during fetching log dirs\", e);\n          return Mono.just(new HashMap<>());\n        });\n  }\n\n  public Flux<BrokersLogdirsDTO> getAllBrokersLogdirs(KafkaCluster cluster, List<Integer> brokers) {\n    return getClusterLogDirs(cluster, brokers)\n        .map(describeLogDirsMapper::toBrokerLogDirsList)\n        .flatMapMany(Flux::fromIterable);\n  }\n\n  public Flux<InternalBrokerConfig> getBrokerConfig(KafkaCluster cluster, Integer brokerId) {\n    return getBrokersConfig(cluster, brokerId);\n  }\n\n  public Mono<List<RawMetric>> getBrokerMetrics(KafkaCluster cluster, Integer brokerId) {\n    return Mono.justOrEmpty(statisticsCache.get(cluster).getMetrics().getPerBrokerMetrics().get(brokerId));\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ClusterService.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport com.provectus.kafka.ui.mapper.ClusterMapper;\nimport com.provectus.kafka.ui.model.ClusterDTO;\nimport com.provectus.kafka.ui.model.ClusterMetricsDTO;\nimport com.provectus.kafka.ui.model.ClusterStatsDTO;\nimport com.provectus.kafka.ui.model.InternalClusterState;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Service;\nimport reactor.core.publisher.Mono;\n\n@Service\n@RequiredArgsConstructor\n@Slf4j\npublic class ClusterService {\n\n  private final StatisticsCache statisticsCache;\n  private final ClustersStorage clustersStorage;\n  private final ClusterMapper clusterMapper;\n  private final StatisticsService statisticsService;\n\n  public List<ClusterDTO> getClusters() {\n    return clustersStorage.getKafkaClusters()\n        .stream()\n        .map(c -> clusterMapper.toCluster(new InternalClusterState(c, statisticsCache.get(c))))\n        .collect(Collectors.toList());\n  }\n\n  public Mono<ClusterStatsDTO> getClusterStats(KafkaCluster cluster) {\n    return Mono.justOrEmpty(\n        clusterMapper.toClusterStats(\n            new InternalClusterState(cluster, statisticsCache.get(cluster)))\n    );\n  }\n\n  public Mono<ClusterMetricsDTO> getClusterMetrics(KafkaCluster cluster) {\n\n    return Mono.just(\n        clusterMapper.toClusterMetrics(\n            statisticsCache.get(cluster).getMetrics()));\n  }\n\n  public Mono<ClusterDTO> updateCluster(KafkaCluster cluster) {\n    return statisticsService.updateCache(cluster)\n        .map(metrics -> clusterMapper.toCluster(new InternalClusterState(cluster, metrics)));\n  }\n}"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ClustersStatisticsScheduler.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Flux;\nimport reactor.core.scheduler.Schedulers;\n\n@Component\n@RequiredArgsConstructor\n@Slf4j\npublic class ClustersStatisticsScheduler {\n\n  private final ClustersStorage clustersStorage;\n\n  private final StatisticsService statisticsService;\n\n  @Scheduled(fixedRateString = \"${kafka.update-metrics-rate-millis:30000}\")\n  public void updateStatistics() {\n    Flux.fromIterable(clustersStorage.getKafkaClusters())\n        .parallel()\n        .runOn(Schedulers.parallel())\n        .flatMap(cluster -> {\n          log.debug(\"Start getting metrics for kafkaCluster: {}\", cluster.getName());\n          return statisticsService.updateCache(cluster)\n              .doOnSuccess(m -> log.debug(\"Metrics updated for cluster: {}\", cluster.getName()));\n        })\n        .then()\n        .block();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ClustersStorage.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport com.google.common.collect.ImmutableMap;\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport java.util.Collection;\nimport java.util.Optional;\nimport org.springframework.stereotype.Component;\n\n@Component\npublic class ClustersStorage {\n\n  private final ImmutableMap<String, KafkaCluster> kafkaClusters;\n\n  public ClustersStorage(ClustersProperties properties, KafkaClusterFactory factory) {\n    var builder = ImmutableMap.<String, KafkaCluster>builder();\n    properties.getClusters().forEach(c -> builder.put(c.getName(), factory.create(properties, c)));\n    this.kafkaClusters = builder.build();\n  }\n\n  public Collection<KafkaCluster> getKafkaClusters() {\n    return kafkaClusters.values();\n  }\n\n  public Optional<KafkaCluster> getClusterByName(String clusterName) {\n    return Optional.ofNullable(kafkaClusters.get(clusterName));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ConsumerGroupService.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport com.google.common.collect.Streams;\nimport com.google.common.collect.Table;\nimport com.provectus.kafka.ui.emitter.EnhancedConsumer;\nimport com.provectus.kafka.ui.model.ConsumerGroupOrderingDTO;\nimport com.provectus.kafka.ui.model.InternalConsumerGroup;\nimport com.provectus.kafka.ui.model.InternalTopicConsumerGroup;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.SortOrderDTO;\nimport com.provectus.kafka.ui.service.rbac.AccessControlService;\nimport com.provectus.kafka.ui.util.ApplicationMetrics;\nimport com.provectus.kafka.ui.util.SslPropertiesUtil;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Properties;\nimport java.util.function.ToIntFunction;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport javax.annotation.Nullable;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.kafka.clients.admin.ConsumerGroupDescription;\nimport org.apache.kafka.clients.admin.ConsumerGroupListing;\nimport org.apache.kafka.clients.admin.OffsetSpec;\nimport org.apache.kafka.clients.consumer.ConsumerConfig;\nimport org.apache.kafka.common.ConsumerGroupState;\nimport org.apache.kafka.common.TopicPartition;\nimport org.springframework.stereotype.Service;\nimport reactor.core.publisher.Mono;\n\n@Service\n@RequiredArgsConstructor\npublic class ConsumerGroupService {\n\n  private final AdminClientService adminClientService;\n  private final AccessControlService accessControlService;\n\n  private Mono<List<InternalConsumerGroup>> getConsumerGroups(\n      ReactiveAdminClient ac,\n      List<ConsumerGroupDescription> descriptions) {\n    var groupNames = descriptions.stream().map(ConsumerGroupDescription::groupId).toList();\n    // 1. getting committed offsets for all groups\n    return ac.listConsumerGroupOffsets(groupNames, null)\n        .flatMap((Table<String, TopicPartition, Long> committedOffsets) -> {\n          // 2. getting end offsets for partitions with committed offsets\n          return ac.listOffsets(committedOffsets.columnKeySet(), OffsetSpec.latest(), false)\n              .map(endOffsets ->\n                  descriptions.stream()\n                      .map(desc -> {\n                        var groupOffsets = committedOffsets.row(desc.groupId());\n                        var endOffsetsForGroup = new HashMap<>(endOffsets);\n                        endOffsetsForGroup.keySet().retainAll(groupOffsets.keySet());\n                        // 3. gathering description & offsets\n                        return InternalConsumerGroup.create(desc, groupOffsets, endOffsetsForGroup);\n                      })\n                      .collect(Collectors.toList()));\n        });\n  }\n\n  public Mono<List<InternalTopicConsumerGroup>> getConsumerGroupsForTopic(KafkaCluster cluster,\n                                                                          String topic) {\n    return adminClientService.get(cluster)\n        // 1. getting topic's end offsets\n        .flatMap(ac -> ac.listTopicOffsets(topic, OffsetSpec.latest(), false)\n            .flatMap(endOffsets -> {\n              var tps = new ArrayList<>(endOffsets.keySet());\n              // 2. getting all consumer groups\n              return describeConsumerGroups(ac)\n                  .flatMap((List<ConsumerGroupDescription> groups) -> {\n                        // 3. trying to find committed offsets for topic\n                        var groupNames = groups.stream().map(ConsumerGroupDescription::groupId).toList();\n                        return ac.listConsumerGroupOffsets(groupNames, tps).map(offsets ->\n                            groups.stream()\n                                // 4. keeping only groups that relates to topic\n                                .filter(g -> isConsumerGroupRelatesToTopic(topic, g, offsets.containsRow(g.groupId())))\n                                .map(g ->\n                                    // 5. constructing results\n                                    InternalTopicConsumerGroup.create(topic, g, offsets.row(g.groupId()), endOffsets))\n                                .toList()\n                        );\n                      }\n                  );\n            }));\n  }\n\n  private boolean isConsumerGroupRelatesToTopic(String topic,\n                                                ConsumerGroupDescription description,\n                                                boolean hasCommittedOffsets) {\n    boolean hasActiveMembersForTopic = description.members()\n        .stream()\n        .anyMatch(m -> m.assignment().topicPartitions().stream().anyMatch(tp -> tp.topic().equals(topic)));\n    return hasActiveMembersForTopic || hasCommittedOffsets;\n  }\n\n  public record ConsumerGroupsPage(List<InternalConsumerGroup> consumerGroups, int totalPages) {\n  }\n\n  private record GroupWithDescr(InternalConsumerGroup icg, ConsumerGroupDescription cgd) {\n  }\n\n  public Mono<ConsumerGroupsPage> getConsumerGroupsPage(\n      KafkaCluster cluster,\n      int pageNum,\n      int perPage,\n      @Nullable String search,\n      ConsumerGroupOrderingDTO orderBy,\n      SortOrderDTO sortOrderDto) {\n    return adminClientService.get(cluster).flatMap(ac ->\n        ac.listConsumerGroups()\n            .map(listing -> search == null\n                ? listing\n                : listing.stream()\n                .filter(g -> StringUtils.containsIgnoreCase(g.groupId(), search))\n                .toList()\n            )\n            .flatMapIterable(lst -> lst)\n            .filterWhen(cg -> accessControlService.isConsumerGroupAccessible(cg.groupId(), cluster.getName()))\n            .collectList()\n            .flatMap(allGroups ->\n                loadSortedDescriptions(ac, allGroups, pageNum, perPage, orderBy, sortOrderDto)\n                    .flatMap(descriptions -> getConsumerGroups(ac, descriptions)\n                        .map(page -> new ConsumerGroupsPage(\n                            page,\n                            (allGroups.size() / perPage) + (allGroups.size() % perPage == 0 ? 0 : 1))))));\n  }\n\n  private Mono<List<ConsumerGroupDescription>> loadSortedDescriptions(ReactiveAdminClient ac,\n                                                                      List<ConsumerGroupListing> groups,\n                                                                      int pageNum,\n                                                                      int perPage,\n                                                                      ConsumerGroupOrderingDTO orderBy,\n                                                                      SortOrderDTO sortOrderDto) {\n    return switch (orderBy) {\n      case NAME -> {\n        Comparator<ConsumerGroupListing> comparator = Comparator.comparing(ConsumerGroupListing::groupId);\n        yield loadDescriptionsByListings(ac, groups, comparator, pageNum, perPage, sortOrderDto);\n      }\n      case STATE -> {\n        ToIntFunction<ConsumerGroupListing> statesPriorities =\n            cg -> switch (cg.state().orElse(ConsumerGroupState.UNKNOWN)) {\n              case STABLE -> 0;\n              case COMPLETING_REBALANCE -> 1;\n              case PREPARING_REBALANCE -> 2;\n              case EMPTY -> 3;\n              case DEAD -> 4;\n              case UNKNOWN -> 5;\n            };\n        var comparator = Comparator.comparingInt(statesPriorities);\n        yield loadDescriptionsByListings(ac, groups, comparator, pageNum, perPage, sortOrderDto);\n      }\n      case MEMBERS -> {\n        var comparator = Comparator.<ConsumerGroupDescription>comparingInt(cg -> cg.members().size());\n        var groupNames = groups.stream().map(ConsumerGroupListing::groupId).toList();\n        yield ac.describeConsumerGroups(groupNames)\n            .map(descriptions ->\n                sortAndPaginate(descriptions.values(), comparator, pageNum, perPage, sortOrderDto).toList());\n      }\n      case MESSAGES_BEHIND -> {\n\n        Comparator<GroupWithDescr> comparator = Comparator.comparingLong(gwd ->\n            gwd.icg.getConsumerLag() == null ? 0L : gwd.icg.getConsumerLag());\n\n        yield loadDescriptionsByInternalConsumerGroups(ac, groups, comparator, pageNum, perPage, sortOrderDto);\n      }\n\n      case TOPIC_NUM -> {\n\n        Comparator<GroupWithDescr> comparator = Comparator.comparingInt(gwd -> gwd.icg.getTopicNum());\n\n        yield loadDescriptionsByInternalConsumerGroups(ac, groups, comparator, pageNum, perPage, sortOrderDto);\n\n      }\n    };\n  }\n\n  private Mono<List<ConsumerGroupDescription>> loadDescriptionsByListings(ReactiveAdminClient ac,\n                                                                          List<ConsumerGroupListing> listings,\n                                                                          Comparator<ConsumerGroupListing> comparator,\n                                                                          int pageNum,\n                                                                          int perPage,\n                                                                          SortOrderDTO sortOrderDto) {\n    List<String> sortedGroups = sortAndPaginate(listings, comparator, pageNum, perPage, sortOrderDto)\n        .map(ConsumerGroupListing::groupId)\n        .toList();\n    return ac.describeConsumerGroups(sortedGroups)\n        .map(descrMap -> sortedGroups.stream().map(descrMap::get).toList());\n  }\n\n  private <T> Stream<T> sortAndPaginate(Collection<T> collection,\n                                        Comparator<T> comparator,\n                                        int pageNum,\n                                        int perPage,\n                                        SortOrderDTO sortOrderDto) {\n    return collection.stream()\n        .sorted(sortOrderDto == SortOrderDTO.ASC ? comparator : comparator.reversed())\n        .skip((long) (pageNum - 1) * perPage)\n        .limit(perPage);\n  }\n\n  private Mono<List<ConsumerGroupDescription>> describeConsumerGroups(ReactiveAdminClient ac) {\n    return ac.listConsumerGroupNames()\n        .flatMap(ac::describeConsumerGroups)\n        .map(cgs -> new ArrayList<>(cgs.values()));\n  }\n\n\n  private Mono<List<ConsumerGroupDescription>> loadDescriptionsByInternalConsumerGroups(ReactiveAdminClient ac,\n                                                                                  List<ConsumerGroupListing> groups,\n                                                                                  Comparator<GroupWithDescr> comparator,\n                                                                                  int pageNum,\n                                                                                  int perPage,\n                                                                                  SortOrderDTO sortOrderDto) {\n    var groupNames = groups.stream().map(ConsumerGroupListing::groupId).toList();\n\n    return ac.describeConsumerGroups(groupNames)\n        .flatMap(descriptionsMap -> {\n              List<ConsumerGroupDescription> descriptions = descriptionsMap.values().stream().toList();\n              return getConsumerGroups(ac, descriptions)\n                  .map(icg -> Streams.zip(icg.stream(), descriptions.stream(), GroupWithDescr::new).toList())\n                  .map(gwd -> sortAndPaginate(gwd, comparator, pageNum, perPage, sortOrderDto)\n                      .map(GroupWithDescr::cgd).toList());\n            }\n        );\n\n  }\n\n  public Mono<InternalConsumerGroup> getConsumerGroupDetail(KafkaCluster cluster,\n                                                            String consumerGroupId) {\n    return adminClientService.get(cluster)\n        .flatMap(ac -> ac.describeConsumerGroups(List.of(consumerGroupId))\n            .filter(m -> m.containsKey(consumerGroupId))\n            .map(r -> r.get(consumerGroupId))\n            .flatMap(descr ->\n                getConsumerGroups(ac, List.of(descr))\n                    .filter(groups -> !groups.isEmpty())\n                    .map(groups -> groups.get(0))));\n  }\n\n  public Mono<Void> deleteConsumerGroupById(KafkaCluster cluster,\n                                            String groupId) {\n    return adminClientService.get(cluster)\n        .flatMap(adminClient -> adminClient.deleteConsumerGroups(List.of(groupId)));\n  }\n\n  public EnhancedConsumer createConsumer(KafkaCluster cluster) {\n    return createConsumer(cluster, Map.of());\n  }\n\n  public EnhancedConsumer createConsumer(KafkaCluster cluster,\n                                         Map<String, Object> properties) {\n    Properties props = new Properties();\n    SslPropertiesUtil.addKafkaSslProperties(cluster.getOriginalProperties().getSsl(), props);\n    props.putAll(cluster.getProperties());\n    props.put(ConsumerConfig.CLIENT_ID_CONFIG, \"kafka-ui-consumer-\" + System.currentTimeMillis());\n    props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers());\n    props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, \"earliest\");\n    props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, \"false\");\n    props.put(ConsumerConfig.ALLOW_AUTO_CREATE_TOPICS_CONFIG, \"false\");\n    props.putAll(properties);\n\n    return new EnhancedConsumer(\n        props,\n        cluster.getPollingSettings().getPollingThrottler(),\n        ApplicationMetrics.forCluster(cluster)\n    );\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/DeserializationService.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.SerdeDescriptionDTO;\nimport com.provectus.kafka.ui.serde.api.SchemaDescription;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport com.provectus.kafka.ui.serdes.ClusterSerdes;\nimport com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer;\nimport com.provectus.kafka.ui.serdes.ProducerRecordCreator;\nimport com.provectus.kafka.ui.serdes.SerdeInstance;\nimport com.provectus.kafka.ui.serdes.SerdesInitializer;\nimport java.io.Closeable;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport javax.annotation.Nullable;\nimport javax.validation.ValidationException;\nimport org.springframework.core.env.Environment;\nimport org.springframework.stereotype.Component;\n\n/**\n * Class is responsible for managing serdes for kafka clusters.\n * NOTE: Since Serde interface is designed to be blocking it is required that DeserializationService\n * (and all Serde-related code) calls executed within special thread pool (boundedElastic).\n */\n@Component\npublic class DeserializationService implements Closeable {\n\n  private final Map<String, ClusterSerdes> clusterSerdes = new ConcurrentHashMap<>();\n\n  public DeserializationService(Environment env,\n                                ClustersStorage clustersStorage,\n                                ClustersProperties clustersProperties) {\n    var serdesInitializer = new SerdesInitializer();\n    for (int i = 0; i < clustersProperties.getClusters().size(); i++) {\n      var clusterProperties = clustersProperties.getClusters().get(i);\n      var cluster = clustersStorage.getClusterByName(clusterProperties.getName()).get();\n      clusterSerdes.put(cluster.getName(), serdesInitializer.init(env, clustersProperties, i));\n    }\n  }\n\n  private ClusterSerdes getSerdesFor(KafkaCluster cluster) {\n    return clusterSerdes.get(cluster.getName());\n  }\n\n  private Serde.Serializer getSerializer(KafkaCluster cluster,\n                                         String topic,\n                                         Serde.Target type,\n                                         String serdeName) {\n    var serdes = getSerdesFor(cluster);\n    var serde = serdes.serdeForName(serdeName)\n        .orElseThrow(() -> new ValidationException(\n            String.format(\"Serde %s not found\", serdeName)));\n    if (!serde.canSerialize(topic, type)) {\n      throw new ValidationException(\n          String.format(\"Serde %s can't be applied for '%s' topic's %s serialization\", serde, topic, type));\n    }\n    return serde.serializer(topic, type);\n  }\n\n  private SerdeInstance getSerdeForDeserialize(KafkaCluster cluster,\n                                               String topic,\n                                               Serde.Target type,\n                                               @Nullable String serdeName) {\n    var serdes = getSerdesFor(cluster);\n    if (serdeName != null) {\n      var serde = serdes.serdeForName(serdeName)\n          .orElseThrow(() -> new ValidationException(String.format(\"Serde '%s' not found\", serdeName)));\n      if (!serde.canDeserialize(topic, type)) {\n        throw new ValidationException(\n            String.format(\"Serde '%s' can't be applied to '%s' topic %s\", serdeName, topic, type));\n      }\n      return serde;\n    } else {\n      return serdes.suggestSerdeForDeserialize(topic, type);\n    }\n  }\n\n  public ProducerRecordCreator producerRecordCreator(KafkaCluster cluster,\n                                                     String topic,\n                                                     String keySerdeName,\n                                                     String valueSerdeName) {\n    return new ProducerRecordCreator(\n        getSerializer(cluster, topic, Serde.Target.KEY, keySerdeName),\n        getSerializer(cluster, topic, Serde.Target.VALUE, valueSerdeName)\n    );\n  }\n\n  public ConsumerRecordDeserializer deserializerFor(KafkaCluster cluster,\n                                                    String topic,\n                                                    @Nullable String keySerdeName,\n                                                    @Nullable String valueSerdeName) {\n    var keySerde = getSerdeForDeserialize(cluster, topic, Serde.Target.KEY, keySerdeName);\n    var valueSerde = getSerdeForDeserialize(cluster, topic, Serde.Target.VALUE, valueSerdeName);\n    var fallbackSerde = getSerdesFor(cluster).getFallbackSerde();\n    return new ConsumerRecordDeserializer(\n        keySerde.getName(),\n        keySerde.deserializer(topic, Serde.Target.KEY),\n        valueSerde.getName(),\n        valueSerde.deserializer(topic, Serde.Target.VALUE),\n        fallbackSerde.getName(),\n        fallbackSerde.deserializer(topic, Serde.Target.KEY),\n        fallbackSerde.deserializer(topic, Serde.Target.VALUE),\n        cluster.getMasking().getMaskerForTopic(topic)\n    );\n  }\n\n  public List<SerdeDescriptionDTO> getSerdesForSerialize(KafkaCluster cluster,\n                                                         String topic,\n                                                         Serde.Target serdeType) {\n    var serdes = getSerdesFor(cluster);\n    var preferred = serdes.suggestSerdeForSerialize(topic, serdeType);\n    var result = new ArrayList<SerdeDescriptionDTO>();\n    result.add(toDto(preferred, topic, serdeType, true));\n    serdes.all()\n        .filter(s -> !s.getName().equals(preferred.getName()))\n        .filter(s -> s.canSerialize(topic, serdeType))\n        .forEach(s -> result.add(toDto(s, topic, serdeType, false)));\n    return result;\n  }\n\n  public List<SerdeDescriptionDTO> getSerdesForDeserialize(KafkaCluster cluster,\n                                                           String topic,\n                                                           Serde.Target serdeType) {\n    var serdes = getSerdesFor(cluster);\n    var preferred = serdes.suggestSerdeForDeserialize(topic, serdeType);\n    var result = new ArrayList<SerdeDescriptionDTO>();\n    result.add(toDto(preferred, topic, serdeType, true));\n    serdes.all()\n        .filter(s -> !s.getName().equals(preferred.getName()))\n        .filter(s -> s.canDeserialize(topic, serdeType))\n        .forEach(s -> result.add(toDto(s, topic, serdeType, false)));\n    return result;\n  }\n\n  private SerdeDescriptionDTO toDto(SerdeInstance serdeInstance,\n                                    String topic,\n                                    Serde.Target serdeType,\n                                    boolean preferred) {\n    var schemaOpt = serdeInstance.getSchema(topic, serdeType);\n    return new SerdeDescriptionDTO()\n        .name(serdeInstance.getName())\n        .description(serdeInstance.description().orElse(null))\n        .schema(schemaOpt.map(SchemaDescription::getSchema).orElse(null))\n        .additionalProperties(schemaOpt.map(SchemaDescription::getAdditionalProperties).orElse(null))\n        .preferred(preferred);\n  }\n\n  @Override\n  public void close() {\n    clusterSerdes.values().forEach(ClusterSerdes::close);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/FeatureService.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport com.provectus.kafka.ui.model.ClusterFeature;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.service.ReactiveAdminClient.ClusterDescription;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.function.Predicate;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.kafka.common.acl.AclOperation;\nimport org.springframework.stereotype.Service;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\n@Service\n@Slf4j\npublic class FeatureService {\n\n  public Mono<List<ClusterFeature>> getAvailableFeatures(ReactiveAdminClient adminClient,\n                                                         KafkaCluster cluster,\n                                                         ClusterDescription clusterDescription) {\n    List<Mono<ClusterFeature>> features = new ArrayList<>();\n\n    if (Optional.ofNullable(cluster.getConnectsClients())\n        .filter(Predicate.not(Map::isEmpty))\n        .isPresent()) {\n      features.add(Mono.just(ClusterFeature.KAFKA_CONNECT));\n    }\n\n    if (cluster.getKsqlClient() != null) {\n      features.add(Mono.just(ClusterFeature.KSQL_DB));\n    }\n\n    if (cluster.getSchemaRegistryClient() != null) {\n      features.add(Mono.just(ClusterFeature.SCHEMA_REGISTRY));\n    }\n\n    features.add(topicDeletionEnabled(adminClient));\n    features.add(aclView(adminClient));\n    features.add(aclEdit(adminClient, clusterDescription));\n\n    return Flux.fromIterable(features).flatMap(m -> m).collectList();\n  }\n\n  private Mono<ClusterFeature> topicDeletionEnabled(ReactiveAdminClient adminClient) {\n    return adminClient.isTopicDeletionEnabled()\n        ? Mono.just(ClusterFeature.TOPIC_DELETION)\n        : Mono.empty();\n  }\n\n  private Mono<ClusterFeature> aclEdit(ReactiveAdminClient adminClient, ClusterDescription clusterDescription) {\n    var authorizedOps = Optional.ofNullable(clusterDescription.getAuthorizedOperations()).orElse(Set.of());\n    boolean canEdit = aclViewEnabled(adminClient)\n        && (authorizedOps.contains(AclOperation.ALL) || authorizedOps.contains(AclOperation.ALTER));\n    return canEdit\n        ? Mono.just(ClusterFeature.KAFKA_ACL_EDIT)\n        : Mono.empty();\n  }\n\n  private Mono<ClusterFeature> aclView(ReactiveAdminClient adminClient) {\n    return aclViewEnabled(adminClient)\n        ? Mono.just(ClusterFeature.KAFKA_ACL_VIEW)\n        : Mono.empty();\n  }\n\n  private boolean aclViewEnabled(ReactiveAdminClient adminClient) {\n    return adminClient.getClusterFeatures().contains(ReactiveAdminClient.SupportedFeature.AUTHORIZED_SECURITY_ENABLED);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaClusterFactory.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport com.provectus.kafka.ui.client.RetryingKafkaConnectClient;\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.config.WebclientProperties;\nimport com.provectus.kafka.ui.connect.api.KafkaConnectClientApi;\nimport com.provectus.kafka.ui.emitter.PollingSettings;\nimport com.provectus.kafka.ui.model.ApplicationPropertyValidationDTO;\nimport com.provectus.kafka.ui.model.ClusterConfigValidationDTO;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.MetricsConfig;\nimport com.provectus.kafka.ui.service.ksql.KsqlApiClient;\nimport com.provectus.kafka.ui.service.masking.DataMasking;\nimport com.provectus.kafka.ui.sr.ApiClient;\nimport com.provectus.kafka.ui.sr.api.KafkaSrClientApi;\nimport com.provectus.kafka.ui.util.KafkaServicesValidation;\nimport com.provectus.kafka.ui.util.ReactiveFailover;\nimport com.provectus.kafka.ui.util.WebClientConfigurator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Properties;\nimport java.util.stream.Stream;\nimport javax.annotation.Nullable;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.unit.DataSize;\nimport org.springframework.web.reactive.function.client.WebClient;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.util.function.Tuple2;\nimport reactor.util.function.Tuples;\n\n@Service\n@Slf4j\npublic class KafkaClusterFactory {\n\n  private static final DataSize DEFAULT_WEBCLIENT_BUFFER = DataSize.parse(\"20MB\");\n\n  private final DataSize webClientMaxBuffSize;\n\n  public KafkaClusterFactory(WebclientProperties webclientProperties) {\n    this.webClientMaxBuffSize = Optional.ofNullable(webclientProperties.getMaxInMemoryBufferSize())\n        .map(DataSize::parse)\n        .orElse(DEFAULT_WEBCLIENT_BUFFER);\n  }\n\n  public KafkaCluster create(ClustersProperties properties,\n                             ClustersProperties.Cluster clusterProperties) {\n    KafkaCluster.KafkaClusterBuilder builder = KafkaCluster.builder();\n\n    builder.name(clusterProperties.getName());\n    builder.bootstrapServers(clusterProperties.getBootstrapServers());\n    builder.properties(convertProperties(clusterProperties.getProperties()));\n    builder.readOnly(clusterProperties.isReadOnly());\n    builder.masking(DataMasking.create(clusterProperties.getMasking()));\n    builder.pollingSettings(PollingSettings.create(clusterProperties, properties));\n\n    if (schemaRegistryConfigured(clusterProperties)) {\n      builder.schemaRegistryClient(schemaRegistryClient(clusterProperties));\n    }\n    if (connectClientsConfigured(clusterProperties)) {\n      builder.connectsClients(connectClients(clusterProperties));\n    }\n    if (ksqlConfigured(clusterProperties)) {\n      builder.ksqlClient(ksqlClient(clusterProperties));\n    }\n    if (metricsConfigured(clusterProperties)) {\n      builder.metricsConfig(metricsConfigDataToMetricsConfig(clusterProperties.getMetrics()));\n    }\n    builder.originalProperties(clusterProperties);\n    return builder.build();\n  }\n\n  public Mono<ClusterConfigValidationDTO> validate(ClustersProperties.Cluster clusterProperties) {\n    if (clusterProperties.getSsl() != null) {\n      Optional<String> errMsg = KafkaServicesValidation.validateTruststore(clusterProperties.getSsl());\n      if (errMsg.isPresent()) {\n        return Mono.just(new ClusterConfigValidationDTO()\n            .kafka(new ApplicationPropertyValidationDTO()\n                .error(true)\n                .errorMessage(\"Truststore not valid: \" + errMsg.get())));\n      }\n    }\n\n    return Mono.zip(\n        KafkaServicesValidation.validateClusterConnection(\n            clusterProperties.getBootstrapServers(),\n            convertProperties(clusterProperties.getProperties()),\n            clusterProperties.getSsl()\n        ),\n        schemaRegistryConfigured(clusterProperties)\n            ? KafkaServicesValidation.validateSchemaRegistry(\n                () -> schemaRegistryClient(clusterProperties)).map(Optional::of)\n            : Mono.<Optional<ApplicationPropertyValidationDTO>>just(Optional.empty()),\n\n        ksqlConfigured(clusterProperties)\n            ? KafkaServicesValidation.validateKsql(() -> ksqlClient(clusterProperties)).map(Optional::of)\n            : Mono.<Optional<ApplicationPropertyValidationDTO>>just(Optional.empty()),\n\n        connectClientsConfigured(clusterProperties)\n            ?\n            Flux.fromIterable(clusterProperties.getKafkaConnect())\n                .flatMap(c ->\n                    KafkaServicesValidation.validateConnect(() -> connectClient(clusterProperties, c))\n                        .map(r -> Tuples.of(c.getName(), r)))\n                .collectMap(Tuple2::getT1, Tuple2::getT2)\n                .map(Optional::of)\n            :\n            Mono.<Optional<Map<String, ApplicationPropertyValidationDTO>>>just(Optional.empty())\n    ).map(tuple -> {\n      var validation = new ClusterConfigValidationDTO();\n      validation.kafka(tuple.getT1());\n      tuple.getT2().ifPresent(validation::schemaRegistry);\n      tuple.getT3().ifPresent(validation::ksqldb);\n      tuple.getT4().ifPresent(validation::kafkaConnects);\n      return validation;\n    });\n  }\n\n  private Properties convertProperties(Map<String, Object> propertiesMap) {\n    Properties properties = new Properties();\n    if (propertiesMap != null) {\n      properties.putAll(propertiesMap);\n    }\n    return properties;\n  }\n\n  private boolean connectClientsConfigured(ClustersProperties.Cluster clusterProperties) {\n    return clusterProperties.getKafkaConnect() != null;\n  }\n\n  private Map<String, ReactiveFailover<KafkaConnectClientApi>> connectClients(\n      ClustersProperties.Cluster clusterProperties) {\n    Map<String, ReactiveFailover<KafkaConnectClientApi>> connects = new HashMap<>();\n    clusterProperties.getKafkaConnect().forEach(c -> connects.put(c.getName(), connectClient(clusterProperties, c)));\n    return connects;\n  }\n\n  private ReactiveFailover<KafkaConnectClientApi> connectClient(ClustersProperties.Cluster cluster,\n                                                                ClustersProperties.ConnectCluster connectCluster) {\n    return ReactiveFailover.create(\n        parseUrlList(connectCluster.getAddress()),\n        url -> new RetryingKafkaConnectClient(\n            connectCluster.toBuilder().address(url).build(),\n            cluster.getSsl(),\n            webClientMaxBuffSize\n        ),\n        ReactiveFailover.CONNECTION_REFUSED_EXCEPTION_FILTER,\n        \"No alive connect instances available\",\n        ReactiveFailover.DEFAULT_RETRY_GRACE_PERIOD_MS\n    );\n  }\n\n  private boolean schemaRegistryConfigured(ClustersProperties.Cluster clusterProperties) {\n    return clusterProperties.getSchemaRegistry() != null;\n  }\n\n  private ReactiveFailover<KafkaSrClientApi> schemaRegistryClient(ClustersProperties.Cluster clusterProperties) {\n    var auth = Optional.ofNullable(clusterProperties.getSchemaRegistryAuth())\n        .orElse(new ClustersProperties.SchemaRegistryAuth());\n    WebClient webClient = new WebClientConfigurator()\n        .configureSsl(clusterProperties.getSsl(), clusterProperties.getSchemaRegistrySsl())\n        .configureBasicAuth(auth.getUsername(), auth.getPassword())\n        .configureBufferSize(webClientMaxBuffSize)\n        .build();\n    return ReactiveFailover.create(\n        parseUrlList(clusterProperties.getSchemaRegistry()),\n        url -> new KafkaSrClientApi(new ApiClient(webClient, null, null).setBasePath(url)),\n        ReactiveFailover.CONNECTION_REFUSED_EXCEPTION_FILTER,\n        \"No live schemaRegistry instances available\",\n        ReactiveFailover.DEFAULT_RETRY_GRACE_PERIOD_MS\n    );\n  }\n\n  private boolean ksqlConfigured(ClustersProperties.Cluster clusterProperties) {\n    return clusterProperties.getKsqldbServer() != null;\n  }\n\n  private ReactiveFailover<KsqlApiClient> ksqlClient(ClustersProperties.Cluster clusterProperties) {\n    return ReactiveFailover.create(\n        parseUrlList(clusterProperties.getKsqldbServer()),\n        url -> new KsqlApiClient(\n            url,\n            clusterProperties.getKsqldbServerAuth(),\n            clusterProperties.getSsl(),\n            clusterProperties.getKsqldbServerSsl(),\n            webClientMaxBuffSize\n        ),\n        ReactiveFailover.CONNECTION_REFUSED_EXCEPTION_FILTER,\n        \"No live ksqldb instances available\",\n        ReactiveFailover.DEFAULT_RETRY_GRACE_PERIOD_MS\n    );\n  }\n\n  private List<String> parseUrlList(String url) {\n    return Stream.of(url.split(\",\")).map(String::trim).filter(s -> !s.isBlank()).toList();\n  }\n\n  private boolean metricsConfigured(ClustersProperties.Cluster clusterProperties) {\n    return clusterProperties.getMetrics() != null;\n  }\n\n  @Nullable\n  private MetricsConfig metricsConfigDataToMetricsConfig(ClustersProperties.MetricsConfigData metricsConfigData) {\n    if (metricsConfigData == null) {\n      return null;\n    }\n    MetricsConfig.MetricsConfigBuilder builder = MetricsConfig.builder();\n    builder.type(metricsConfigData.getType());\n    builder.port(metricsConfigData.getPort());\n    builder.ssl(Optional.ofNullable(metricsConfigData.getSsl()).orElse(false));\n    builder.username(metricsConfigData.getUsername());\n    builder.password(metricsConfigData.getPassword());\n    builder.keystoreLocation(metricsConfigData.getKeystoreLocation());\n    builder.keystorePassword(metricsConfigData.getKeystorePassword());\n    return builder.build();\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConfigSanitizer.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport static java.util.regex.Pattern.CASE_INSENSITIVE;\n\nimport com.google.common.collect.ImmutableList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\nimport javax.annotation.Nullable;\nimport org.apache.kafka.common.config.ConfigDef;\nimport org.apache.kafka.common.config.SaslConfigs;\nimport org.apache.kafka.common.config.SslConfigs;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Component;\n\n@Component\nclass KafkaConfigSanitizer {\n\n  private static final String SANITIZED_VALUE = \"******\";\n\n  private static final String[] REGEX_PARTS = {\"*\", \"$\", \"^\", \"+\"};\n\n  private static final List<String> DEFAULT_PATTERNS_TO_SANITIZE = ImmutableList.<String>builder()\n      .addAll(kafkaConfigKeysToSanitize())\n      .add(\n          \"basic.auth.user.info\",  /* For Schema Registry credentials */\n          \"password\", \"secret\", \"token\", \"key\", \".*credentials.*\",   /* General credential patterns */\n          \"aws.access.*\", \"aws.secret.*\", \"aws.session.*\"   /* AWS-related credential patterns */\n      )\n      .build();\n\n  private final List<Pattern> sanitizeKeysPatterns;\n\n  KafkaConfigSanitizer(\n      @Value(\"${kafka.config.sanitizer.enabled:true}\") boolean enabled,\n      @Value(\"${kafka.config.sanitizer.patterns:}\") List<String> patternsToSanitize\n  ) {\n    this.sanitizeKeysPatterns = enabled\n        ? compile(patternsToSanitize.isEmpty() ? DEFAULT_PATTERNS_TO_SANITIZE : patternsToSanitize)\n        : List.of();\n  }\n\n  private static List<Pattern> compile(Collection<String> patternStrings) {\n    return patternStrings.stream()\n        .map(p -> isRegex(p)\n            ? Pattern.compile(p, CASE_INSENSITIVE)\n            : Pattern.compile(\".*\" + p + \"$\", CASE_INSENSITIVE))\n        .toList();\n  }\n\n  private static boolean isRegex(String str) {\n    return Arrays.stream(REGEX_PARTS).anyMatch(str::contains);\n  }\n\n  private static Set<String> kafkaConfigKeysToSanitize() {\n    final ConfigDef configDef = new ConfigDef();\n    SslConfigs.addClientSslSupport(configDef);\n    SaslConfigs.addClientSaslSupport(configDef);\n    return configDef.configKeys().entrySet().stream()\n        .filter(entry -> entry.getValue().type().equals(ConfigDef.Type.PASSWORD))\n        .map(Map.Entry::getKey)\n        .collect(Collectors.toSet());\n  }\n\n  @Nullable\n  public Object sanitize(String key, @Nullable Object value) {\n    for (Pattern pattern : sanitizeKeysPatterns) {\n      if (pattern.matcher(key).matches()) {\n        return SANITIZED_VALUE;\n      }\n    }\n    return value;\n  }\n\n  public Map<String, Object> sanitizeConnectorConfig(@Nullable Map<String, Object> original) {\n    var result = new HashMap<String, Object>(); //null-values supporting map!\n    if (original != null) {\n      original.forEach((k, v) -> result.put(k, sanitize(k, v)));\n    }\n    return result;\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConnectService.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.provectus.kafka.ui.connect.api.KafkaConnectClientApi;\nimport com.provectus.kafka.ui.connect.model.ConnectorStatus;\nimport com.provectus.kafka.ui.connect.model.ConnectorStatusConnector;\nimport com.provectus.kafka.ui.connect.model.ConnectorTopics;\nimport com.provectus.kafka.ui.connect.model.TaskStatus;\nimport com.provectus.kafka.ui.exception.NotFoundException;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.mapper.ClusterMapper;\nimport com.provectus.kafka.ui.mapper.KafkaConnectMapper;\nimport com.provectus.kafka.ui.model.ConnectDTO;\nimport com.provectus.kafka.ui.model.ConnectorActionDTO;\nimport com.provectus.kafka.ui.model.ConnectorDTO;\nimport com.provectus.kafka.ui.model.ConnectorPluginConfigValidationResponseDTO;\nimport com.provectus.kafka.ui.model.ConnectorPluginDTO;\nimport com.provectus.kafka.ui.model.ConnectorStateDTO;\nimport com.provectus.kafka.ui.model.ConnectorTaskStatusDTO;\nimport com.provectus.kafka.ui.model.FullConnectorInfoDTO;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.NewConnectorDTO;\nimport com.provectus.kafka.ui.model.TaskDTO;\nimport com.provectus.kafka.ui.model.connect.InternalConnectInfo;\nimport com.provectus.kafka.ui.util.ReactiveFailover;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.function.Predicate;\nimport java.util.stream.Stream;\nimport javax.annotation.Nullable;\nimport lombok.RequiredArgsConstructor;\nimport lombok.SneakyThrows;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Service;\nimport org.springframework.web.reactive.function.client.WebClientResponseException;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\n@Service\n@Slf4j\n@RequiredArgsConstructor\npublic class KafkaConnectService {\n  private final ClusterMapper clusterMapper;\n  private final KafkaConnectMapper kafkaConnectMapper;\n  private final ObjectMapper objectMapper;\n  private final KafkaConfigSanitizer kafkaConfigSanitizer;\n\n  public Flux<ConnectDTO> getConnects(KafkaCluster cluster) {\n    return Flux.fromIterable(\n        Optional.ofNullable(cluster.getOriginalProperties().getKafkaConnect())\n            .map(lst -> lst.stream().map(clusterMapper::toKafkaConnect).toList())\n            .orElse(List.of())\n    );\n  }\n\n  public Flux<FullConnectorInfoDTO> getAllConnectors(final KafkaCluster cluster,\n                                                     @Nullable final String search) {\n    return getConnects(cluster)\n        .flatMap(connect ->\n            getConnectorNamesWithErrorsSuppress(cluster, connect.getName())\n                .flatMap(connectorName ->\n                    Mono.zip(\n                        getConnector(cluster, connect.getName(), connectorName),\n                        getConnectorConfig(cluster, connect.getName(), connectorName),\n                        getConnectorTasks(cluster, connect.getName(), connectorName).collectList(),\n                        getConnectorTopics(cluster, connect.getName(), connectorName)\n                    ).map(tuple ->\n                        InternalConnectInfo.builder()\n                            .connector(tuple.getT1())\n                            .config(tuple.getT2())\n                            .tasks(tuple.getT3())\n                            .topics(tuple.getT4().getTopics())\n                            .build())))\n        .map(kafkaConnectMapper::fullConnectorInfo)\n        .filter(matchesSearchTerm(search));\n  }\n\n  private Predicate<FullConnectorInfoDTO> matchesSearchTerm(@Nullable final String search) {\n    if (search == null) {\n      return c -> true;\n    }\n    return connector -> getStringsForSearch(connector)\n        .anyMatch(string -> StringUtils.containsIgnoreCase(string, search));\n  }\n\n  private Stream<String> getStringsForSearch(FullConnectorInfoDTO fullConnectorInfo) {\n    return Stream.of(\n        fullConnectorInfo.getName(),\n        fullConnectorInfo.getConnect(),\n        fullConnectorInfo.getStatus().getState().getValue(),\n        fullConnectorInfo.getType().getValue());\n  }\n\n  public Mono<ConnectorTopics> getConnectorTopics(KafkaCluster cluster, String connectClusterName,\n                                                  String connectorName) {\n    return api(cluster, connectClusterName)\n        .mono(c -> c.getConnectorTopics(connectorName))\n        .map(result -> result.get(connectorName))\n        // old Connect API versions don't have this endpoint, setting empty list for\n        // backward-compatibility\n        .onErrorResume(Exception.class, e -> Mono.just(new ConnectorTopics().topics(List.of())));\n  }\n\n  public Flux<String> getConnectorNames(KafkaCluster cluster, String connectName) {\n    return api(cluster, connectName)\n        .flux(client -> client.getConnectors(null))\n        // for some reason `getConnectors` method returns the response as a single string\n        .collectList().map(e -> e.get(0))\n        .map(this::parseConnectorsNamesStringToList)\n        .flatMapMany(Flux::fromIterable);\n  }\n\n  // returns empty flux if there was an error communicating with Connect\n  public Flux<String> getConnectorNamesWithErrorsSuppress(KafkaCluster cluster, String connectName) {\n    return getConnectorNames(cluster, connectName).onErrorComplete();\n  }\n\n  @SneakyThrows\n  private List<String> parseConnectorsNamesStringToList(String json) {\n    return objectMapper.readValue(json, new TypeReference<>() {\n    });\n  }\n\n  public Mono<ConnectorDTO> createConnector(KafkaCluster cluster, String connectName,\n                                            Mono<NewConnectorDTO> connector) {\n    return api(cluster, connectName)\n        .mono(client ->\n            connector\n                .flatMap(c -> connectorExists(cluster, connectName, c.getName())\n                    .map(exists -> {\n                      if (Boolean.TRUE.equals(exists)) {\n                        throw new ValidationException(\n                            String.format(\"Connector with name %s already exists\", c.getName()));\n                      }\n                      return c;\n                    }))\n                .map(kafkaConnectMapper::toClient)\n                .flatMap(client::createConnector)\n                .flatMap(c -> getConnector(cluster, connectName, c.getName()))\n        );\n  }\n\n  private Mono<Boolean> connectorExists(KafkaCluster cluster, String connectName,\n                                        String connectorName) {\n    return getConnectorNames(cluster, connectName)\n        .any(name -> name.equals(connectorName));\n  }\n\n  public Mono<ConnectorDTO> getConnector(KafkaCluster cluster, String connectName,\n                                         String connectorName) {\n    return api(cluster, connectName)\n        .mono(client -> client.getConnector(connectorName)\n            .map(kafkaConnectMapper::fromClient)\n            .flatMap(connector ->\n                client.getConnectorStatus(connector.getName())\n                    // status request can return 404 if tasks not assigned yet\n                    .onErrorResume(WebClientResponseException.NotFound.class,\n                        e -> emptyStatus(connectorName))\n                    .map(connectorStatus -> {\n                      var status = connectorStatus.getConnector();\n                      var sanitizedConfig = kafkaConfigSanitizer.sanitizeConnectorConfig(connector.getConfig());\n                      ConnectorDTO result = new ConnectorDTO()\n                          .connect(connectName)\n                          .status(kafkaConnectMapper.fromClient(status))\n                          .type(connector.getType())\n                          .tasks(connector.getTasks())\n                          .name(connector.getName())\n                          .config(sanitizedConfig);\n\n                      if (connectorStatus.getTasks() != null) {\n                        boolean isAnyTaskFailed = connectorStatus.getTasks().stream()\n                            .map(TaskStatus::getState)\n                            .anyMatch(TaskStatus.StateEnum.FAILED::equals);\n\n                        if (isAnyTaskFailed) {\n                          result.getStatus().state(ConnectorStateDTO.TASK_FAILED);\n                        }\n                      }\n                      return result;\n                    })\n            )\n        );\n  }\n\n  private Mono<ConnectorStatus> emptyStatus(String connectorName) {\n    return Mono.just(new ConnectorStatus()\n        .name(connectorName)\n        .tasks(List.of())\n        .connector(new ConnectorStatusConnector()\n            .state(ConnectorStatusConnector.StateEnum.UNASSIGNED)));\n  }\n\n  public Mono<Map<String, Object>> getConnectorConfig(KafkaCluster cluster, String connectName,\n                                                      String connectorName) {\n    return api(cluster, connectName)\n        .mono(c -> c.getConnectorConfig(connectorName))\n        .map(kafkaConfigSanitizer::sanitizeConnectorConfig);\n  }\n\n  public Mono<ConnectorDTO> setConnectorConfig(KafkaCluster cluster, String connectName,\n                                               String connectorName, Mono<Map<String, Object>> requestBody) {\n    return api(cluster, connectName)\n        .mono(c ->\n            requestBody\n                .flatMap(body -> c.setConnectorConfig(connectorName, body))\n                .map(kafkaConnectMapper::fromClient));\n  }\n\n  public Mono<Void> deleteConnector(\n      KafkaCluster cluster, String connectName, String connectorName) {\n    return api(cluster, connectName)\n        .mono(c -> c.deleteConnector(connectorName));\n  }\n\n  public Mono<Void> updateConnectorState(KafkaCluster cluster, String connectName,\n                                         String connectorName, ConnectorActionDTO action) {\n    return api(cluster, connectName)\n        .mono(client -> {\n          switch (action) {\n            case RESTART:\n              return client.restartConnector(connectorName, false, false);\n            case RESTART_ALL_TASKS:\n              return restartTasks(cluster, connectName, connectorName, task -> true);\n            case RESTART_FAILED_TASKS:\n              return restartTasks(cluster, connectName, connectorName,\n                  t -> t.getStatus().getState() == ConnectorTaskStatusDTO.FAILED);\n            case PAUSE:\n              return client.pauseConnector(connectorName);\n            case RESUME:\n              return client.resumeConnector(connectorName);\n            default:\n              throw new IllegalStateException(\"Unexpected value: \" + action);\n          }\n        });\n  }\n\n  private Mono<Void> restartTasks(KafkaCluster cluster, String connectName,\n                                  String connectorName, Predicate<TaskDTO> taskFilter) {\n    return getConnectorTasks(cluster, connectName, connectorName)\n        .filter(taskFilter)\n        .flatMap(t ->\n            restartConnectorTask(cluster, connectName, connectorName, t.getId().getTask()))\n        .then();\n  }\n\n  public Flux<TaskDTO> getConnectorTasks(KafkaCluster cluster, String connectName, String connectorName) {\n    return api(cluster, connectName)\n        .flux(client ->\n            client.getConnectorTasks(connectorName)\n                .onErrorResume(WebClientResponseException.NotFound.class, e -> Flux.empty())\n                .map(kafkaConnectMapper::fromClient)\n                .flatMap(task ->\n                    client\n                        .getConnectorTaskStatus(connectorName, task.getId().getTask())\n                        .onErrorResume(WebClientResponseException.NotFound.class, e -> Mono.empty())\n                        .map(kafkaConnectMapper::fromClient)\n                        .map(task::status)\n                ));\n  }\n\n  public Mono<Void> restartConnectorTask(KafkaCluster cluster, String connectName,\n                                         String connectorName, Integer taskId) {\n    return api(cluster, connectName)\n        .mono(client -> client.restartConnectorTask(connectorName, taskId));\n  }\n\n  public Flux<ConnectorPluginDTO> getConnectorPlugins(KafkaCluster cluster,\n                                                      String connectName) {\n    return api(cluster, connectName)\n        .flux(client -> client.getConnectorPlugins().map(kafkaConnectMapper::fromClient));\n  }\n\n  public Mono<ConnectorPluginConfigValidationResponseDTO> validateConnectorPluginConfig(\n      KafkaCluster cluster, String connectName, String pluginName, Mono<Map<String, Object>> requestBody) {\n    return api(cluster, connectName)\n        .mono(client ->\n            requestBody\n                .flatMap(body ->\n                    client.validateConnectorPluginConfig(pluginName, body))\n                .map(kafkaConnectMapper::fromClient)\n        );\n  }\n\n  private ReactiveFailover<KafkaConnectClientApi> api(KafkaCluster cluster, String connectName) {\n    var client = cluster.getConnectsClients().get(connectName);\n    if (client == null) {\n      throw new NotFoundException(\n          \"Connect %s not found for cluster %s\".formatted(connectName, cluster.getName()));\n    }\n    return client;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/MessagesService.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport com.google.common.util.concurrent.RateLimiter;\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.emitter.BackwardEmitter;\nimport com.provectus.kafka.ui.emitter.ForwardEmitter;\nimport com.provectus.kafka.ui.emitter.MessageFilters;\nimport com.provectus.kafka.ui.emitter.TailingEmitter;\nimport com.provectus.kafka.ui.exception.TopicNotFoundException;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.model.ConsumerPosition;\nimport com.provectus.kafka.ui.model.CreateTopicMessageDTO;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.MessageFilterTypeDTO;\nimport com.provectus.kafka.ui.model.SeekDirectionDTO;\nimport com.provectus.kafka.ui.model.SmartFilterTestExecutionDTO;\nimport com.provectus.kafka.ui.model.SmartFilterTestExecutionResultDTO;\nimport com.provectus.kafka.ui.model.TopicMessageDTO;\nimport com.provectus.kafka.ui.model.TopicMessageEventDTO;\nimport com.provectus.kafka.ui.serdes.ProducerRecordCreator;\nimport com.provectus.kafka.ui.util.SslPropertiesUtil;\nimport java.time.Instant;\nimport java.time.OffsetDateTime;\nimport java.time.ZoneOffset;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Properties;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.function.Predicate;\nimport java.util.function.UnaryOperator;\nimport java.util.stream.Collectors;\nimport javax.annotation.Nullable;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.kafka.clients.admin.OffsetSpec;\nimport org.apache.kafka.clients.admin.TopicDescription;\nimport org.apache.kafka.clients.producer.KafkaProducer;\nimport org.apache.kafka.clients.producer.ProducerConfig;\nimport org.apache.kafka.clients.producer.ProducerRecord;\nimport org.apache.kafka.clients.producer.RecordMetadata;\nimport org.apache.kafka.common.TopicPartition;\nimport org.apache.kafka.common.serialization.ByteArraySerializer;\nimport org.springframework.stereotype.Service;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Schedulers;\n\n@Service\n@Slf4j\npublic class MessagesService {\n\n  private static final int DEFAULT_MAX_PAGE_SIZE = 500;\n  private static final int DEFAULT_PAGE_SIZE = 100;\n  // limiting UI messages rate to 20/sec in tailing mode\n  private static final int TAILING_UI_MESSAGE_THROTTLE_RATE = 20;\n\n  private final AdminClientService adminClientService;\n  private final DeserializationService deserializationService;\n  private final ConsumerGroupService consumerGroupService;\n  private final int maxPageSize;\n  private final int defaultPageSize;\n\n  public MessagesService(AdminClientService adminClientService,\n                         DeserializationService deserializationService,\n                         ConsumerGroupService consumerGroupService,\n                         ClustersProperties properties) {\n    this.adminClientService = adminClientService;\n    this.deserializationService = deserializationService;\n    this.consumerGroupService = consumerGroupService;\n\n    var pollingProps = Optional.ofNullable(properties.getPolling())\n        .orElseGet(ClustersProperties.PollingProperties::new);\n    this.maxPageSize = Optional.ofNullable(pollingProps.getMaxPageSize())\n        .orElse(DEFAULT_MAX_PAGE_SIZE);\n    this.defaultPageSize = Optional.ofNullable(pollingProps.getDefaultPageSize())\n        .orElse(DEFAULT_PAGE_SIZE);\n  }\n\n  private Mono<TopicDescription> withExistingTopic(KafkaCluster cluster, String topicName) {\n    return adminClientService.get(cluster)\n        .flatMap(client -> client.describeTopic(topicName))\n        .switchIfEmpty(Mono.error(new TopicNotFoundException()));\n  }\n\n  public static SmartFilterTestExecutionResultDTO execSmartFilterTest(SmartFilterTestExecutionDTO execData) {\n    Predicate<TopicMessageDTO> predicate;\n    try {\n      predicate = MessageFilters.createMsgFilter(\n          execData.getFilterCode(),\n          MessageFilterTypeDTO.GROOVY_SCRIPT\n      );\n    } catch (Exception e) {\n      log.info(\"Smart filter '{}' compilation error\", execData.getFilterCode(), e);\n      return new SmartFilterTestExecutionResultDTO()\n          .error(\"Compilation error : \" + e.getMessage());\n    }\n    try {\n      var result = predicate.test(\n          new TopicMessageDTO()\n              .key(execData.getKey())\n              .content(execData.getValue())\n              .headers(execData.getHeaders())\n              .offset(execData.getOffset())\n              .partition(execData.getPartition())\n              .timestamp(\n                  Optional.ofNullable(execData.getTimestampMs())\n                      .map(ts -> OffsetDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneOffset.UTC))\n                      .orElse(null))\n      );\n      return new SmartFilterTestExecutionResultDTO()\n          .result(result);\n    } catch (Exception e) {\n      log.info(\"Smart filter {} execution error\", execData, e);\n      return new SmartFilterTestExecutionResultDTO()\n          .error(\"Execution error : \" + e.getMessage());\n    }\n  }\n\n  public Mono<Void> deleteTopicMessages(KafkaCluster cluster, String topicName,\n                                        List<Integer> partitionsToInclude) {\n    return withExistingTopic(cluster, topicName)\n        .flatMap(td ->\n            offsetsForDeletion(cluster, topicName, partitionsToInclude)\n                .flatMap(offsets ->\n                    adminClientService.get(cluster).flatMap(ac -> ac.deleteRecords(offsets))));\n  }\n\n  private Mono<Map<TopicPartition, Long>> offsetsForDeletion(KafkaCluster cluster, String topicName,\n                                                             List<Integer> partitionsToInclude) {\n    return adminClientService.get(cluster).flatMap(ac ->\n        ac.listTopicOffsets(topicName, OffsetSpec.earliest(), true)\n            .zipWith(ac.listTopicOffsets(topicName, OffsetSpec.latest(), true),\n                (start, end) ->\n                    end.entrySet().stream()\n                        .filter(e -> partitionsToInclude.isEmpty()\n                            || partitionsToInclude.contains(e.getKey().partition()))\n                        // we only need non-empty partitions (where start offset != end offset)\n                        .filter(entry -> !entry.getValue().equals(start.get(entry.getKey())))\n                        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))\n    );\n  }\n\n  public Mono<RecordMetadata> sendMessage(KafkaCluster cluster, String topic,\n                                          CreateTopicMessageDTO msg) {\n    return withExistingTopic(cluster, topic)\n        .publishOn(Schedulers.boundedElastic())\n        .flatMap(desc -> sendMessageImpl(cluster, desc, msg));\n  }\n\n  private Mono<RecordMetadata> sendMessageImpl(KafkaCluster cluster,\n                                               TopicDescription topicDescription,\n                                               CreateTopicMessageDTO msg) {\n    if (msg.getPartition() != null\n        && msg.getPartition() > topicDescription.partitions().size() - 1) {\n      return Mono.error(new ValidationException(\"Invalid partition\"));\n    }\n    ProducerRecordCreator producerRecordCreator =\n        deserializationService.producerRecordCreator(\n            cluster,\n            topicDescription.name(),\n            msg.getKeySerde().get(),\n            msg.getValueSerde().get()\n        );\n\n    try (KafkaProducer<byte[], byte[]> producer = createProducer(cluster, Map.of())) {\n      ProducerRecord<byte[], byte[]> producerRecord = producerRecordCreator.create(\n          topicDescription.name(),\n          msg.getPartition(),\n          msg.getKey().orElse(null),\n          msg.getContent().orElse(null),\n          msg.getHeaders()\n      );\n      CompletableFuture<RecordMetadata> cf = new CompletableFuture<>();\n      producer.send(producerRecord, (metadata, exception) -> {\n        if (exception != null) {\n          cf.completeExceptionally(exception);\n        } else {\n          cf.complete(metadata);\n        }\n      });\n      return Mono.fromFuture(cf);\n    } catch (Throwable e) {\n      return Mono.error(e);\n    }\n  }\n\n  public static KafkaProducer<byte[], byte[]> createProducer(KafkaCluster cluster,\n                                                             Map<String, Object> additionalProps) {\n    Properties properties = new Properties();\n    SslPropertiesUtil.addKafkaSslProperties(cluster.getOriginalProperties().getSsl(), properties);\n    properties.putAll(cluster.getProperties());\n    properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers());\n    properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class);\n    properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class);\n    properties.putAll(additionalProps);\n    return new KafkaProducer<>(properties);\n  }\n\n  public Flux<TopicMessageEventDTO> loadMessages(KafkaCluster cluster, String topic,\n                                                 ConsumerPosition consumerPosition,\n                                                 @Nullable String query,\n                                                 MessageFilterTypeDTO filterQueryType,\n                                                 @Nullable Integer pageSize,\n                                                 SeekDirectionDTO seekDirection,\n                                                 @Nullable String keySerde,\n                                                 @Nullable String valueSerde) {\n    return withExistingTopic(cluster, topic)\n        .flux()\n        .publishOn(Schedulers.boundedElastic())\n        .flatMap(td -> loadMessagesImpl(cluster, topic, consumerPosition, query,\n            filterQueryType, fixPageSize(pageSize), seekDirection, keySerde, valueSerde));\n  }\n\n  private int fixPageSize(@Nullable Integer pageSize) {\n    return Optional.ofNullable(pageSize)\n        .filter(ps -> ps > 0 && ps <= maxPageSize)\n        .orElse(defaultPageSize);\n  }\n\n  private Flux<TopicMessageEventDTO> loadMessagesImpl(KafkaCluster cluster,\n                                                      String topic,\n                                                      ConsumerPosition consumerPosition,\n                                                      @Nullable String query,\n                                                      MessageFilterTypeDTO filterQueryType,\n                                                      int limit,\n                                                      SeekDirectionDTO seekDirection,\n                                                      @Nullable String keySerde,\n                                                      @Nullable String valueSerde) {\n\n    var deserializer = deserializationService.deserializerFor(cluster, topic, keySerde, valueSerde);\n    var filter = getMsgFilter(query, filterQueryType);\n    var emitter = switch (seekDirection) {\n      case FORWARD -> new ForwardEmitter(\n          () -> consumerGroupService.createConsumer(cluster),\n          consumerPosition, limit, deserializer, filter, cluster.getPollingSettings()\n      );\n      case BACKWARD -> new BackwardEmitter(\n          () -> consumerGroupService.createConsumer(cluster),\n          consumerPosition, limit, deserializer, filter, cluster.getPollingSettings()\n      );\n      case TAILING -> new TailingEmitter(\n          () -> consumerGroupService.createConsumer(cluster),\n          consumerPosition, deserializer, filter, cluster.getPollingSettings()\n      );\n    };\n    return Flux.create(emitter)\n        .map(throttleUiPublish(seekDirection));\n  }\n\n  private Predicate<TopicMessageDTO> getMsgFilter(String query,\n                                                  MessageFilterTypeDTO filterQueryType) {\n    if (StringUtils.isEmpty(query)) {\n      return evt -> true;\n    }\n    return MessageFilters.createMsgFilter(query, filterQueryType);\n  }\n\n  private <T> UnaryOperator<T> throttleUiPublish(SeekDirectionDTO seekDirection) {\n    if (seekDirection == SeekDirectionDTO.TAILING) {\n      RateLimiter rateLimiter = RateLimiter.create(TAILING_UI_MESSAGE_THROTTLE_RATE);\n      return m -> {\n        rateLimiter.acquire(1);\n        return m;\n      };\n    }\n    // there is no need to throttle UI production rate for non-tailing modes, since max number of produced\n    // messages is limited for them (with page size)\n    return UnaryOperator.identity();\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/OffsetsResetService.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport static java.util.stream.Collectors.toMap;\nimport static java.util.stream.Collectors.toSet;\nimport static org.apache.kafka.common.ConsumerGroupState.DEAD;\nimport static org.apache.kafka.common.ConsumerGroupState.EMPTY;\n\nimport com.google.common.base.Preconditions;\nimport com.provectus.kafka.ui.exception.NotFoundException;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport javax.annotation.Nullable;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.kafka.clients.admin.OffsetSpec;\nimport org.apache.kafka.common.TopicPartition;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\n\n/**\n * Implementation follows https://cwiki.apache.org/confluence/display/KAFKA/KIP-122%3A+Add+Reset+Consumer+Group+Offsets+tooling\n * to works like \"kafka-consumer-groups --reset-offsets\" console command\n * (see kafka.admin.ConsumerGroupCommand)\n */\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class OffsetsResetService {\n\n  private final AdminClientService adminClientService;\n\n  public Mono<Void> resetToEarliest(\n      KafkaCluster cluster, String group, String topic, Collection<Integer> partitions) {\n    return checkGroupCondition(cluster, group)\n        .flatMap(ac ->\n            offsets(ac, topic, partitions, OffsetSpec.earliest())\n                .flatMap(offsets -> resetOffsets(ac, group, offsets)));\n  }\n\n  private Mono<Map<TopicPartition, Long>> offsets(ReactiveAdminClient client,\n                                                  String topic,\n                                                  @Nullable Collection<Integer> partitions,\n                                                  OffsetSpec spec) {\n    if (partitions == null) {\n      return client.listTopicOffsets(topic, spec, true);\n    }\n    return client.listOffsets(\n        partitions.stream().map(idx -> new TopicPartition(topic, idx)).collect(toSet()),\n        spec,\n        true\n    );\n  }\n\n  public Mono<Void> resetToLatest(\n      KafkaCluster cluster, String group, String topic, Collection<Integer> partitions) {\n    return checkGroupCondition(cluster, group)\n        .flatMap(ac ->\n            offsets(ac, topic, partitions, OffsetSpec.latest())\n                .flatMap(offsets -> resetOffsets(ac, group, offsets)));\n  }\n\n  public Mono<Void> resetToTimestamp(\n      KafkaCluster cluster, String group, String topic, Collection<Integer> partitions,\n      long targetTimestamp) {\n    return checkGroupCondition(cluster, group)\n        .flatMap(ac ->\n            offsets(ac, topic, partitions, OffsetSpec.forTimestamp(targetTimestamp))\n                .flatMap(\n                    foundOffsets -> offsets(ac, topic, partitions, OffsetSpec.latest())\n                        .map(endOffsets -> editTsOffsets(foundOffsets, endOffsets))\n                )\n                .flatMap(offsets -> resetOffsets(ac, group, offsets))\n        );\n  }\n\n  public Mono<Void> resetToOffsets(\n      KafkaCluster cluster, String group, String topic, Map<Integer, Long> targetOffsets) {\n    Preconditions.checkNotNull(targetOffsets);\n    var partitionOffsets = targetOffsets.entrySet().stream()\n        .collect(toMap(e -> new TopicPartition(topic, e.getKey()), Map.Entry::getValue));\n    return checkGroupCondition(cluster, group).flatMap(\n        ac ->\n            ac.listOffsets(partitionOffsets.keySet(), OffsetSpec.earliest(), true)\n                .flatMap(earliest ->\n                    ac.listOffsets(partitionOffsets.keySet(), OffsetSpec.latest(), true)\n                        .map(latest -> editOffsetsBounds(partitionOffsets, earliest, latest))\n                        .flatMap(offsetsToCommit -> resetOffsets(ac, group, offsetsToCommit)))\n    );\n  }\n\n  private Mono<ReactiveAdminClient> checkGroupCondition(KafkaCluster cluster, String groupId) {\n    return adminClientService.get(cluster)\n        .flatMap(ac ->\n            // we need to call listConsumerGroups() to check group existence, because\n            // describeConsumerGroups() will return consumer group even if it doesn't exist\n            ac.listConsumerGroupNames()\n                .filter(cgs -> cgs.stream().anyMatch(g -> g.equals(groupId)))\n                .flatMap(cgs -> ac.describeConsumerGroups(List.of(groupId)))\n                .filter(cgs -> cgs.containsKey(groupId))\n                .map(cgs -> cgs.get(groupId))\n                .flatMap(cg -> {\n                  if (!Set.of(DEAD, EMPTY).contains(cg.state())) {\n                    return Mono.error(\n                        new ValidationException(\n                            String.format(\n                                \"Group's offsets can be reset only if group is inactive,\"\n                                    + \" but group is in %s state\",\n                                cg.state()\n                            )\n                        )\n                    );\n                  }\n                  return Mono.just(ac);\n                })\n                .switchIfEmpty(Mono.error(new NotFoundException(\"Consumer group not found\")))\n        );\n  }\n\n  private Map<TopicPartition, Long> editTsOffsets(Map<TopicPartition, Long> foundTsOffsets,\n                                                  Map<TopicPartition, Long> endOffsets) {\n    // for partitions where we didnt find offset by timestamp, we use end offsets\n    Map<TopicPartition, Long> result = new HashMap<>(endOffsets);\n    result.putAll(foundTsOffsets);\n    return result;\n  }\n\n  /**\n   * Checks if submitted offsets is between earliest and latest offsets. If case of range change\n   * fail we reset offset to either earliest or latest offsets (To follow logic from\n   * kafka.admin.ConsumerGroupCommand.scala)\n   */\n  private Map<TopicPartition, Long> editOffsetsBounds(Map<TopicPartition, Long> offsetsToCheck,\n                                                      Map<TopicPartition, Long> earliestOffsets,\n                                                      Map<TopicPartition, Long> latestOffsets) {\n    var result = new HashMap<TopicPartition, Long>();\n    offsetsToCheck.forEach((tp, offset) -> {\n      if (earliestOffsets.get(tp) > offset) {\n        log.warn(\"Offset for partition {} is lower than earliest offset, resetting to earliest\",\n            tp);\n        result.put(tp, earliestOffsets.get(tp));\n      } else if (latestOffsets.get(tp) < offset) {\n        log.warn(\"Offset for partition {} is greater than latest offset, resetting to latest\", tp);\n        result.put(tp, latestOffsets.get(tp));\n      } else {\n        result.put(tp, offset);\n      }\n    });\n    return result;\n  }\n\n  private Mono<Void> resetOffsets(ReactiveAdminClient adminClient,\n                                  String groupId,\n                                  Map<TopicPartition, Long> offsets) {\n    return adminClient.alterConsumerGroupOffsets(groupId, offsets);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ReactiveAdminClient.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport static java.util.stream.Collectors.toList;\nimport static java.util.stream.Collectors.toMap;\nimport static org.apache.kafka.clients.admin.ListOffsetsResult.ListOffsetsResultInfo;\n\nimport com.google.common.annotations.VisibleForTesting;\nimport com.google.common.base.Preconditions;\nimport com.google.common.collect.ImmutableTable;\nimport com.google.common.collect.Iterables;\nimport com.google.common.collect.Table;\nimport com.provectus.kafka.ui.exception.IllegalEntityStateException;\nimport com.provectus.kafka.ui.exception.NotFoundException;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.util.KafkaVersion;\nimport com.provectus.kafka.ui.util.annotation.KafkaClientInternalsDependant;\nimport java.io.Closeable;\nimport java.time.Duration;\nimport java.time.temporal.ChronoUnit;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ExecutionException;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport java.util.stream.Stream;\nimport javax.annotation.Nullable;\nimport lombok.AccessLevel;\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Getter;\nimport lombok.Value;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.kafka.clients.admin.AdminClient;\nimport org.apache.kafka.clients.admin.AlterConfigOp;\nimport org.apache.kafka.clients.admin.Config;\nimport org.apache.kafka.clients.admin.ConfigEntry;\nimport org.apache.kafka.clients.admin.ConsumerGroupDescription;\nimport org.apache.kafka.clients.admin.ConsumerGroupListing;\nimport org.apache.kafka.clients.admin.DescribeClusterOptions;\nimport org.apache.kafka.clients.admin.DescribeClusterResult;\nimport org.apache.kafka.clients.admin.DescribeConfigsOptions;\nimport org.apache.kafka.clients.admin.ListConsumerGroupOffsetsSpec;\nimport org.apache.kafka.clients.admin.ListOffsetsResult;\nimport org.apache.kafka.clients.admin.ListTopicsOptions;\nimport org.apache.kafka.clients.admin.NewPartitionReassignment;\nimport org.apache.kafka.clients.admin.NewPartitions;\nimport org.apache.kafka.clients.admin.NewTopic;\nimport org.apache.kafka.clients.admin.OffsetSpec;\nimport org.apache.kafka.clients.admin.ProducerState;\nimport org.apache.kafka.clients.admin.RecordsToDelete;\nimport org.apache.kafka.clients.admin.TopicDescription;\nimport org.apache.kafka.clients.consumer.OffsetAndMetadata;\nimport org.apache.kafka.common.KafkaException;\nimport org.apache.kafka.common.KafkaFuture;\nimport org.apache.kafka.common.Node;\nimport org.apache.kafka.common.TopicPartition;\nimport org.apache.kafka.common.TopicPartitionInfo;\nimport org.apache.kafka.common.TopicPartitionReplica;\nimport org.apache.kafka.common.acl.AccessControlEntryFilter;\nimport org.apache.kafka.common.acl.AclBinding;\nimport org.apache.kafka.common.acl.AclBindingFilter;\nimport org.apache.kafka.common.acl.AclOperation;\nimport org.apache.kafka.common.config.ConfigResource;\nimport org.apache.kafka.common.errors.ClusterAuthorizationException;\nimport org.apache.kafka.common.errors.GroupIdNotFoundException;\nimport org.apache.kafka.common.errors.GroupNotEmptyException;\nimport org.apache.kafka.common.errors.InvalidRequestException;\nimport org.apache.kafka.common.errors.SecurityDisabledException;\nimport org.apache.kafka.common.errors.TopicAuthorizationException;\nimport org.apache.kafka.common.errors.UnknownTopicOrPartitionException;\nimport org.apache.kafka.common.errors.UnsupportedVersionException;\nimport org.apache.kafka.common.requests.DescribeLogDirsResponse;\nimport org.apache.kafka.common.resource.ResourcePatternFilter;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Schedulers;\nimport reactor.util.function.Tuple2;\nimport reactor.util.function.Tuples;\n\n\n@Slf4j\n@AllArgsConstructor\npublic class ReactiveAdminClient implements Closeable {\n\n  public enum SupportedFeature {\n    INCREMENTAL_ALTER_CONFIGS(2.3f),\n    CONFIG_DOCUMENTATION_RETRIEVAL(2.6f),\n    DESCRIBE_CLUSTER_INCLUDE_AUTHORIZED_OPERATIONS(2.3f),\n    AUTHORIZED_SECURITY_ENABLED(ReactiveAdminClient::isAuthorizedSecurityEnabled);\n\n    private final BiFunction<AdminClient, Float, Mono<Boolean>> predicate;\n\n    SupportedFeature(BiFunction<AdminClient, Float, Mono<Boolean>> predicate) {\n      this.predicate = predicate;\n    }\n\n    SupportedFeature(float fromVersion) {\n      this.predicate = (admin, ver) -> Mono.just(ver != null && ver >= fromVersion);\n    }\n\n    static Mono<Set<SupportedFeature>> forVersion(AdminClient ac, String kafkaVersionStr) {\n      @Nullable Float kafkaVersion = KafkaVersion.parse(kafkaVersionStr).orElse(null);\n      return Flux.fromArray(SupportedFeature.values())\n          .flatMap(f -> f.predicate.apply(ac, kafkaVersion).map(enabled -> Tuples.of(f, enabled)))\n          .filter(Tuple2::getT2)\n          .map(Tuple2::getT1)\n          .collect(Collectors.toSet());\n    }\n  }\n\n  @Value\n  public static class ClusterDescription {\n    @Nullable\n    Node controller;\n    String clusterId;\n    Collection<Node> nodes;\n    @Nullable // null, if ACL is disabled\n    Set<AclOperation> authorizedOperations;\n  }\n\n  @Builder\n  private record ConfigRelatedInfo(String version,\n                                   Set<SupportedFeature> features,\n                                   boolean topicDeletionIsAllowed) {\n\n    static final Duration UPDATE_DURATION = Duration.of(1, ChronoUnit.HOURS);\n\n    private static Mono<ConfigRelatedInfo> extract(AdminClient ac) {\n      return ReactiveAdminClient.describeClusterImpl(ac, Set.of())\n          .flatMap(desc -> {\n            // choosing node from which we will get configs (starting with controller)\n            var targetNodeId = Optional.ofNullable(desc.controller)\n                .map(Node::id)\n                .orElse(desc.getNodes().iterator().next().id());\n            return loadBrokersConfig(ac, List.of(targetNodeId))\n                .map(map -> map.isEmpty() ? List.<ConfigEntry>of() : map.get(targetNodeId))\n                .flatMap(configs -> {\n                  String version = \"1.0-UNKNOWN\";\n                  boolean topicDeletionEnabled = true;\n                  for (ConfigEntry entry : configs) {\n                    if (entry.name().contains(\"inter.broker.protocol.version\")) {\n                      version = entry.value();\n                    }\n                    if (entry.name().equals(\"delete.topic.enable\")) {\n                      topicDeletionEnabled = Boolean.parseBoolean(entry.value());\n                    }\n                  }\n                  final String finalVersion = version;\n                  final boolean finalTopicDeletionEnabled = topicDeletionEnabled;\n                  return SupportedFeature.forVersion(ac, version)\n                      .map(features -> new ConfigRelatedInfo(finalVersion, features, finalTopicDeletionEnabled));\n                });\n          })\n          .cache(UPDATE_DURATION);\n    }\n  }\n\n  public static Mono<ReactiveAdminClient> create(AdminClient adminClient) {\n    Mono<ConfigRelatedInfo> configRelatedInfoMono = ConfigRelatedInfo.extract(adminClient);\n    return configRelatedInfoMono.map(info -> new ReactiveAdminClient(adminClient, configRelatedInfoMono, info));\n  }\n\n\n  private static Mono<Boolean> isAuthorizedSecurityEnabled(AdminClient ac, @Nullable Float kafkaVersion) {\n    return toMono(ac.describeAcls(AclBindingFilter.ANY).values())\n        .thenReturn(true)\n        .doOnError(th -> !(th instanceof SecurityDisabledException)\n                && !(th instanceof InvalidRequestException)\n                && !(th instanceof UnsupportedVersionException),\n            th -> log.debug(\"Error checking if security enabled\", th))\n        .onErrorReturn(false);\n  }\n\n  // NOTE: if KafkaFuture returns null, that Mono will be empty(!), since Reactor does not support nullable results\n  // (see MonoSink.success(..) javadoc for details)\n  public static <T> Mono<T> toMono(KafkaFuture<T> future) {\n    return Mono.<T>create(sink -> future.whenComplete((res, ex) -> {\n      if (ex != null) {\n        // KafkaFuture doc is unclear about what exception wrapper will be used\n        // (from docs it should be ExecutionException, be we actually see CompletionException, so checking both\n        if (ex instanceof CompletionException || ex instanceof ExecutionException) {\n          sink.error(ex.getCause()); //unwrapping exception\n        } else {\n          sink.error(ex);\n        }\n      } else {\n        sink.success(res);\n      }\n    })).doOnCancel(() -> future.cancel(true))\n        // AdminClient is using single thread for kafka communication\n        // and by default all downstream operations (like map(..)) on created Mono will be executed on this thread.\n        // If some of downstream operation are blocking (by mistake) this can lead to\n        // other AdminClient's requests stucking, which can cause timeout exceptions.\n        // So, we explicitly setting Scheduler for downstream processing.\n        .publishOn(Schedulers.parallel());\n  }\n\n  //---------------------------------------------------------------------------------\n\n  @Getter(AccessLevel.PACKAGE) // visible for testing\n  private final AdminClient client;\n  private final Mono<ConfigRelatedInfo> configRelatedInfoMono;\n\n  private volatile ConfigRelatedInfo configRelatedInfo;\n\n  public Set<SupportedFeature> getClusterFeatures() {\n    return configRelatedInfo.features();\n  }\n\n  public Mono<Set<String>> listTopics(boolean listInternal) {\n    return toMono(client.listTopics(new ListTopicsOptions().listInternal(listInternal)).names());\n  }\n\n  public Mono<Void> deleteTopic(String topicName) {\n    return toMono(client.deleteTopics(List.of(topicName)).all());\n  }\n\n  public String getVersion() {\n    return configRelatedInfo.version();\n  }\n\n  public boolean isTopicDeletionEnabled() {\n    return configRelatedInfo.topicDeletionIsAllowed();\n  }\n\n  public Mono<Void> updateInternalStats(@Nullable Node controller) {\n    if (controller == null) {\n      return Mono.empty();\n    }\n    return configRelatedInfoMono\n        .doOnNext(info -> this.configRelatedInfo = info)\n        .then();\n  }\n\n  public Mono<Map<String, List<ConfigEntry>>> getTopicsConfig() {\n    return listTopics(true).flatMap(topics -> getTopicsConfig(topics, false));\n  }\n\n  //NOTE: skips not-found topics (for which UnknownTopicOrPartitionException was thrown by AdminClient)\n  //and topics for which DESCRIBE_CONFIGS permission is not set (TopicAuthorizationException was thrown)\n  public Mono<Map<String, List<ConfigEntry>>> getTopicsConfig(Collection<String> topicNames, boolean includeDoc) {\n    var includeDocFixed = includeDoc && getClusterFeatures().contains(SupportedFeature.CONFIG_DOCUMENTATION_RETRIEVAL);\n    // we need to partition calls, because it can lead to AdminClient timeouts in case of large topics count\n    return partitionCalls(\n        topicNames,\n        200,\n        part -> getTopicsConfigImpl(part, includeDocFixed),\n        mapMerger()\n    );\n  }\n\n  private Mono<Map<String, List<ConfigEntry>>> getTopicsConfigImpl(Collection<String> topicNames, boolean includeDoc) {\n    List<ConfigResource> resources = topicNames.stream()\n        .map(topicName -> new ConfigResource(ConfigResource.Type.TOPIC, topicName))\n        .collect(toList());\n\n    return toMonoWithExceptionFilter(\n        client.describeConfigs(\n            resources,\n            new DescribeConfigsOptions().includeSynonyms(true).includeDocumentation(includeDoc)).values(),\n        UnknownTopicOrPartitionException.class,\n        TopicAuthorizationException.class\n    ).map(config -> config.entrySet().stream()\n        .collect(toMap(\n            c -> c.getKey().name(),\n            c -> List.copyOf(c.getValue().entries()))));\n  }\n\n  private static Mono<Map<Integer, List<ConfigEntry>>> loadBrokersConfig(AdminClient client, List<Integer> brokerIds) {\n    List<ConfigResource> resources = brokerIds.stream()\n        .map(brokerId -> new ConfigResource(ConfigResource.Type.BROKER, Integer.toString(brokerId)))\n        .collect(toList());\n    return toMono(client.describeConfigs(resources).all())\n        // some kafka backends don't support broker's configs retrieval,\n        // and throw various exceptions on describeConfigs() call\n        .onErrorResume(th -> th instanceof InvalidRequestException // MSK Serverless\n                || th instanceof UnknownTopicOrPartitionException, // Azure event hub\n            th -> {\n              log.trace(\"Error while getting configs for brokers {}\", brokerIds, th);\n              return Mono.just(Map.of());\n            })\n        // there are situations when kafka-ui user has no DESCRIBE_CONFIGS permission on cluster\n        .onErrorResume(ClusterAuthorizationException.class, th -> {\n          log.trace(\"AuthorizationException while getting configs for brokers {}\", brokerIds, th);\n          return Mono.just(Map.of());\n        })\n        // catching all remaining exceptions, but logging on WARN level\n        .onErrorResume(th -> true, th -> {\n          log.warn(\"Unexpected error while getting configs for brokers {}\", brokerIds, th);\n          return Mono.just(Map.of());\n        })\n        .map(config -> config.entrySet().stream()\n            .collect(toMap(\n                c -> Integer.valueOf(c.getKey().name()),\n                c -> new ArrayList<>(c.getValue().entries()))));\n  }\n\n  /**\n   * Return per-broker configs or empty map if broker's configs retrieval not supported.\n   */\n  public Mono<Map<Integer, List<ConfigEntry>>> loadBrokersConfig(List<Integer> brokerIds) {\n    return loadBrokersConfig(client, brokerIds);\n  }\n\n  public Mono<Map<String, TopicDescription>> describeTopics() {\n    return listTopics(true).flatMap(this::describeTopics);\n  }\n\n  public Mono<Map<String, TopicDescription>> describeTopics(Collection<String> topics) {\n    // we need to partition calls, because it can lead to AdminClient timeouts in case of large topics count\n    return partitionCalls(\n        topics,\n        200,\n        this::describeTopicsImpl,\n        mapMerger()\n    );\n  }\n\n  private Mono<Map<String, TopicDescription>> describeTopicsImpl(Collection<String> topics) {\n    return toMonoWithExceptionFilter(\n        client.describeTopics(topics).topicNameValues(),\n        UnknownTopicOrPartitionException.class,\n        // we only describe topics that we see from listTopics() API, so we should have permission to do it,\n        // but also adding this exception here for rare case when access restricted after we called listTopics()\n        TopicAuthorizationException.class\n    );\n  }\n\n  /**\n   * Returns TopicDescription mono, or Empty Mono if topic not visible.\n   */\n  public Mono<TopicDescription> describeTopic(String topic) {\n    return describeTopics(List.of(topic)).flatMap(m -> Mono.justOrEmpty(m.get(topic)));\n  }\n\n  /**\n   * Kafka API often returns Map responses with KafkaFuture values. If we do allOf()\n   * logic resulting Mono will be failing if any of Futures finished with error.\n   * In some situations it is not what we want, ex. we call describeTopics(List names) method and\n   * we getting UnknownTopicOrPartitionException for unknown topics and we what to just not put\n   * such topics in resulting map.\n   * <p/>\n   * This method converts input map into Mono[Map] ignoring keys for which KafkaFutures\n   * finished with <code>classes</code> exceptions and empty Monos.\n   */\n  @SafeVarargs\n  static <K, V> Mono<Map<K, V>> toMonoWithExceptionFilter(Map<K, KafkaFuture<V>> values,\n                                                          Class<? extends KafkaException>... classes) {\n    if (values.isEmpty()) {\n      return Mono.just(Map.of());\n    }\n\n    List<Mono<Tuple2<K, Optional<V>>>> monos = values.entrySet().stream()\n        .map(e ->\n            toMono(e.getValue())\n                .map(r -> Tuples.of(e.getKey(), Optional.of(r)))\n                .defaultIfEmpty(Tuples.of(e.getKey(), Optional.empty())) //tracking empty Monos\n                .onErrorResume(\n                    // tracking Monos with suppressible error\n                    th -> Stream.of(classes).anyMatch(clazz -> th.getClass().isAssignableFrom(clazz)),\n                    th -> Mono.just(Tuples.of(e.getKey(), Optional.empty()))))\n        .toList();\n\n    return Mono.zip(\n        monos,\n        resultsArr -> Stream.of(resultsArr)\n            .map(obj -> (Tuple2<K, Optional<V>>) obj)\n            .filter(t -> t.getT2().isPresent()) //skipping empty & suppressible-errors\n            .collect(Collectors.toMap(Tuple2::getT1, t -> t.getT2().get()))\n    );\n  }\n\n  public Mono<Map<Integer, Map<String, DescribeLogDirsResponse.LogDirInfo>>> describeLogDirs() {\n    return describeCluster()\n        .map(d -> d.getNodes().stream().map(Node::id).collect(toList()))\n        .flatMap(this::describeLogDirs);\n  }\n\n  public Mono<Map<Integer, Map<String, DescribeLogDirsResponse.LogDirInfo>>> describeLogDirs(\n      Collection<Integer> brokerIds) {\n    return toMono(client.describeLogDirs(brokerIds).all())\n        .onErrorResume(UnsupportedVersionException.class, th -> Mono.just(Map.of()))\n        .onErrorResume(ClusterAuthorizationException.class, th -> Mono.just(Map.of()))\n        .onErrorResume(th -> true, th -> {\n          log.warn(\"Error while calling describeLogDirs\", th);\n          return Mono.just(Map.of());\n        });\n  }\n\n  public Mono<ClusterDescription> describeCluster() {\n    return describeClusterImpl(client, getClusterFeatures());\n  }\n\n  private static Mono<ClusterDescription> describeClusterImpl(AdminClient client, Set<SupportedFeature> features) {\n    boolean includeAuthorizedOperations =\n        features.contains(SupportedFeature.DESCRIBE_CLUSTER_INCLUDE_AUTHORIZED_OPERATIONS);\n    DescribeClusterResult result = client.describeCluster(\n        new DescribeClusterOptions().includeAuthorizedOperations(includeAuthorizedOperations));\n    var allOfFuture = KafkaFuture.allOf(\n        result.controller(), result.clusterId(), result.nodes(), result.authorizedOperations());\n    return toMono(allOfFuture).then(\n        Mono.fromCallable(() ->\n          new ClusterDescription(\n            result.controller().get(),\n            result.clusterId().get(),\n            result.nodes().get(),\n            result.authorizedOperations().get()\n          )\n        )\n    );\n  }\n\n  public Mono<Void> deleteConsumerGroups(Collection<String> groupIds) {\n    return toMono(client.deleteConsumerGroups(groupIds).all())\n        .onErrorResume(GroupIdNotFoundException.class,\n            th -> Mono.error(new NotFoundException(\"The group id does not exist\")))\n        .onErrorResume(GroupNotEmptyException.class,\n            th -> Mono.error(new IllegalEntityStateException(\"The group is not empty\")));\n  }\n\n  public Mono<Void> createTopic(String name,\n                                int numPartitions,\n                                @Nullable Integer replicationFactor,\n                                Map<String, String> configs) {\n    var newTopic = new NewTopic(\n        name,\n        Optional.of(numPartitions),\n        Optional.ofNullable(replicationFactor).map(Integer::shortValue)\n    ).configs(configs);\n    return toMono(client.createTopics(List.of(newTopic)).all());\n  }\n\n  public Mono<Void> alterPartitionReassignments(\n      Map<TopicPartition, Optional<NewPartitionReassignment>> reassignments) {\n    return toMono(client.alterPartitionReassignments(reassignments).all());\n  }\n\n  public Mono<Void> createPartitions(Map<String, NewPartitions> newPartitionsMap) {\n    return toMono(client.createPartitions(newPartitionsMap).all());\n  }\n\n\n  // NOTE: places whole current topic config with new one. Entries that were present in old config,\n  // but missed in new will be set to default\n  public Mono<Void> updateTopicConfig(String topicName, Map<String, String> configs) {\n    if (getClusterFeatures().contains(SupportedFeature.INCREMENTAL_ALTER_CONFIGS)) {\n      return getTopicsConfigImpl(List.of(topicName), false)\n          .map(conf -> conf.getOrDefault(topicName, List.of()))\n          .flatMap(currentConfigs -> incrementalAlterConfig(topicName, currentConfigs, configs));\n    } else {\n      return alterConfig(topicName, configs);\n    }\n  }\n\n  public Mono<List<String>> listConsumerGroupNames() {\n    return listConsumerGroups().map(lst -> lst.stream().map(ConsumerGroupListing::groupId).toList());\n  }\n\n  public Mono<Collection<ConsumerGroupListing>> listConsumerGroups() {\n    return toMono(client.listConsumerGroups().all());\n  }\n\n  public Mono<Map<String, ConsumerGroupDescription>> describeConsumerGroups(Collection<String> groupIds) {\n    return partitionCalls(\n        groupIds,\n        25,\n        4,\n        ids -> toMono(client.describeConsumerGroups(ids).all()),\n        mapMerger()\n    );\n  }\n\n  // group -> partition -> offset\n  // NOTE: partitions with no committed offsets will be skipped\n  public Mono<Table<String, TopicPartition, Long>> listConsumerGroupOffsets(List<String> consumerGroups,\n                                                                            // all partitions if null passed\n                                                                            @Nullable List<TopicPartition> partitions) {\n    Function<Collection<String>, Mono<Map<String, Map<TopicPartition, OffsetAndMetadata>>>> call =\n        groups -> toMono(\n            client.listConsumerGroupOffsets(\n                groups.stream()\n                    .collect(Collectors.toMap(\n                        g -> g,\n                        g -> new ListConsumerGroupOffsetsSpec().topicPartitions(partitions)\n                    ))).all()\n        );\n\n    Mono<Map<String, Map<TopicPartition, OffsetAndMetadata>>> merged = partitionCalls(\n        consumerGroups,\n        25,\n        4,\n        call,\n        mapMerger()\n    );\n\n    return merged.map(map -> {\n      var table = ImmutableTable.<String, TopicPartition, Long>builder();\n      map.forEach((g, tpOffsets) -> tpOffsets.forEach((tp, offset) -> {\n        if (offset != null) {\n          // offset will be null for partitions that don't have committed offset for this group\n          table.put(g, tp, offset.offset());\n        }\n      }));\n      return table.build();\n    });\n  }\n\n  public Mono<Void> alterConsumerGroupOffsets(String groupId, Map<TopicPartition, Long> offsets) {\n    return toMono(client.alterConsumerGroupOffsets(\n            groupId,\n            offsets.entrySet().stream()\n                .collect(toMap(Map.Entry::getKey, e -> new OffsetAndMetadata(e.getValue()))))\n        .all());\n  }\n\n  /**\n   * List offset for the topic's partitions and OffsetSpec.\n   *\n   * @param failOnUnknownLeader true - throw exception in case of no-leader partitions,\n   *                            false - skip partitions with no leader\n   */\n  public Mono<Map<TopicPartition, Long>> listTopicOffsets(String topic,\n                                                          OffsetSpec offsetSpec,\n                                                          boolean failOnUnknownLeader) {\n    return describeTopic(topic)\n        .map(td -> filterPartitionsWithLeaderCheck(List.of(td), p -> true, failOnUnknownLeader))\n        .flatMap(partitions -> listOffsetsUnsafe(partitions, offsetSpec));\n  }\n\n  /**\n   * List offset for the specified partitions and OffsetSpec.\n   *\n   * @param failOnUnknownLeader true - throw exception in case of no-leader partitions,\n   *                            false - skip partitions with no leader\n   */\n  public Mono<Map<TopicPartition, Long>> listOffsets(Collection<TopicPartition> partitions,\n                                                     OffsetSpec offsetSpec,\n                                                     boolean failOnUnknownLeader) {\n    return filterPartitionsWithLeaderCheck(partitions, failOnUnknownLeader)\n        .flatMap(parts -> listOffsetsUnsafe(parts, offsetSpec));\n  }\n\n  /**\n   * List offset for the specified topics, skipping no-leader partitions.\n   */\n  public Mono<Map<TopicPartition, Long>> listOffsets(Collection<TopicDescription> topicDescriptions,\n                                                     OffsetSpec offsetSpec) {\n    return listOffsetsUnsafe(filterPartitionsWithLeaderCheck(topicDescriptions, p -> true, false), offsetSpec);\n  }\n\n  private Mono<Collection<TopicPartition>> filterPartitionsWithLeaderCheck(Collection<TopicPartition> partitions,\n                                                                           boolean failOnUnknownLeader) {\n    var targetTopics = partitions.stream().map(TopicPartition::topic).collect(Collectors.toSet());\n    return describeTopicsImpl(targetTopics)\n        .map(descriptions ->\n            filterPartitionsWithLeaderCheck(\n                descriptions.values(), partitions::contains, failOnUnknownLeader));\n  }\n\n  @VisibleForTesting\n  static Set<TopicPartition> filterPartitionsWithLeaderCheck(Collection<TopicDescription> topicDescriptions,\n                                                              Predicate<TopicPartition> partitionPredicate,\n                                                              boolean failOnUnknownLeader) {\n    var goodPartitions = new HashSet<TopicPartition>();\n    for (TopicDescription description : topicDescriptions) {\n      var goodTopicPartitions = new ArrayList<TopicPartition>();\n      for (TopicPartitionInfo partitionInfo : description.partitions()) {\n        TopicPartition topicPartition = new TopicPartition(description.name(), partitionInfo.partition());\n        if (partitionInfo.leader() == null) {\n          if (failOnUnknownLeader) {\n            throw new ValidationException(String.format(\"Topic partition %s has no leader\", topicPartition));\n          } else {\n            // if ANY of topic partitions has no leader - we have to skip all topic partitions\n            goodTopicPartitions.clear();\n            break;\n          }\n        }\n        if (partitionPredicate.test(topicPartition)) {\n          goodTopicPartitions.add(topicPartition);\n        }\n      }\n      goodPartitions.addAll(goodTopicPartitions);\n    }\n    return goodPartitions;\n  }\n\n  // 1. NOTE(!): should only apply for partitions from topics where all partitions have leaders,\n  //    otherwise AdminClient will try to fetch topic metadata, fail and retry infinitely (until timeout)\n  // 2. NOTE(!): Skips partitions that were not initialized yet\n  //    (UnknownTopicOrPartitionException thrown, ex. after topic creation)\n  // 3. TODO: check if it is a bug that AdminClient never throws LeaderNotAvailableException and just retrying instead\n  @KafkaClientInternalsDependant\n  @VisibleForTesting\n  Mono<Map<TopicPartition, Long>> listOffsetsUnsafe(Collection<TopicPartition> partitions, OffsetSpec offsetSpec) {\n    if (partitions.isEmpty()) {\n      return Mono.just(Map.of());\n    }\n\n    Function<Collection<TopicPartition>, Mono<Map<TopicPartition, Long>>> call =\n        parts -> {\n          ListOffsetsResult r = client.listOffsets(parts.stream().collect(toMap(tp -> tp, tp -> offsetSpec)));\n          Map<TopicPartition, KafkaFuture<ListOffsetsResultInfo>> perPartitionResults = new HashMap<>();\n          parts.forEach(p -> perPartitionResults.put(p, r.partitionResult(p)));\n\n          return toMonoWithExceptionFilter(perPartitionResults, UnknownTopicOrPartitionException.class)\n              .map(offsets -> offsets.entrySet().stream()\n                  // filtering partitions for which offsets were not found\n                  .filter(e -> e.getValue().offset() >= 0)\n                  .collect(toMap(Map.Entry::getKey, e -> e.getValue().offset())));\n        };\n\n    return partitionCalls(\n        partitions,\n        200,\n        call,\n        mapMerger()\n    );\n  }\n\n  public Mono<Collection<AclBinding>> listAcls(ResourcePatternFilter filter) {\n    Preconditions.checkArgument(getClusterFeatures().contains(SupportedFeature.AUTHORIZED_SECURITY_ENABLED));\n    return toMono(client.describeAcls(new AclBindingFilter(filter, AccessControlEntryFilter.ANY)).values());\n  }\n\n  public Mono<Void> createAcls(Collection<AclBinding> aclBindings) {\n    Preconditions.checkArgument(getClusterFeatures().contains(SupportedFeature.AUTHORIZED_SECURITY_ENABLED));\n    return toMono(client.createAcls(aclBindings).all());\n  }\n\n  public Mono<Void> deleteAcls(Collection<AclBinding> aclBindings) {\n    Preconditions.checkArgument(getClusterFeatures().contains(SupportedFeature.AUTHORIZED_SECURITY_ENABLED));\n    var filters = aclBindings.stream().map(AclBinding::toFilter).collect(Collectors.toSet());\n    return toMono(client.deleteAcls(filters).all()).then();\n  }\n\n  public Mono<Void> updateBrokerConfigByName(Integer brokerId, String name, String value) {\n    ConfigResource cr = new ConfigResource(ConfigResource.Type.BROKER, String.valueOf(brokerId));\n    AlterConfigOp op = new AlterConfigOp(new ConfigEntry(name, value), AlterConfigOp.OpType.SET);\n    return toMono(client.incrementalAlterConfigs(Map.of(cr, List.of(op))).all());\n  }\n\n  public Mono<Void> deleteRecords(Map<TopicPartition, Long> offsets) {\n    var records = offsets.entrySet().stream()\n        .map(entry -> Map.entry(entry.getKey(), RecordsToDelete.beforeOffset(entry.getValue())))\n        .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));\n    return toMono(client.deleteRecords(records).all());\n  }\n\n  public Mono<Void> alterReplicaLogDirs(Map<TopicPartitionReplica, String> replicaAssignment) {\n    return toMono(client.alterReplicaLogDirs(replicaAssignment).all());\n  }\n\n  // returns tp -> list of active producer's states (if any)\n  public Mono<Map<TopicPartition, List<ProducerState>>> getActiveProducersState(String topic) {\n    return describeTopic(topic)\n        .map(td -> client.describeProducers(\n                IntStream.range(0, td.partitions().size())\n                    .mapToObj(i -> new TopicPartition(topic, i))\n                    .toList()\n            ).all()\n        )\n        .flatMap(ReactiveAdminClient::toMono)\n        .map(map -> map.entrySet().stream()\n            .filter(e -> !e.getValue().activeProducers().isEmpty()) // skipping partitions without producers\n            .collect(toMap(Map.Entry::getKey, e -> e.getValue().activeProducers())));\n  }\n\n  private Mono<Void> incrementalAlterConfig(String topicName,\n                                            List<ConfigEntry> currentConfigs,\n                                            Map<String, String> newConfigs) {\n    var configsToDelete = currentConfigs.stream()\n        .filter(e -> e.source() == ConfigEntry.ConfigSource.DYNAMIC_TOPIC_CONFIG) //manually set configs only\n        .filter(e -> !newConfigs.containsKey(e.name()))\n        .map(e -> new AlterConfigOp(e, AlterConfigOp.OpType.DELETE));\n\n    var configsToSet = newConfigs.entrySet().stream()\n        .map(e -> new AlterConfigOp(new ConfigEntry(e.getKey(), e.getValue()), AlterConfigOp.OpType.SET));\n\n    return toMono(client.incrementalAlterConfigs(\n        Map.of(\n            new ConfigResource(ConfigResource.Type.TOPIC, topicName),\n            Stream.concat(configsToDelete, configsToSet).toList()\n        )).all());\n  }\n\n  @SuppressWarnings(\"deprecation\")\n  private Mono<Void> alterConfig(String topicName, Map<String, String> configs) {\n    List<ConfigEntry> configEntries = configs.entrySet().stream()\n        .flatMap(cfg -> Stream.of(new ConfigEntry(cfg.getKey(), cfg.getValue())))\n        .collect(toList());\n    Config config = new Config(configEntries);\n    var topicResource = new ConfigResource(ConfigResource.Type.TOPIC, topicName);\n    return toMono(client.alterConfigs(Map.of(topicResource, config)).all());\n  }\n\n  /**\n   * Splits input collection into batches, converts each batch into Mono, sequentially subscribes to them\n   * and merges output Monos into one Mono.\n   */\n  private static <R, I> Mono<R> partitionCalls(Collection<I> items,\n                                               int partitionSize,\n                                               Function<Collection<I>, Mono<R>> call,\n                                               BiFunction<R, R, R> merger) {\n    if (items.isEmpty()) {\n      return call.apply(items);\n    }\n    Iterable<List<I>> parts = Iterables.partition(items, partitionSize);\n    return Flux.fromIterable(parts)\n        .concatMap(call)\n        .reduce(merger);\n  }\n\n  /**\n   * Splits input collection into batches, converts each batch into Mono, subscribes to them (concurrently,\n   * with specified concurrency level) and merges output Monos into one Mono.\n   */\n  private static <R, I> Mono<R> partitionCalls(Collection<I> items,\n                                               int partitionSize,\n                                               int concurrency,\n                                               Function<Collection<I>, Mono<R>> call,\n                                               BiFunction<R, R, R> merger) {\n    if (items.isEmpty()) {\n      return call.apply(items);\n    }\n    Iterable<List<I>> parts = Iterables.partition(items, partitionSize);\n    return Flux.fromIterable(parts)\n        .flatMap(call, concurrency)\n        .reduce(merger);\n  }\n\n  private static <K, V> BiFunction<Map<K, V>, Map<K, V>, Map<K, V>> mapMerger() {\n    return (m1, m2) -> {\n      var merged = new HashMap<K, V>();\n      merged.putAll(m1);\n      merged.putAll(m2);\n      return merged;\n    };\n  }\n\n  @Override\n  public void close() {\n    client.close();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/SchemaRegistryService.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport com.provectus.kafka.ui.exception.SchemaCompatibilityException;\nimport com.provectus.kafka.ui.exception.SchemaNotFoundException;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.sr.api.KafkaSrClientApi;\nimport com.provectus.kafka.ui.sr.model.Compatibility;\nimport com.provectus.kafka.ui.sr.model.CompatibilityCheckResponse;\nimport com.provectus.kafka.ui.sr.model.CompatibilityConfig;\nimport com.provectus.kafka.ui.sr.model.CompatibilityLevelChange;\nimport com.provectus.kafka.ui.sr.model.NewSubject;\nimport com.provectus.kafka.ui.sr.model.SchemaSubject;\nimport com.provectus.kafka.ui.util.ReactiveFailover;\nimport java.nio.charset.Charset;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\nimport lombok.RequiredArgsConstructor;\nimport lombok.SneakyThrows;\nimport lombok.experimental.Delegate;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Service;\nimport org.springframework.web.reactive.function.client.WebClientResponseException;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\n@Service\n@Slf4j\n@RequiredArgsConstructor\npublic class SchemaRegistryService {\n\n  private static final String LATEST = \"latest\";\n\n  @AllArgsConstructor\n  public static class SubjectWithCompatibilityLevel {\n    @Delegate\n    SchemaSubject subject;\n    @Getter\n    Compatibility compatibility;\n  }\n\n  private ReactiveFailover<KafkaSrClientApi> api(KafkaCluster cluster) {\n    return cluster.getSchemaRegistryClient();\n  }\n\n  public Mono<List<SubjectWithCompatibilityLevel>> getAllLatestVersionSchemas(KafkaCluster cluster,\n                                                                              List<String> subjects) {\n    return Flux.fromIterable(subjects)\n        .concatMap(subject -> getLatestSchemaVersionBySubject(cluster, subject))\n        .collect(Collectors.toList());\n  }\n\n  public Mono<List<String>> getAllSubjectNames(KafkaCluster cluster) {\n    return api(cluster)\n        .mono(c -> c.getAllSubjectNames(null, false))\n        .flatMapIterable(this::parseSubjectListString)\n        .collectList();\n  }\n\n  @SneakyThrows\n  private List<String> parseSubjectListString(String subjectNamesStr) {\n    //workaround for https://github.com/spring-projects/spring-framework/issues/24734\n    return new JsonMapper().readValue(subjectNamesStr, new TypeReference<List<String>>() {\n    });\n  }\n\n  public Flux<SubjectWithCompatibilityLevel> getAllVersionsBySubject(KafkaCluster cluster, String subject) {\n    Flux<Integer> versions = getSubjectVersions(cluster, subject);\n    return versions.flatMap(version -> getSchemaSubjectByVersion(cluster, subject, version));\n  }\n\n  private Flux<Integer> getSubjectVersions(KafkaCluster cluster, String schemaName) {\n    return api(cluster).flux(c -> c.getSubjectVersions(schemaName));\n  }\n\n  public Mono<SubjectWithCompatibilityLevel> getSchemaSubjectByVersion(KafkaCluster cluster,\n                                                                       String schemaName,\n                                                                       Integer version) {\n    return getSchemaSubject(cluster, schemaName, String.valueOf(version));\n  }\n\n  public Mono<SubjectWithCompatibilityLevel> getLatestSchemaVersionBySubject(KafkaCluster cluster,\n                                                                             String schemaName) {\n    return getSchemaSubject(cluster, schemaName, LATEST);\n  }\n\n  private Mono<SubjectWithCompatibilityLevel> getSchemaSubject(KafkaCluster cluster, String schemaName,\n                                                               String version) {\n    return api(cluster)\n        .mono(c -> c.getSubjectVersion(schemaName, version, false))\n        .zipWith(getSchemaCompatibilityInfoOrGlobal(cluster, schemaName))\n        .map(t -> new SubjectWithCompatibilityLevel(t.getT1(), t.getT2()))\n        .onErrorResume(WebClientResponseException.NotFound.class, th -> Mono.error(new SchemaNotFoundException()));\n  }\n\n  public Mono<Void> deleteSchemaSubjectByVersion(KafkaCluster cluster, String schemaName, Integer version) {\n    return deleteSchemaSubject(cluster, schemaName, String.valueOf(version));\n  }\n\n  public Mono<Void> deleteLatestSchemaSubject(KafkaCluster cluster, String schemaName) {\n    return deleteSchemaSubject(cluster, schemaName, LATEST);\n  }\n\n  private Mono<Void> deleteSchemaSubject(KafkaCluster cluster, String schemaName, String version) {\n    return api(cluster).mono(c -> c.deleteSubjectVersion(schemaName, version, false));\n  }\n\n  public Mono<Void> deleteSchemaSubjectEntirely(KafkaCluster cluster, String schemaName) {\n    return api(cluster).mono(c -> c.deleteAllSubjectVersions(schemaName, false));\n  }\n\n  /**\n   * Checks whether the provided schema duplicates the previous or not, creates a new schema\n   * and then returns the whole content by requesting its latest version.\n   */\n  public Mono<SubjectWithCompatibilityLevel> registerNewSchema(KafkaCluster cluster,\n                                                               String subject,\n                                                               NewSubject newSchemaSubject) {\n    return api(cluster)\n        .mono(c -> c.registerNewSchema(subject, newSchemaSubject))\n        .onErrorMap(WebClientResponseException.Conflict.class,\n            th -> new SchemaCompatibilityException())\n        .onErrorMap(WebClientResponseException.UnprocessableEntity.class,\n            th -> new ValidationException(\"Invalid schema. Error from registry: \" + th.getResponseBodyAsString()))\n        .then(getLatestSchemaVersionBySubject(cluster, subject));\n  }\n\n  public Mono<Void> updateSchemaCompatibility(KafkaCluster cluster,\n                                              String schemaName,\n                                              Compatibility compatibility) {\n    return api(cluster)\n        .mono(c -> c.updateSubjectCompatibilityLevel(\n            schemaName, new CompatibilityLevelChange().compatibility(compatibility)))\n        .then();\n  }\n\n  public Mono<Void> updateGlobalSchemaCompatibility(KafkaCluster cluster,\n                                                    Compatibility compatibility) {\n    return api(cluster)\n        .mono(c -> c.updateGlobalCompatibilityLevel(new CompatibilityLevelChange().compatibility(compatibility)))\n        .then();\n  }\n\n  public Mono<Compatibility> getSchemaCompatibilityLevel(KafkaCluster cluster,\n                                                         String schemaName) {\n    return api(cluster)\n        .mono(c -> c.getSubjectCompatibilityLevel(schemaName, true))\n        .map(CompatibilityConfig::getCompatibilityLevel)\n        .onErrorResume(error -> Mono.empty());\n  }\n\n  public Mono<Compatibility> getGlobalSchemaCompatibilityLevel(KafkaCluster cluster) {\n    return api(cluster)\n        .mono(KafkaSrClientApi::getGlobalCompatibilityLevel)\n        .map(CompatibilityConfig::getCompatibilityLevel);\n  }\n\n  private Mono<Compatibility> getSchemaCompatibilityInfoOrGlobal(KafkaCluster cluster,\n                                                                 String schemaName) {\n    return getSchemaCompatibilityLevel(cluster, schemaName)\n        .switchIfEmpty(this.getGlobalSchemaCompatibilityLevel(cluster));\n  }\n\n  public Mono<CompatibilityCheckResponse> checksSchemaCompatibility(KafkaCluster cluster,\n                                                                    String schemaName,\n                                                                    NewSubject newSchemaSubject) {\n    return api(cluster).mono(c -> c.checkSchemaCompatibility(schemaName, LATEST, true, newSchemaSubject));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/StatisticsCache.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.ServerStatusDTO;\nimport com.provectus.kafka.ui.model.Statistics;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.concurrent.ConcurrentHashMap;\nimport org.apache.kafka.clients.admin.ConfigEntry;\nimport org.apache.kafka.clients.admin.TopicDescription;\nimport org.springframework.stereotype.Component;\n\n@Component\npublic class StatisticsCache {\n\n  private final Map<String, Statistics> cache = new ConcurrentHashMap<>();\n\n  public StatisticsCache(ClustersStorage clustersStorage) {\n    var initializing = Statistics.empty().toBuilder().status(ServerStatusDTO.INITIALIZING).build();\n    clustersStorage.getKafkaClusters().forEach(c -> cache.put(c.getName(), initializing));\n  }\n\n  public synchronized void replace(KafkaCluster c, Statistics stats) {\n    cache.put(c.getName(), stats);\n  }\n\n  public synchronized void update(KafkaCluster c,\n                                  Map<String, TopicDescription> descriptions,\n                                  Map<String, List<ConfigEntry>> configs) {\n    var metrics = get(c);\n    var updatedDescriptions = new HashMap<>(metrics.getTopicDescriptions());\n    updatedDescriptions.putAll(descriptions);\n    var updatedConfigs = new HashMap<>(metrics.getTopicConfigs());\n    updatedConfigs.putAll(configs);\n    replace(\n        c,\n        metrics.toBuilder()\n            .topicDescriptions(updatedDescriptions)\n            .topicConfigs(updatedConfigs)\n            .build()\n    );\n  }\n\n  public synchronized void onTopicDelete(KafkaCluster c, String topic) {\n    var metrics = get(c);\n    var updatedDescriptions = new HashMap<>(metrics.getTopicDescriptions());\n    updatedDescriptions.remove(topic);\n    var updatedConfigs = new HashMap<>(metrics.getTopicConfigs());\n    updatedConfigs.remove(topic);\n    replace(\n        c,\n        metrics.toBuilder()\n            .topicDescriptions(updatedDescriptions)\n            .topicConfigs(updatedConfigs)\n            .build()\n    );\n  }\n\n  public Statistics get(KafkaCluster c) {\n    return Objects.requireNonNull(cache.get(c.getName()), \"Unknown cluster metrics requested\");\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/StatisticsService.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport static com.provectus.kafka.ui.service.ReactiveAdminClient.ClusterDescription;\n\nimport com.provectus.kafka.ui.model.ClusterFeature;\nimport com.provectus.kafka.ui.model.InternalLogDirStats;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.Metrics;\nimport com.provectus.kafka.ui.model.ServerStatusDTO;\nimport com.provectus.kafka.ui.model.Statistics;\nimport com.provectus.kafka.ui.service.metrics.MetricsCollector;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.kafka.clients.admin.ConfigEntry;\nimport org.apache.kafka.clients.admin.TopicDescription;\nimport org.apache.kafka.common.Node;\nimport org.springframework.stereotype.Service;\nimport reactor.core.publisher.Mono;\n\n@Service\n@RequiredArgsConstructor\n@Slf4j\npublic class StatisticsService {\n\n  private final MetricsCollector metricsCollector;\n  private final AdminClientService adminClientService;\n  private final FeatureService featureService;\n  private final StatisticsCache cache;\n\n  public Mono<Statistics> updateCache(KafkaCluster c) {\n    return getStatistics(c).doOnSuccess(m -> cache.replace(c, m));\n  }\n\n  private Mono<Statistics> getStatistics(KafkaCluster cluster) {\n    return adminClientService.get(cluster).flatMap(ac ->\n            ac.describeCluster().flatMap(description ->\n                ac.updateInternalStats(description.getController()).then(\n                    Mono.zip(\n                        List.of(\n                            metricsCollector.getBrokerMetrics(cluster, description.getNodes()),\n                            getLogDirInfo(description, ac),\n                            featureService.getAvailableFeatures(ac, cluster, description),\n                            loadTopicConfigs(cluster),\n                            describeTopics(cluster)),\n                        results ->\n                            Statistics.builder()\n                                .status(ServerStatusDTO.ONLINE)\n                                .clusterDescription(description)\n                                .version(ac.getVersion())\n                                .metrics((Metrics) results[0])\n                                .logDirInfo((InternalLogDirStats) results[1])\n                                .features((List<ClusterFeature>) results[2])\n                                .topicConfigs((Map<String, List<ConfigEntry>>) results[3])\n                                .topicDescriptions((Map<String, TopicDescription>) results[4])\n                                .build()\n                    ))))\n        .doOnError(e ->\n            log.error(\"Failed to collect cluster {} info\", cluster.getName(), e))\n        .onErrorResume(\n            e -> Mono.just(Statistics.empty().toBuilder().lastKafkaException(e).build()));\n  }\n\n  private Mono<InternalLogDirStats> getLogDirInfo(ClusterDescription desc, ReactiveAdminClient ac) {\n    var brokerIds = desc.getNodes().stream().map(Node::id).collect(Collectors.toSet());\n    return ac.describeLogDirs(brokerIds).map(InternalLogDirStats::new);\n  }\n\n  private Mono<Map<String, TopicDescription>> describeTopics(KafkaCluster c) {\n    return adminClientService.get(c).flatMap(ReactiveAdminClient::describeTopics);\n  }\n\n  private Mono<Map<String, List<ConfigEntry>>> loadTopicConfigs(KafkaCluster c) {\n    return adminClientService.get(c).flatMap(ReactiveAdminClient::getTopicsConfig);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/TopicsService.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport static java.util.stream.Collectors.toList;\nimport static java.util.stream.Collectors.toMap;\n\nimport com.google.common.collect.Sets;\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.exception.TopicMetadataException;\nimport com.provectus.kafka.ui.exception.TopicNotFoundException;\nimport com.provectus.kafka.ui.exception.TopicRecreationException;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.model.ClusterFeature;\nimport com.provectus.kafka.ui.model.InternalLogDirStats;\nimport com.provectus.kafka.ui.model.InternalPartition;\nimport com.provectus.kafka.ui.model.InternalPartitionsOffsets;\nimport com.provectus.kafka.ui.model.InternalReplica;\nimport com.provectus.kafka.ui.model.InternalTopic;\nimport com.provectus.kafka.ui.model.InternalTopicConfig;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.Metrics;\nimport com.provectus.kafka.ui.model.PartitionsIncreaseDTO;\nimport com.provectus.kafka.ui.model.PartitionsIncreaseResponseDTO;\nimport com.provectus.kafka.ui.model.ReplicationFactorChangeDTO;\nimport com.provectus.kafka.ui.model.ReplicationFactorChangeResponseDTO;\nimport com.provectus.kafka.ui.model.Statistics;\nimport com.provectus.kafka.ui.model.TopicCreationDTO;\nimport com.provectus.kafka.ui.model.TopicUpdateDTO;\nimport java.time.Duration;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.kafka.clients.admin.ConfigEntry;\nimport org.apache.kafka.clients.admin.NewPartitionReassignment;\nimport org.apache.kafka.clients.admin.NewPartitions;\nimport org.apache.kafka.clients.admin.OffsetSpec;\nimport org.apache.kafka.clients.admin.ProducerState;\nimport org.apache.kafka.clients.admin.TopicDescription;\nimport org.apache.kafka.common.Node;\nimport org.apache.kafka.common.TopicPartition;\nimport org.apache.kafka.common.errors.TopicExistsException;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\n\n@Service\n@RequiredArgsConstructor\npublic class TopicsService {\n\n  private final AdminClientService adminClientService;\n  private final StatisticsCache statisticsCache;\n  private final ClustersProperties clustersProperties;\n  @Value(\"${topic.recreate.maxRetries:15}\")\n  private int recreateMaxRetries;\n  @Value(\"${topic.recreate.delay.seconds:1}\")\n  private int recreateDelayInSeconds;\n  @Value(\"${topic.load.after.create.maxRetries:10}\")\n  private int loadTopicAfterCreateRetries;\n  @Value(\"${topic.load.after.create.delay.ms:500}\")\n  private int loadTopicAfterCreateDelayInMs;\n\n  public Mono<List<InternalTopic>> loadTopics(KafkaCluster c, List<String> topics) {\n    if (topics.isEmpty()) {\n      return Mono.just(List.of());\n    }\n    return adminClientService.get(c)\n        .flatMap(ac ->\n            ac.describeTopics(topics).zipWith(ac.getTopicsConfig(topics, false),\n                (descriptions, configs) -> {\n                  statisticsCache.update(c, descriptions, configs);\n                  return getPartitionOffsets(descriptions, ac).map(offsets -> {\n                    var metrics = statisticsCache.get(c);\n                    return createList(\n                        topics,\n                        descriptions,\n                        configs,\n                        offsets,\n                        metrics.getMetrics(),\n                        metrics.getLogDirInfo()\n                    );\n                  });\n                })).flatMap(Function.identity());\n  }\n\n  private Mono<InternalTopic> loadTopic(KafkaCluster c, String topicName) {\n    return loadTopics(c, List.of(topicName))\n        .flatMap(lst -> lst.stream().findFirst()\n            .map(Mono::just)\n            .orElse(Mono.error(TopicNotFoundException::new)));\n  }\n\n  /**\n   *  After creation topic can be invisible via API for some time.\n   *  To workaround this, we retyring topic loading until it becomes visible.\n   */\n  private Mono<InternalTopic> loadTopicAfterCreation(KafkaCluster c, String topicName) {\n    return loadTopic(c, topicName)\n        .retryWhen(\n            Retry\n                .fixedDelay(\n                    loadTopicAfterCreateRetries,\n                    Duration.ofMillis(loadTopicAfterCreateDelayInMs)\n                )\n                .filter(TopicNotFoundException.class::isInstance)\n                .onRetryExhaustedThrow((spec, sig) ->\n                    new TopicMetadataException(\n                        String.format(\n                            \"Error while loading created topic '%s' - topic is not visible via API \"\n                                + \"after waiting for %d ms.\",\n                            topicName,\n                            loadTopicAfterCreateDelayInMs * loadTopicAfterCreateRetries)))\n        );\n  }\n\n  private List<InternalTopic> createList(List<String> orderedNames,\n                                         Map<String, TopicDescription> descriptions,\n                                         Map<String, List<ConfigEntry>> configs,\n                                         InternalPartitionsOffsets partitionsOffsets,\n                                         Metrics metrics,\n                                         InternalLogDirStats logDirInfo) {\n    return orderedNames.stream()\n        .filter(descriptions::containsKey)\n        .map(t -> InternalTopic.from(\n            descriptions.get(t),\n            configs.getOrDefault(t, List.of()),\n            partitionsOffsets,\n            metrics,\n            logDirInfo,\n            clustersProperties.getInternalTopicPrefix()\n        ))\n        .collect(toList());\n  }\n\n  private Mono<InternalPartitionsOffsets> getPartitionOffsets(Map<String, TopicDescription>\n                                                                  descriptionsMap,\n                                                              ReactiveAdminClient ac) {\n    var descriptions = descriptionsMap.values();\n    return ac.listOffsets(descriptions, OffsetSpec.earliest())\n        .zipWith(ac.listOffsets(descriptions, OffsetSpec.latest()),\n            (earliest, latest) ->\n                Sets.intersection(earliest.keySet(), latest.keySet())\n                    .stream()\n                    .map(tp ->\n                        Map.entry(tp,\n                            new InternalPartitionsOffsets.Offsets(\n                                earliest.get(tp), latest.get(tp))))\n                    .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)))\n        .map(InternalPartitionsOffsets::new);\n  }\n\n  public Mono<InternalTopic> getTopicDetails(KafkaCluster cluster, String topicName) {\n    return loadTopic(cluster, topicName);\n  }\n\n  public Mono<List<ConfigEntry>> getTopicConfigs(KafkaCluster cluster, String topicName) {\n    // there 2 case that we cover here:\n    // 1. topic not found/visible - describeTopic() will be empty and we will throw TopicNotFoundException\n    // 2. topic is visible, but we don't have DESCRIBE_CONFIG permission - we should return empty list\n    return adminClientService.get(cluster)\n        .flatMap(ac -> ac.describeTopic(topicName)\n            .switchIfEmpty(Mono.error(new TopicNotFoundException()))\n            .then(ac.getTopicsConfig(List.of(topicName), true))\n            .map(m -> m.values().stream().findFirst().orElse(List.of())));\n  }\n\n  private Mono<InternalTopic> createTopic(KafkaCluster c, ReactiveAdminClient adminClient, TopicCreationDTO topicData) {\n    return adminClient.createTopic(\n            topicData.getName(),\n            topicData.getPartitions(),\n            topicData.getReplicationFactor(),\n            topicData.getConfigs())\n        .thenReturn(topicData)\n        .onErrorMap(t -> new TopicMetadataException(t.getMessage(), t))\n        .then(loadTopicAfterCreation(c, topicData.getName()));\n  }\n\n  public Mono<InternalTopic> createTopic(KafkaCluster cluster, TopicCreationDTO topicCreation) {\n    return adminClientService.get(cluster)\n        .flatMap(ac -> createTopic(cluster, ac, topicCreation));\n  }\n\n  public Mono<InternalTopic> recreateTopic(KafkaCluster cluster, String topicName) {\n    return loadTopic(cluster, topicName)\n        .flatMap(t -> deleteTopic(cluster, topicName)\n            .thenReturn(t)\n            .delayElement(Duration.ofSeconds(recreateDelayInSeconds))\n            .flatMap(topic ->\n                adminClientService.get(cluster)\n                    .flatMap(ac ->\n                        ac.createTopic(\n                                topic.getName(),\n                                topic.getPartitionCount(),\n                                topic.getReplicationFactor(),\n                                topic.getTopicConfigs()\n                                    .stream()\n                                    .collect(Collectors.toMap(InternalTopicConfig::getName,\n                                        InternalTopicConfig::getValue))\n                            )\n                            .thenReturn(topicName)\n                    )\n                    .retryWhen(\n                        Retry.fixedDelay(recreateMaxRetries, Duration.ofSeconds(recreateDelayInSeconds))\n                            .filter(TopicExistsException.class::isInstance)\n                            .onRetryExhaustedThrow((a, b) ->\n                                new TopicRecreationException(topicName,\n                                    recreateMaxRetries * recreateDelayInSeconds))\n                    )\n                    .flatMap(a -> loadTopicAfterCreation(cluster, topicName))\n            )\n        );\n  }\n\n  private Mono<InternalTopic> updateTopic(KafkaCluster cluster,\n                                          String topicName,\n                                          TopicUpdateDTO topicUpdate) {\n    return adminClientService.get(cluster)\n        .flatMap(ac ->\n            ac.updateTopicConfig(topicName, topicUpdate.getConfigs())\n                .then(loadTopic(cluster, topicName)));\n  }\n\n  public Mono<InternalTopic> updateTopic(KafkaCluster cl, String topicName,\n                                    Mono<TopicUpdateDTO> topicUpdate) {\n    return topicUpdate\n        .flatMap(t -> updateTopic(cl, topicName, t));\n  }\n\n  private Mono<InternalTopic> changeReplicationFactor(\n      KafkaCluster cluster,\n      ReactiveAdminClient adminClient,\n      String topicName,\n      Map<TopicPartition, Optional<NewPartitionReassignment>> reassignments\n  ) {\n    return adminClient.alterPartitionReassignments(reassignments)\n        .then(loadTopic(cluster, topicName));\n  }\n\n  /**\n   * Change topic replication factor, works on brokers versions 5.4.x and higher\n   */\n  public Mono<ReplicationFactorChangeResponseDTO> changeReplicationFactor(\n      KafkaCluster cluster,\n      String topicName,\n      ReplicationFactorChangeDTO replicationFactorChange) {\n    return loadTopic(cluster, topicName).flatMap(topic -> adminClientService.get(cluster)\n        .flatMap(ac -> {\n          Integer actual = topic.getReplicationFactor();\n          Integer requested = replicationFactorChange.getTotalReplicationFactor();\n          Integer brokersCount = statisticsCache.get(cluster).getClusterDescription()\n              .getNodes().size();\n\n          if (requested.equals(actual)) {\n            return Mono.error(\n                new ValidationException(\n                    String.format(\"Topic already has replicationFactor %s.\", actual)));\n          }\n          if (requested <= 0) {\n            return Mono.error(\n                new ValidationException(\n                    String.format(\"Requested replication factor (%s) should be greater or equal to 1.\", requested)));\n          }\n          if (requested > brokersCount) {\n            return Mono.error(\n                new ValidationException(\n                    String.format(\"Requested replication factor %s more than brokers count %s.\",\n                        requested, brokersCount)));\n          }\n          return changeReplicationFactor(cluster, ac, topicName,\n              getPartitionsReassignments(cluster, topic,\n                  replicationFactorChange));\n        })\n        .map(t -> new ReplicationFactorChangeResponseDTO()\n            .topicName(t.getName())\n            .totalReplicationFactor(t.getReplicationFactor())));\n  }\n\n  private Map<TopicPartition, Optional<NewPartitionReassignment>> getPartitionsReassignments(\n      KafkaCluster cluster,\n      InternalTopic topic,\n      ReplicationFactorChangeDTO replicationFactorChange) {\n    // Current assignment map (Partition number -> List of brokers)\n    Map<Integer, List<Integer>> currentAssignment = getCurrentAssignment(topic);\n    // Brokers map (Broker id -> count)\n    Map<Integer, Integer> brokersUsage = getBrokersMap(cluster, currentAssignment);\n    int currentReplicationFactor = topic.getReplicationFactor();\n\n    // If we should to increase Replication factor\n    if (replicationFactorChange.getTotalReplicationFactor() > currentReplicationFactor) {\n      // For each partition\n      for (var assignmentList : currentAssignment.values()) {\n        // Get brokers list sorted by usage\n        var brokers = brokersUsage.entrySet().stream()\n            .sorted(Map.Entry.comparingByValue())\n            .map(Map.Entry::getKey)\n            .collect(toList());\n\n        // Iterate brokers and try to add them in assignment\n        // while partition replicas count != requested replication factor\n        for (Integer broker : brokers) {\n          if (!assignmentList.contains(broker)) {\n            assignmentList.add(broker);\n            brokersUsage.merge(broker, 1, Integer::sum);\n          }\n          if (assignmentList.size() == replicationFactorChange.getTotalReplicationFactor()) {\n            break;\n          }\n        }\n        if (assignmentList.size() != replicationFactorChange.getTotalReplicationFactor()) {\n          throw new ValidationException(\"Something went wrong during adding replicas\");\n        }\n      }\n\n      // If we should to decrease Replication factor\n    } else if (replicationFactorChange.getTotalReplicationFactor() < currentReplicationFactor) {\n      for (Map.Entry<Integer, List<Integer>> assignmentEntry : currentAssignment.entrySet()) {\n        var partition = assignmentEntry.getKey();\n        var brokers = assignmentEntry.getValue();\n\n        // Get brokers list sorted by usage in reverse order\n        var brokersUsageList = brokersUsage.entrySet().stream()\n            .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))\n            .map(Map.Entry::getKey)\n            .collect(toList());\n\n        // Iterate brokers and try to remove them from assignment\n        // while partition replicas count != requested replication factor\n        for (Integer broker : brokersUsageList) {\n          // Check is the broker the leader of partition\n          if (!topic.getPartitions().get(partition).getLeader()\n              .equals(broker)) {\n            brokers.remove(broker);\n            brokersUsage.merge(broker, -1, Integer::sum);\n          }\n          if (brokers.size() == replicationFactorChange.getTotalReplicationFactor()) {\n            break;\n          }\n        }\n        if (brokers.size() != replicationFactorChange.getTotalReplicationFactor()) {\n          throw new ValidationException(\"Something went wrong during removing replicas\");\n        }\n      }\n    } else {\n      throw new ValidationException(\"Replication factor already equals requested\");\n    }\n\n    // Return result map\n    return currentAssignment.entrySet().stream().collect(toMap(\n        e -> new TopicPartition(topic.getName(), e.getKey()),\n        e -> Optional.of(new NewPartitionReassignment(e.getValue()))\n    ));\n  }\n\n  private Map<Integer, List<Integer>> getCurrentAssignment(InternalTopic topic) {\n    return topic.getPartitions().values().stream()\n        .collect(toMap(\n            InternalPartition::getPartition,\n            p -> p.getReplicas().stream()\n                .map(InternalReplica::getBroker)\n                .collect(toList())\n        ));\n  }\n\n  private Map<Integer, Integer> getBrokersMap(KafkaCluster cluster,\n                                              Map<Integer, List<Integer>> currentAssignment) {\n    Map<Integer, Integer> result = statisticsCache.get(cluster).getClusterDescription().getNodes()\n        .stream()\n        .map(Node::id)\n        .collect(toMap(\n            c -> c,\n            c -> 0\n        ));\n    currentAssignment.values().forEach(brokers -> brokers\n        .forEach(broker -> result.put(broker, result.get(broker) + 1)));\n\n    return result;\n  }\n\n  public Mono<PartitionsIncreaseResponseDTO> increaseTopicPartitions(\n      KafkaCluster cluster,\n      String topicName,\n      PartitionsIncreaseDTO partitionsIncrease) {\n    return loadTopic(cluster, topicName).flatMap(topic ->\n        adminClientService.get(cluster).flatMap(ac -> {\n          Integer actualCount = topic.getPartitionCount();\n          Integer requestedCount = partitionsIncrease.getTotalPartitionsCount();\n\n          if (requestedCount < actualCount) {\n            return Mono.error(\n                new ValidationException(String.format(\n                    \"Topic currently has %s partitions, which is higher than the requested %s.\",\n                    actualCount, requestedCount)));\n          }\n          if (requestedCount.equals(actualCount)) {\n            return Mono.error(\n                new ValidationException(\n                    String.format(\"Topic already has %s partitions.\", actualCount)));\n          }\n\n          Map<String, NewPartitions> newPartitionsMap = Collections.singletonMap(\n              topicName,\n              NewPartitions.increaseTo(partitionsIncrease.getTotalPartitionsCount())\n          );\n          return ac.createPartitions(newPartitionsMap)\n              .then(loadTopic(cluster, topicName));\n        }).map(t -> new PartitionsIncreaseResponseDTO()\n            .topicName(t.getName())\n            .totalPartitionsCount(t.getPartitionCount())\n        )\n    );\n  }\n\n  public Mono<Void> deleteTopic(KafkaCluster cluster, String topicName) {\n    if (statisticsCache.get(cluster).getFeatures().contains(ClusterFeature.TOPIC_DELETION)) {\n      return adminClientService.get(cluster).flatMap(c -> c.deleteTopic(topicName))\n          .doOnSuccess(t -> statisticsCache.onTopicDelete(cluster, topicName));\n    } else {\n      return Mono.error(new ValidationException(\"Topic deletion restricted\"));\n    }\n  }\n\n  public Mono<InternalTopic> cloneTopic(\n      KafkaCluster cluster, String topicName, String newTopicName) {\n    return loadTopic(cluster, topicName).flatMap(topic ->\n        adminClientService.get(cluster)\n            .flatMap(ac ->\n                ac.createTopic(\n                    newTopicName,\n                    topic.getPartitionCount(),\n                    topic.getReplicationFactor(),\n                    topic.getTopicConfigs()\n                        .stream()\n                        .collect(Collectors\n                            .toMap(InternalTopicConfig::getName, InternalTopicConfig::getValue))\n                )\n            ).thenReturn(newTopicName)\n            .flatMap(a -> loadTopicAfterCreation(cluster, newTopicName))\n    );\n  }\n\n  public Mono<List<InternalTopic>> getTopicsForPagination(KafkaCluster cluster) {\n    Statistics stats = statisticsCache.get(cluster);\n    return filterExisting(cluster, stats.getTopicDescriptions().keySet())\n        .map(lst -> lst.stream()\n            .map(topicName ->\n                InternalTopic.from(\n                    stats.getTopicDescriptions().get(topicName),\n                    stats.getTopicConfigs().getOrDefault(topicName, List.of()),\n                    InternalPartitionsOffsets.empty(),\n                    stats.getMetrics(),\n                    stats.getLogDirInfo(),\n                    clustersProperties.getInternalTopicPrefix()\n                    ))\n            .collect(toList())\n        );\n  }\n\n  public Mono<Map<TopicPartition, List<ProducerState>>> getActiveProducersState(KafkaCluster cluster, String topic) {\n    return adminClientService.get(cluster)\n        .flatMap(ac -> ac.getActiveProducersState(topic));\n  }\n\n  private Mono<List<String>> filterExisting(KafkaCluster cluster, Collection<String> topics) {\n    return adminClientService.get(cluster)\n        .flatMap(ac -> ac.listTopics(true))\n        .map(existing -> existing\n            .stream()\n            .filter(topics::contains)\n            .collect(toList()));\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/acl/AclCsv.java",
    "content": "package com.provectus.kafka.ui.service.acl;\n\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport org.apache.kafka.common.acl.AccessControlEntry;\nimport org.apache.kafka.common.acl.AclBinding;\nimport org.apache.kafka.common.acl.AclOperation;\nimport org.apache.kafka.common.acl.AclPermissionType;\nimport org.apache.kafka.common.resource.PatternType;\nimport org.apache.kafka.common.resource.ResourcePattern;\nimport org.apache.kafka.common.resource.ResourceType;\n\npublic class AclCsv {\n\n  private static final String LINE_SEPARATOR = System.lineSeparator();\n  private static final String VALUES_SEPARATOR = \",\";\n  private static final String HEADER = \"Principal,ResourceType,PatternType,ResourceName,Operation,PermissionType,Host\";\n\n  public static String transformToCsvString(Collection<AclBinding> acls) {\n    return Stream.concat(Stream.of(HEADER), acls.stream().map(AclCsv::createAclString))\n        .collect(Collectors.joining(System.lineSeparator()));\n  }\n\n  public static String createAclString(AclBinding binding) {\n    var pattern = binding.pattern();\n    var filter  = binding.toFilter().entryFilter();\n    return String.format(\n        \"%s,%s,%s,%s,%s,%s,%s\",\n        filter.principal(),\n        pattern.resourceType(),\n        pattern.patternType(),\n        pattern.name(),\n        filter.operation(),\n        filter.permissionType(),\n        filter.host()\n    );\n  }\n\n  private static AclBinding parseCsvLine(String csv, int line) {\n    String[] values = csv.split(VALUES_SEPARATOR);\n    if (values.length != 7) {\n      throw new ValidationException(\"Input csv is not valid - there should be 7 columns in line \" + line);\n    }\n    for (int i = 0; i < values.length; i++) {\n      if ((values[i] = values[i].trim()).isBlank()) {\n        throw new ValidationException(\"Input csv is not valid - blank value in colum \" + i + \", line \" + line);\n      }\n    }\n    try {\n      return new AclBinding(\n          new ResourcePattern(\n              ResourceType.valueOf(values[1]), values[3], PatternType.valueOf(values[2])),\n          new AccessControlEntry(\n              values[0], values[6], AclOperation.valueOf(values[4]), AclPermissionType.valueOf(values[5]))\n      );\n    } catch (IllegalArgumentException enumParseError) {\n      throw new ValidationException(\"Error parsing enum value in line \" + line);\n    }\n  }\n\n  public static Collection<AclBinding> parseCsv(String csvString) {\n    String[] lines = csvString.split(LINE_SEPARATOR);\n    if (lines.length == 0) {\n      throw new ValidationException(\"Error parsing ACL csv file: no lines in file\");\n    }\n    boolean firstLineIsHeader = HEADER.equalsIgnoreCase(lines[0].trim().replace(\" \", \"\"));\n    Set<AclBinding> result = new HashSet<>();\n    for (int i = firstLineIsHeader ? 1 : 0; i < lines.length; i++) {\n      String line = lines[i];\n      if (!line.isBlank()) {\n        AclBinding aclBinding = parseCsvLine(line, i);\n        result.add(aclBinding);\n      }\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/acl/AclsService.java",
    "content": "package com.provectus.kafka.ui.service.acl;\n\nimport static org.apache.kafka.common.acl.AclOperation.ALL;\nimport static org.apache.kafka.common.acl.AclOperation.CREATE;\nimport static org.apache.kafka.common.acl.AclOperation.DESCRIBE;\nimport static org.apache.kafka.common.acl.AclOperation.IDEMPOTENT_WRITE;\nimport static org.apache.kafka.common.acl.AclOperation.READ;\nimport static org.apache.kafka.common.acl.AclOperation.WRITE;\nimport static org.apache.kafka.common.acl.AclPermissionType.ALLOW;\nimport static org.apache.kafka.common.resource.PatternType.LITERAL;\nimport static org.apache.kafka.common.resource.PatternType.PREFIXED;\nimport static org.apache.kafka.common.resource.ResourceType.CLUSTER;\nimport static org.apache.kafka.common.resource.ResourceType.GROUP;\nimport static org.apache.kafka.common.resource.ResourceType.TOPIC;\nimport static org.apache.kafka.common.resource.ResourceType.TRANSACTIONAL_ID;\n\nimport com.google.common.collect.Sets;\nimport com.provectus.kafka.ui.model.CreateConsumerAclDTO;\nimport com.provectus.kafka.ui.model.CreateProducerAclDTO;\nimport com.provectus.kafka.ui.model.CreateStreamAppAclDTO;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.service.AdminClientService;\nimport com.provectus.kafka.ui.service.ReactiveAdminClient;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Set;\nimport javax.annotation.Nullable;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.kafka.common.acl.AccessControlEntry;\nimport org.apache.kafka.common.acl.AclBinding;\nimport org.apache.kafka.common.acl.AclOperation;\nimport org.apache.kafka.common.resource.Resource;\nimport org.apache.kafka.common.resource.ResourcePattern;\nimport org.apache.kafka.common.resource.ResourcePatternFilter;\nimport org.apache.kafka.common.resource.ResourceType;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.CollectionUtils;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\n@Slf4j\n@Service\n@RequiredArgsConstructor\npublic class AclsService {\n\n  private final AdminClientService adminClientService;\n\n  public Mono<Void> createAcl(KafkaCluster cluster, AclBinding aclBinding) {\n    return adminClientService.get(cluster)\n        .flatMap(ac -> createAclsWithLogging(ac, List.of(aclBinding)));\n  }\n\n  private Mono<Void> createAclsWithLogging(ReactiveAdminClient ac, Collection<AclBinding> bindings) {\n    bindings.forEach(b -> log.info(\"CREATING ACL: [{}]\", AclCsv.createAclString(b)));\n    return ac.createAcls(bindings)\n        .doOnSuccess(v -> bindings.forEach(b -> log.info(\"ACL CREATED: [{}]\", AclCsv.createAclString(b))));\n  }\n\n  public Mono<Void> deleteAcl(KafkaCluster cluster, AclBinding aclBinding) {\n    var aclString = AclCsv.createAclString(aclBinding);\n    log.info(\"DELETING ACL: [{}]\", aclString);\n    return adminClientService.get(cluster)\n        .flatMap(ac -> ac.deleteAcls(List.of(aclBinding)))\n        .doOnSuccess(v -> log.info(\"ACL DELETED: [{}]\", aclString));\n  }\n\n  public Flux<AclBinding> listAcls(KafkaCluster cluster, ResourcePatternFilter filter) {\n    return adminClientService.get(cluster)\n        .flatMap(c -> c.listAcls(filter))\n        .flatMapIterable(acls -> acls)\n        .sort(Comparator.comparing(AclBinding::toString));  //sorting to keep stable order on different calls\n  }\n\n  public Mono<String> getAclAsCsvString(KafkaCluster cluster) {\n    return adminClientService.get(cluster)\n        .flatMap(c -> c.listAcls(ResourcePatternFilter.ANY))\n        .map(AclCsv::transformToCsvString);\n  }\n\n  public Mono<Void> syncAclWithAclCsv(KafkaCluster cluster, String csv) {\n    return adminClientService.get(cluster)\n        .flatMap(ac -> ac.listAcls(ResourcePatternFilter.ANY).flatMap(existingAclList -> {\n          var existingSet = Set.copyOf(existingAclList);\n          var newAcls = Set.copyOf(AclCsv.parseCsv(csv));\n          var toDelete = Sets.difference(existingSet, newAcls);\n          var toAdd = Sets.difference(newAcls, existingSet);\n          logAclSyncPlan(cluster, toAdd, toDelete);\n          if (toAdd.isEmpty() && toDelete.isEmpty()) {\n            return Mono.empty();\n          }\n          log.info(\"Starting new ACLs creation\");\n          return ac.createAcls(toAdd)\n              .doOnSuccess(v -> {\n                log.info(\"{} new ACLs created\", toAdd.size());\n                log.info(\"Starting ACLs deletion\");\n              })\n              .then(ac.deleteAcls(toDelete)\n                  .doOnSuccess(v -> log.info(\"{} ACLs deleted\", toDelete.size())));\n        }));\n  }\n\n  private void logAclSyncPlan(KafkaCluster cluster, Set<AclBinding> toBeAdded, Set<AclBinding> toBeDeleted) {\n    log.info(\"'{}' cluster ACL sync plan: \", cluster.getName());\n    if (toBeAdded.isEmpty() && toBeDeleted.isEmpty()) {\n      log.info(\"Nothing to do, ACL is already in sync\");\n      return;\n    }\n    if (!toBeAdded.isEmpty()) {\n      log.info(\"ACLs to be added ({}): \", toBeAdded.size());\n      for (AclBinding aclBinding : toBeAdded) {\n        log.info(\" \" + AclCsv.createAclString(aclBinding));\n      }\n    }\n    if (!toBeDeleted.isEmpty()) {\n      log.info(\"ACLs to be deleted ({}): \", toBeDeleted.size());\n      for (AclBinding aclBinding : toBeDeleted) {\n        log.info(\" \" + AclCsv.createAclString(aclBinding));\n      }\n    }\n  }\n\n  // creates allow binding for resources by prefix or specific names list\n  private List<AclBinding> createAllowBindings(ResourceType resourceType,\n                                               List<AclOperation> opsToAllow,\n                                               String principal,\n                                               String host,\n                                               @Nullable String resourcePrefix,\n                                               @Nullable Collection<String> resourceNames) {\n    List<AclBinding> bindings = new ArrayList<>();\n    if (resourcePrefix != null) {\n      for (var op : opsToAllow) {\n        bindings.add(\n            new AclBinding(\n                new ResourcePattern(resourceType, resourcePrefix, PREFIXED),\n                new AccessControlEntry(principal, host, op, ALLOW)));\n      }\n    }\n    if (!CollectionUtils.isEmpty(resourceNames)) {\n      resourceNames.stream()\n          .distinct()\n          .forEach(resource ->\n              opsToAllow.forEach(op ->\n                  bindings.add(\n                      new AclBinding(\n                          new ResourcePattern(resourceType, resource, LITERAL),\n                          new AccessControlEntry(principal, host, op, ALLOW)))));\n    }\n    return bindings;\n  }\n\n  public Mono<Void> createConsumerAcl(KafkaCluster cluster, CreateConsumerAclDTO request) {\n    return adminClientService.get(cluster)\n        .flatMap(ac -> createAclsWithLogging(ac, createConsumerBindings(request)))\n        .then();\n  }\n\n  //Read, Describe on topics, Read on consumerGroups\n  private List<AclBinding> createConsumerBindings(CreateConsumerAclDTO request) {\n    List<AclBinding> bindings = new ArrayList<>();\n    bindings.addAll(\n        createAllowBindings(TOPIC,\n            List.of(READ, DESCRIBE),\n            request.getPrincipal(),\n            request.getHost(),\n            request.getTopicsPrefix(),\n            request.getTopics()));\n\n    bindings.addAll(\n        createAllowBindings(\n            GROUP,\n            List.of(READ),\n            request.getPrincipal(),\n            request.getHost(),\n            request.getConsumerGroupsPrefix(),\n            request.getConsumerGroups()));\n    return bindings;\n  }\n\n  public Mono<Void> createProducerAcl(KafkaCluster cluster, CreateProducerAclDTO request) {\n    return adminClientService.get(cluster)\n        .flatMap(ac -> createAclsWithLogging(ac, createProducerBindings(request)))\n        .then();\n  }\n\n  //Write, Describe, Create permission on topics, Write, Describe on transactionalIds\n  //IDEMPOTENT_WRITE on cluster if idempotent is enabled\n  private List<AclBinding> createProducerBindings(CreateProducerAclDTO request) {\n    List<AclBinding> bindings = new ArrayList<>();\n    bindings.addAll(\n        createAllowBindings(\n            TOPIC,\n            List.of(WRITE, DESCRIBE, CREATE),\n            request.getPrincipal(),\n            request.getHost(),\n            request.getTopicsPrefix(),\n            request.getTopics()));\n\n    bindings.addAll(\n        createAllowBindings(\n            TRANSACTIONAL_ID,\n            List.of(WRITE, DESCRIBE),\n            request.getPrincipal(),\n            request.getHost(),\n            request.getTransactionsIdPrefix(),\n            Optional.ofNullable(request.getTransactionalId()).map(List::of).orElse(null)));\n\n    if (Boolean.TRUE.equals(request.getIdempotent())) {\n      bindings.addAll(\n          createAllowBindings(\n              CLUSTER,\n              List.of(IDEMPOTENT_WRITE),\n              request.getPrincipal(),\n              request.getHost(),\n              null,\n              List.of(Resource.CLUSTER_NAME))); // cluster name is a const string in ACL api\n    }\n    return bindings;\n  }\n\n  public Mono<Void> createStreamAppAcl(KafkaCluster cluster, CreateStreamAppAclDTO request) {\n    return adminClientService.get(cluster)\n        .flatMap(ac -> createAclsWithLogging(ac, createStreamAppBindings(request)))\n        .then();\n  }\n\n  // Read on input topics, Write on output topics\n  // ALL on applicationId-prefixed Groups and Topics\n  private List<AclBinding> createStreamAppBindings(CreateStreamAppAclDTO request) {\n    List<AclBinding> bindings = new ArrayList<>();\n    bindings.addAll(\n        createAllowBindings(\n            TOPIC,\n            List.of(READ),\n            request.getPrincipal(),\n            request.getHost(),\n            null,\n            request.getInputTopics()));\n\n    bindings.addAll(\n        createAllowBindings(\n            TOPIC,\n            List.of(WRITE),\n            request.getPrincipal(),\n            request.getHost(),\n            null,\n            request.getOutputTopics()));\n\n    bindings.addAll(\n        createAllowBindings(\n            GROUP,\n            List.of(ALL),\n            request.getPrincipal(),\n            request.getHost(),\n            request.getApplicationId(),\n            null));\n\n    bindings.addAll(\n        createAllowBindings(\n            TOPIC,\n            List.of(ALL),\n            request.getPrincipal(),\n            request.getHost(),\n            request.getApplicationId(),\n            null));\n    return bindings;\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/AnalysisTasksStore.java",
    "content": "package com.provectus.kafka.ui.service.analyze;\n\nimport com.google.common.base.Throwables;\nimport com.provectus.kafka.ui.model.TopicAnalysisDTO;\nimport com.provectus.kafka.ui.model.TopicAnalysisProgressDTO;\nimport com.provectus.kafka.ui.model.TopicAnalysisResultDTO;\nimport java.io.Closeable;\nimport java.math.BigDecimal;\nimport java.time.Instant;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.stream.Collectors;\nimport javax.annotation.Nullable;\nimport lombok.Builder;\nimport lombok.SneakyThrows;\nimport lombok.Value;\n\nclass AnalysisTasksStore {\n\n  private final Map<TopicIdentity, RunningAnalysis> running = new ConcurrentHashMap<>();\n  private final Map<TopicIdentity, TopicAnalysisResultDTO> completed = new ConcurrentHashMap<>();\n\n  void setAnalysisError(TopicIdentity topicId,\n                        Instant collectionStartedAt,\n                        Throwable th) {\n    running.remove(topicId);\n    completed.put(\n        topicId,\n        new TopicAnalysisResultDTO()\n            .startedAt(collectionStartedAt.toEpochMilli())\n            .finishedAt(System.currentTimeMillis())\n            .error(Throwables.getStackTraceAsString(th))\n    );\n  }\n\n  void setAnalysisResult(TopicIdentity topicId,\n                         Instant collectionStartedAt,\n                         TopicAnalysisStats totalStats,\n                         Map<Integer, TopicAnalysisStats> partitionStats) {\n    running.remove(topicId);\n    completed.put(topicId,\n        new TopicAnalysisResultDTO()\n            .startedAt(collectionStartedAt.toEpochMilli())\n            .finishedAt(System.currentTimeMillis())\n            .totalStats(totalStats.toDto(null))\n            .partitionStats(\n                partitionStats.entrySet().stream()\n                    .map(e -> e.getValue().toDto(e.getKey()))\n                    .collect(Collectors.toList())\n            ));\n  }\n\n  void updateProgress(TopicIdentity topicId,\n                      long msgsScanned,\n                      long bytesScanned,\n                      Double completeness) {\n    running.computeIfPresent(topicId, (k, state) ->\n        state.toBuilder()\n            .msgsScanned(msgsScanned)\n            .bytesScanned(bytesScanned)\n            .completenessPercent(completeness)\n            .build());\n  }\n\n  void registerNewTask(TopicIdentity topicId, Closeable task) {\n    running.put(topicId, new RunningAnalysis(Instant.now(), 0.0, 0, 0, task));\n  }\n\n  void cancelAnalysis(TopicIdentity topicId) {\n    Optional.ofNullable(running.remove(topicId))\n        .ifPresent(RunningAnalysis::stopTask);\n  }\n\n  boolean isAnalysisInProgress(TopicIdentity id) {\n    return running.containsKey(id);\n  }\n\n  Optional<TopicAnalysisDTO> getTopicAnalysis(TopicIdentity id) {\n    var runningState = running.get(id);\n    var completedState = completed.get(id);\n    if (runningState == null && completedState == null) {\n      return Optional.empty();\n    }\n    return Optional.of(createAnalysisDto(runningState, completedState));\n  }\n\n  private TopicAnalysisDTO createAnalysisDto(@Nullable RunningAnalysis runningState,\n                                             @Nullable TopicAnalysisResultDTO completedState) {\n    return new TopicAnalysisDTO()\n        .progress(runningState != null ? runningState.toDto() : null)\n        .result(completedState);\n  }\n\n  @Builder(toBuilder = true)\n  private record RunningAnalysis(Instant startedAt,\n                                 double completenessPercent,\n                                 long msgsScanned,\n                                 long bytesScanned,\n                                 Closeable task) {\n\n    TopicAnalysisProgressDTO toDto() {\n      return new TopicAnalysisProgressDTO()\n          .startedAt(startedAt.toEpochMilli())\n          .bytesScanned(bytesScanned)\n          .msgsScanned(msgsScanned)\n          .completenessPercent(BigDecimal.valueOf(completenessPercent));\n    }\n\n    @SneakyThrows\n    void stopTask() {\n      task.close();\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisService.java",
    "content": "package com.provectus.kafka.ui.service.analyze;\n\nimport static com.provectus.kafka.ui.model.SeekTypeDTO.BEGINNING;\n\nimport com.provectus.kafka.ui.emitter.EnhancedConsumer;\nimport com.provectus.kafka.ui.emitter.SeekOperations;\nimport com.provectus.kafka.ui.exception.TopicAnalysisException;\nimport com.provectus.kafka.ui.model.ConsumerPosition;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.TopicAnalysisDTO;\nimport com.provectus.kafka.ui.service.ConsumerGroupService;\nimport com.provectus.kafka.ui.service.TopicsService;\nimport java.io.Closeable;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.kafka.clients.consumer.ConsumerConfig;\nimport org.apache.kafka.common.errors.InterruptException;\nimport org.apache.kafka.common.errors.WakeupException;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Scheduler;\nimport reactor.core.scheduler.Schedulers;\n\n\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class TopicAnalysisService {\n\n  private static final Scheduler SCHEDULER = Schedulers.newBoundedElastic(\n      Schedulers.DEFAULT_BOUNDED_ELASTIC_SIZE,\n      Schedulers.DEFAULT_BOUNDED_ELASTIC_QUEUESIZE,\n      \"topic-analysis-tasks\",\n      10, //ttl for idle threads (in sec)\n      true //daemon\n  );\n\n  private final AnalysisTasksStore analysisTasksStore = new AnalysisTasksStore();\n\n  private final TopicsService topicsService;\n  private final ConsumerGroupService consumerGroupService;\n\n  public Mono<Void> analyze(KafkaCluster cluster, String topicName) {\n    return topicsService.getTopicDetails(cluster, topicName)\n        .doOnNext(topic -> startAnalysis(cluster, topicName))\n        .then();\n  }\n\n  private synchronized void startAnalysis(KafkaCluster cluster, String topic) {\n    var topicId = new TopicIdentity(cluster, topic);\n    if (analysisTasksStore.isAnalysisInProgress(topicId)) {\n      throw new TopicAnalysisException(\"Topic is already analyzing\");\n    }\n    var task = new AnalysisTask(cluster, topicId);\n    analysisTasksStore.registerNewTask(topicId, task);\n    SCHEDULER.schedule(task);\n  }\n\n  public void cancelAnalysis(KafkaCluster cluster, String topicName) {\n    analysisTasksStore.cancelAnalysis(new TopicIdentity(cluster, topicName));\n  }\n\n  public Optional<TopicAnalysisDTO> getTopicAnalysis(KafkaCluster cluster, String topicName) {\n    return analysisTasksStore.getTopicAnalysis(new TopicIdentity(cluster, topicName));\n  }\n\n  class AnalysisTask implements Runnable, Closeable {\n\n    private final Instant startedAt = Instant.now();\n\n    private final TopicIdentity topicId;\n\n    private final TopicAnalysisStats totalStats = new TopicAnalysisStats();\n    private final Map<Integer, TopicAnalysisStats> partitionStats = new HashMap<>();\n\n    private final EnhancedConsumer consumer;\n\n    AnalysisTask(KafkaCluster cluster, TopicIdentity topicId) {\n      this.topicId = topicId;\n      this.consumer = consumerGroupService.createConsumer(\n          cluster,\n          // to improve polling throughput\n          Map.of(\n              ConsumerConfig.RECEIVE_BUFFER_CONFIG, \"-1\", //let OS tune buffer size\n              ConsumerConfig.MAX_POLL_RECORDS_CONFIG, \"100000\"\n          )\n      );\n    }\n\n    @Override\n    public void close() {\n      consumer.wakeup();\n    }\n\n    @Override\n    public void run() {\n      try {\n        log.info(\"Starting {} topic analysis\", topicId);\n        consumer.partitionsFor(topicId.topicName)\n            .forEach(tp -> partitionStats.put(tp.partition(), new TopicAnalysisStats()));\n\n        var seekOperations = SeekOperations.create(consumer, new ConsumerPosition(BEGINNING, topicId.topicName, null));\n        long summaryOffsetsRange = seekOperations.summaryOffsetsRange();\n        seekOperations.assignAndSeekNonEmptyPartitions();\n\n        while (!seekOperations.assignedPartitionsFullyPolled()) {\n          var polled = consumer.pollEnhanced(Duration.ofSeconds(3));\n          polled.forEach(r -> {\n            totalStats.apply(r);\n            partitionStats.get(r.partition()).apply(r);\n          });\n          updateProgress(seekOperations.offsetsProcessedFromSeek(), summaryOffsetsRange);\n        }\n        analysisTasksStore.setAnalysisResult(topicId, startedAt, totalStats, partitionStats);\n        log.info(\"{} topic analysis finished\", topicId);\n      } catch (WakeupException | InterruptException cancelException) {\n        log.info(\"{} topic analysis stopped\", topicId);\n        // calling cancel for cases when our thread was interrupted by some non-user cancellation reason\n        analysisTasksStore.cancelAnalysis(topicId);\n      } catch (Throwable th) {\n        log.error(\"Error analyzing topic {}\", topicId, th);\n        analysisTasksStore.setAnalysisError(topicId, startedAt, th);\n      } finally {\n        consumer.close();\n      }\n    }\n\n    private void updateProgress(long processedOffsets, long summaryOffsetsRange) {\n      if (processedOffsets > 0 && summaryOffsetsRange != 0) {\n        analysisTasksStore.updateProgress(\n            topicId,\n            totalStats.totalMsgs,\n            totalStats.keysSize.sum + totalStats.valuesSize.sum,\n            Math.min(100.0, (((double) processedOffsets) / summaryOffsetsRange) * 100)\n        );\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisStats.java",
    "content": "package com.provectus.kafka.ui.service.analyze;\n\nimport com.provectus.kafka.ui.model.TopicAnalysisSizeStatsDTO;\nimport com.provectus.kafka.ui.model.TopicAnalysisStatsDTO;\nimport com.provectus.kafka.ui.model.TopicAnalysisStatsHourlyMsgCountsInnerDTO;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport javax.annotation.Nullable;\nimport org.apache.datasketches.hll.HllSketch;\nimport org.apache.datasketches.quantiles.DoublesSketch;\nimport org.apache.datasketches.quantiles.UpdateDoublesSketch;\nimport org.apache.kafka.clients.consumer.ConsumerRecord;\nimport org.apache.kafka.common.utils.Bytes;\n\nclass TopicAnalysisStats {\n\n  Long totalMsgs = 0L;\n  Long minOffset;\n  Long maxOffset;\n\n  Long minTimestamp;\n  Long maxTimestamp;\n\n  long nullKeys = 0L;\n  long nullValues = 0L;\n\n  final SizeStats keysSize = new SizeStats();\n  final SizeStats valuesSize = new SizeStats();\n\n  final HllSketch uniqKeys = new HllSketch();\n  final HllSketch uniqValues = new HllSketch();\n\n  final HourlyCounts hourlyCounts = new HourlyCounts();\n\n  static class SizeStats {\n    long sum = 0;\n    Long min;\n    Long max;\n    final UpdateDoublesSketch sizeSketch = DoublesSketch.builder().build();\n\n    void apply(int len) {\n      sum += len;\n      min = minNullable(min, len);\n      max = maxNullable(max, len);\n      sizeSketch.update(len);\n    }\n\n    TopicAnalysisSizeStatsDTO toDto() {\n      return new TopicAnalysisSizeStatsDTO()\n          .sum(sum)\n          .min(min)\n          .max(max)\n          .avg((long) (((double) sum) / sizeSketch.getN()))\n          .prctl50((long) sizeSketch.getQuantile(0.5))\n          .prctl75((long) sizeSketch.getQuantile(0.75))\n          .prctl95((long) sizeSketch.getQuantile(0.95))\n          .prctl99((long) sizeSketch.getQuantile(0.99))\n          .prctl999((long) sizeSketch.getQuantile(0.999));\n    }\n  }\n\n  static class HourlyCounts {\n\n    // hour start ms -> count\n    private final Map<Long, Long> hourlyStats = new HashMap<>();\n    private final long minTs = Instant.now().minus(Duration.ofDays(14)).toEpochMilli();\n\n    void apply(ConsumerRecord<?, ?> rec) {\n      if (rec.timestamp() > minTs) {\n        var hourStart = rec.timestamp() - rec.timestamp() % (1_000 * 60 * 60);\n        hourlyStats.compute(hourStart, (h, cnt) -> cnt == null ? 1 : cnt + 1);\n      }\n    }\n\n    List<TopicAnalysisStatsHourlyMsgCountsInnerDTO> toDto() {\n      return hourlyStats.entrySet().stream()\n          .sorted(Comparator.comparingLong(Map.Entry::getKey))\n          .map(e -> new TopicAnalysisStatsHourlyMsgCountsInnerDTO()\n              .hourStart(e.getKey())\n              .count(e.getValue()))\n          .collect(Collectors.toList());\n    }\n  }\n\n  void apply(ConsumerRecord<Bytes, Bytes> rec) {\n    totalMsgs++;\n    minTimestamp = minNullable(minTimestamp, rec.timestamp());\n    maxTimestamp = maxNullable(maxTimestamp, rec.timestamp());\n    minOffset = minNullable(minOffset, rec.offset());\n    maxOffset = maxNullable(maxOffset, rec.offset());\n    hourlyCounts.apply(rec);\n\n    if (rec.key() != null) {\n      byte[] keyBytes = rec.key().get();\n      keysSize.apply(rec.serializedKeySize());\n      uniqKeys.update(keyBytes);\n    } else {\n      nullKeys++;\n    }\n\n    if (rec.value() != null) {\n      byte[] valueBytes = rec.value().get();\n      valuesSize.apply(rec.serializedValueSize());\n      uniqValues.update(valueBytes);\n    } else {\n      nullValues++;\n    }\n  }\n\n  TopicAnalysisStatsDTO toDto(@Nullable Integer partition) {\n    return new TopicAnalysisStatsDTO()\n        .partition(partition)\n        .totalMsgs(totalMsgs)\n        .minOffset(minOffset)\n        .maxOffset(maxOffset)\n        .minTimestamp(minTimestamp)\n        .maxTimestamp(maxTimestamp)\n        .nullKeys(nullKeys)\n        .nullValues(nullValues)\n        // because of hll error estimated size can be greater that actual msgs count\n        .approxUniqKeys(Math.min(totalMsgs, (long) uniqKeys.getEstimate()))\n        .approxUniqValues(Math.min(totalMsgs, (long) uniqValues.getEstimate()))\n        .keySize(keysSize.toDto())\n        .valueSize(valuesSize.toDto())\n        .hourlyMsgCounts(hourlyCounts.toDto());\n  }\n\n  private static Long maxNullable(@Nullable Long v1, long v2) {\n    return v1 == null ? v2 : Math.max(v1, v2);\n  }\n\n  private static Long minNullable(@Nullable Long v1, long v2) {\n    return v1 == null ? v2 : Math.min(v1, v2);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicIdentity.java",
    "content": "package com.provectus.kafka.ui.service.analyze;\n\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\n\n@ToString\n@EqualsAndHashCode\nclass TopicIdentity {\n  final String clusterName;\n  final String topicName;\n\n  public TopicIdentity(KafkaCluster cluster, String topic) {\n    this.clusterName = cluster.getName();\n    this.topicName = topic;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/audit/AuditRecord.java",
    "content": "package com.provectus.kafka.ui.service.audit;\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport com.provectus.kafka.ui.exception.CustomBaseException;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.model.rbac.AccessContext;\nimport com.provectus.kafka.ui.model.rbac.Resource;\nimport com.provectus.kafka.ui.model.rbac.permission.PermissibleAction;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport javax.annotation.Nullable;\nimport lombok.SneakyThrows;\nimport org.springframework.security.access.AccessDeniedException;\n\nrecord AuditRecord(String timestamp,\n                   String username,\n                   String clusterName,\n                   List<AuditResource> resources,\n                   String operation,\n                   Object operationParams,\n                   OperationResult result) {\n\n  static final JsonMapper MAPPER = new JsonMapper();\n\n  static {\n    MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);\n  }\n\n  @SneakyThrows\n  String toJson() {\n    return MAPPER.writeValueAsString(this);\n  }\n\n  record AuditResource(String accessType, boolean alter, Resource type, @Nullable Object id) {\n\n    private static AuditResource create(PermissibleAction action, Resource type, @Nullable Object id) {\n      return new AuditResource(action.name(), action.isAlter(), type, id);\n    }\n\n    static List<AuditResource> getAccessedResources(AccessContext ctx) {\n      List<AuditResource> resources = new ArrayList<>();\n      ctx.getClusterConfigActions()\n          .forEach(a -> resources.add(create(a, Resource.CLUSTERCONFIG, null)));\n      ctx.getTopicActions()\n          .forEach(a -> resources.add(create(a, Resource.TOPIC, nameId(ctx.getTopic()))));\n      ctx.getConsumerGroupActions()\n          .forEach(a -> resources.add(create(a, Resource.CONSUMER, nameId(ctx.getConsumerGroup()))));\n      ctx.getConnectActions()\n          .forEach(a -> {\n            Map<String, String> resourceId = new LinkedHashMap<>();\n            resourceId.put(\"connect\", ctx.getConnect());\n            if (ctx.getConnector() != null) {\n              resourceId.put(\"connector\", ctx.getConnector());\n            }\n            resources.add(create(a, Resource.CONNECT, resourceId));\n          });\n      ctx.getSchemaActions()\n          .forEach(a -> resources.add(create(a, Resource.SCHEMA, nameId(ctx.getSchema()))));\n      ctx.getKsqlActions()\n          .forEach(a -> resources.add(create(a, Resource.KSQL, null)));\n      ctx.getAclActions()\n          .forEach(a -> resources.add(create(a, Resource.ACL, null)));\n      ctx.getAuditAction()\n          .forEach(a -> resources.add(create(a, Resource.AUDIT, null)));\n      return resources;\n    }\n\n    @Nullable\n    private static Map<String, Object> nameId(@Nullable String name) {\n      return name != null ? Map.of(\"name\", name) : null;\n    }\n  }\n\n  record OperationResult(boolean success, OperationError error) {\n\n    static OperationResult successful() {\n      return new OperationResult(true, null);\n    }\n\n    static OperationResult error(Throwable th) {\n      OperationError err = OperationError.UNRECOGNIZED_ERROR;\n      if (th instanceof AccessDeniedException) {\n        err = OperationError.ACCESS_DENIED;\n      } else if (th instanceof ValidationException) {\n        err = OperationError.VALIDATION_ERROR;\n      } else if (th instanceof CustomBaseException) {\n        err = OperationError.EXECUTION_ERROR;\n      }\n      return new OperationResult(false, err);\n    }\n\n    enum OperationError {\n      ACCESS_DENIED,\n      VALIDATION_ERROR,\n      EXECUTION_ERROR,\n      UNRECOGNIZED_ERROR\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/audit/AuditService.java",
    "content": "package com.provectus.kafka.ui.service.audit;\n\nimport static com.provectus.kafka.ui.config.ClustersProperties.AuditProperties.LogLevel.ALTER_ONLY;\nimport static com.provectus.kafka.ui.service.MessagesService.createProducer;\n\nimport com.google.common.annotations.VisibleForTesting;\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.config.auth.AuthenticatedUser;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.rbac.AccessContext;\nimport com.provectus.kafka.ui.service.AdminClientService;\nimport com.provectus.kafka.ui.service.ClustersStorage;\nimport com.provectus.kafka.ui.service.ReactiveAdminClient;\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.time.Duration;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\nimport javax.annotation.Nullable;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.kafka.clients.producer.KafkaProducer;\nimport org.apache.kafka.clients.producer.ProducerConfig;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.security.core.userdetails.UserDetails;\nimport org.springframework.stereotype.Service;\nimport reactor.core.publisher.Mono;\nimport reactor.core.publisher.Signal;\n\n\n@Slf4j\n@Service\npublic class AuditService implements Closeable {\n\n  private static final Mono<AuthenticatedUser> NO_AUTH_USER = Mono.just(new AuthenticatedUser(\"Unknown\", Set.of()));\n  private static final Duration BLOCK_TIMEOUT = Duration.ofSeconds(5);\n\n  private static final String DEFAULT_AUDIT_TOPIC_NAME = \"__kui-audit-log\";\n  private static final int DEFAULT_AUDIT_TOPIC_PARTITIONS = 1;\n  private static final Map<String, String> DEFAULT_AUDIT_TOPIC_CONFIG = Map.of(\n      \"retention.ms\", String.valueOf(TimeUnit.DAYS.toMillis(7)),\n      \"cleanup.policy\", \"delete\"\n  );\n  private static final Map<String, Object> AUDIT_PRODUCER_CONFIG = Map.of(\n      ProducerConfig.COMPRESSION_TYPE_CONFIG, \"gzip\"\n  );\n\n  private static final Logger AUDIT_LOGGER = LoggerFactory.getLogger(\"audit\");\n\n  private final Map<String, AuditWriter> auditWriters;\n\n  @Autowired\n  public AuditService(AdminClientService adminClientService, ClustersStorage clustersStorage) {\n    Map<String, AuditWriter> auditWriters = new HashMap<>();\n    for (var cluster : clustersStorage.getKafkaClusters()) {\n      Supplier<ReactiveAdminClient> adminClientSupplier = () -> adminClientService.get(cluster).block(BLOCK_TIMEOUT);\n      createAuditWriter(cluster, adminClientSupplier, () -> createProducer(cluster, AUDIT_PRODUCER_CONFIG))\n          .ifPresent(writer -> auditWriters.put(cluster.getName(), writer));\n    }\n    this.auditWriters = auditWriters;\n  }\n\n  @VisibleForTesting\n  AuditService(Map<String, AuditWriter> auditWriters) {\n    this.auditWriters = auditWriters;\n  }\n\n  @VisibleForTesting\n  static Optional<AuditWriter> createAuditWriter(KafkaCluster cluster,\n                                                 Supplier<ReactiveAdminClient> acSupplier,\n                                                 Supplier<KafkaProducer<byte[], byte[]>> producerFactory) {\n    var auditProps = cluster.getOriginalProperties().getAudit();\n    if (auditProps == null) {\n      return Optional.empty();\n    }\n    boolean topicAudit = Optional.ofNullable(auditProps.getTopicAuditEnabled()).orElse(false);\n    boolean consoleAudit = Optional.ofNullable(auditProps.getConsoleAuditEnabled()).orElse(false);\n    boolean alterLogOnly = Optional.ofNullable(auditProps.getLevel()).map(lvl -> lvl == ALTER_ONLY).orElse(true);\n    if (!topicAudit && !consoleAudit) {\n      return Optional.empty();\n    }\n    if (!topicAudit) {\n      log.info(\"Audit initialization finished for cluster '{}' (console only)\", cluster.getName());\n      return Optional.of(consoleOnlyWriter(cluster, alterLogOnly));\n    }\n    String auditTopicName = Optional.ofNullable(auditProps.getTopic()).orElse(DEFAULT_AUDIT_TOPIC_NAME);\n    boolean topicAuditCanBeDone = createTopicIfNeeded(cluster, acSupplier, auditTopicName, auditProps);\n    if (!topicAuditCanBeDone) {\n      if (consoleAudit) {\n        log.info(\n            \"Audit initialization finished for cluster '{}' (console only, topic audit init failed)\",\n            cluster.getName()\n        );\n        return Optional.of(consoleOnlyWriter(cluster, alterLogOnly));\n      }\n      return Optional.empty();\n    }\n    log.info(\"Audit initialization finished for cluster '{}'\", cluster.getName());\n    return Optional.of(\n        new AuditWriter(\n            cluster.getName(),\n            alterLogOnly,\n            auditTopicName,\n            producerFactory.get(),\n            consoleAudit ? AUDIT_LOGGER : null\n        )\n    );\n  }\n\n  private static AuditWriter consoleOnlyWriter(KafkaCluster cluster, boolean alterLogOnly) {\n    return new AuditWriter(cluster.getName(), alterLogOnly, null, null, AUDIT_LOGGER);\n  }\n\n  /**\n   * return true if topic created/existing and producing can be enabled.\n   */\n  private static boolean createTopicIfNeeded(KafkaCluster cluster,\n                                             Supplier<ReactiveAdminClient> acSupplier,\n                                             String auditTopicName,\n                                             ClustersProperties.AuditProperties auditProps) {\n    ReactiveAdminClient ac;\n    try {\n      ac = acSupplier.get();\n    } catch (Exception e) {\n      printAuditInitError(cluster, \"Error while connecting to the cluster\", e);\n      return false;\n    }\n    boolean topicExists;\n    try {\n      topicExists = ac.listTopics(true).block(BLOCK_TIMEOUT).contains(auditTopicName);\n    } catch (Exception e) {\n      printAuditInitError(cluster, \"Error checking audit topic existence\", e);\n      return false;\n    }\n    if (topicExists) {\n      return true;\n    }\n    try {\n      int topicPartitions =\n          Optional.ofNullable(auditProps.getAuditTopicsPartitions())\n              .orElse(DEFAULT_AUDIT_TOPIC_PARTITIONS);\n\n      Map<String, String> topicConfig = new HashMap<>(DEFAULT_AUDIT_TOPIC_CONFIG);\n      Optional.ofNullable(auditProps.getAuditTopicProperties())\n          .ifPresent(topicConfig::putAll);\n\n      log.info(\"Creating audit topic '{}' for cluster '{}'\", auditTopicName, cluster.getName());\n      ac.createTopic(auditTopicName, topicPartitions, null, topicConfig).block(BLOCK_TIMEOUT);\n      log.info(\"Audit topic created for cluster '{}'\", cluster.getName());\n      return true;\n    } catch (Exception e) {\n      printAuditInitError(cluster, \"Error creating topic '%s'\".formatted(auditTopicName), e);\n      return false;\n    }\n  }\n\n  private static void printAuditInitError(KafkaCluster cluster, String errorMsg, Exception cause) {\n    log.error(\"-----------------------------------------------------------------\");\n    log.error(\n        \"Error initializing Audit for cluster '{}'. Audit will be disabled. See error below: \",\n        cluster.getName()\n    );\n    log.error(\"{}\", errorMsg, cause);\n    log.error(\"-----------------------------------------------------------------\");\n  }\n\n  public boolean isAuditTopic(KafkaCluster cluster, String topic) {\n    var writer = auditWriters.get(cluster.getName());\n    return writer != null\n        && topic.equals(writer.targetTopic())\n        && writer.isTopicWritingEnabled();\n  }\n\n  public void audit(AccessContext acxt, Signal<?> sig) {\n    if (sig.isOnComplete()) {\n      extractUser(sig)\n          .doOnNext(u -> sendAuditRecord(acxt, u))\n          .subscribe();\n    } else if (sig.isOnError()) {\n      extractUser(sig)\n          .doOnNext(u -> sendAuditRecord(acxt, u, sig.getThrowable()))\n          .subscribe();\n    }\n  }\n\n  private Mono<AuthenticatedUser> extractUser(Signal<?> sig) {\n    //see ReactiveSecurityContextHolder for impl details\n    Object key = SecurityContext.class;\n    if (sig.getContextView().hasKey(key)) {\n      return sig.getContextView().<Mono<SecurityContext>>get(key)\n          .map(context -> context.getAuthentication().getPrincipal())\n          .cast(UserDetails.class)\n          .map(user -> {\n            var roles = user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());\n            return new AuthenticatedUser(user.getUsername(), roles);\n          })\n          .switchIfEmpty(NO_AUTH_USER);\n    } else {\n      return NO_AUTH_USER;\n    }\n  }\n\n  private void sendAuditRecord(AccessContext ctx, AuthenticatedUser user) {\n    sendAuditRecord(ctx, user, null);\n  }\n\n  private void sendAuditRecord(AccessContext ctx, AuthenticatedUser user, @Nullable Throwable th) {\n    try {\n      if (ctx.getCluster() != null) {\n        var writer = auditWriters.get(ctx.getCluster());\n        if (writer != null) {\n          writer.write(ctx, user, th);\n        }\n      } else {\n        // cluster-independent operation\n        AuditWriter.writeAppOperation(AUDIT_LOGGER, ctx, user, th);\n      }\n    } catch (Exception e) {\n      log.warn(\"Error sending audit record\", e);\n    }\n  }\n\n  @Override\n  public void close() throws IOException {\n    auditWriters.values().forEach(AuditWriter::close);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/audit/AuditWriter.java",
    "content": "package com.provectus.kafka.ui.service.audit;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\nimport com.provectus.kafka.ui.config.auth.AuthenticatedUser;\nimport com.provectus.kafka.ui.model.rbac.AccessContext;\nimport com.provectus.kafka.ui.service.audit.AuditRecord.AuditResource;\nimport com.provectus.kafka.ui.service.audit.AuditRecord.OperationResult;\nimport java.io.Closeable;\nimport java.time.Instant;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Optional;\nimport javax.annotation.Nullable;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.kafka.clients.producer.KafkaProducer;\nimport org.apache.kafka.clients.producer.ProducerRecord;\nimport org.slf4j.Logger;\n\n@Slf4j\nrecord AuditWriter(String clusterName,\n                   boolean logAlterOperationsOnly,\n                   @Nullable String targetTopic,\n                   @Nullable KafkaProducer<byte[], byte[]> producer,\n                   @Nullable Logger consoleLogger) implements Closeable {\n\n  boolean isTopicWritingEnabled() {\n    return producer != null;\n  }\n\n  // application-level (cluster-independent) operation\n  static void writeAppOperation(Logger consoleLogger,\n                                AccessContext ctx,\n                                AuthenticatedUser user,\n                                @Nullable Throwable th) {\n    consoleLogger.info(createRecord(ctx, user, th).toJson());\n  }\n\n  void write(AccessContext ctx, AuthenticatedUser user, @Nullable Throwable th) {\n    write(createRecord(ctx, user, th));\n  }\n\n  private void write(AuditRecord rec) {\n    if (logAlterOperationsOnly && rec.resources().stream().noneMatch(AuditResource::alter)) {\n      //we should only log alter operations, but this is read-only op\n      return;\n    }\n    String json = rec.toJson();\n    if (consoleLogger != null) {\n      consoleLogger.info(json);\n    }\n    if (targetTopic != null && producer != null) {\n      producer.send(\n          new ProducerRecord<>(targetTopic, null, json.getBytes(UTF_8)),\n          (metadata, ex) -> {\n            if (ex != null) {\n              log.warn(\"Error sending Audit record to kafka for cluster {}\", clusterName, ex);\n            }\n          });\n    }\n  }\n\n  private static AuditRecord createRecord(AccessContext ctx,\n                                          AuthenticatedUser user,\n                                          @Nullable Throwable th) {\n    return new AuditRecord(\n        DateTimeFormatter.ISO_INSTANT.format(Instant.now()),\n        user.principal(),\n        ctx.getCluster(), //can be null, if it is application-level action\n        AuditResource.getAccessedResources(ctx),\n        ctx.getOperationName(),\n        ctx.getOperationParams(),\n        th == null ? OperationResult.successful() : OperationResult.error(th)\n    );\n  }\n\n  @Override\n  public void close() {\n    Optional.ofNullable(producer).ifPresent(KafkaProducer::close);\n  }\n\n}\n\n\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/ConnectorInfo.java",
    "content": "package com.provectus.kafka.ui.service.integration.odd;\n\nimport com.provectus.kafka.ui.model.ConnectorTypeDTO;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.function.Function;\nimport java.util.stream.Stream;\nimport javax.annotation.Nullable;\nimport org.apache.commons.collections4.CollectionUtils;\nimport org.opendatadiscovery.oddrn.JdbcUrlParser;\nimport org.opendatadiscovery.oddrn.model.HivePath;\nimport org.opendatadiscovery.oddrn.model.MysqlPath;\nimport org.opendatadiscovery.oddrn.model.PostgreSqlPath;\nimport org.opendatadiscovery.oddrn.model.SnowflakePath;\n\nrecord ConnectorInfo(List<String> inputs,\n                     List<String> outputs) {\n\n  static ConnectorInfo extract(String className,\n                               ConnectorTypeDTO type,\n                               Map<String, Object> config,\n                               List<String> topicsFromApi, // can be empty for old Connect API versions\n                               Function<String, String> topicOddrnBuilder) {\n    return switch (className) {\n      case \"org.apache.kafka.connect.file.FileStreamSinkConnector\",\n          \"org.apache.kafka.connect.file.FileStreamSourceConnector\",\n          \"FileStreamSource\",\n          \"FileStreamSink\" -> extractFileIoConnector(type, topicsFromApi, config, topicOddrnBuilder);\n      case \"io.confluent.connect.s3.S3SinkConnector\" -> extractS3Sink(type, topicsFromApi, config, topicOddrnBuilder);\n      case \"io.confluent.connect.jdbc.JdbcSinkConnector\" ->\n          extractJdbcSink(type, topicsFromApi, config, topicOddrnBuilder);\n      case \"io.debezium.connector.postgresql.PostgresConnector\" -> extractDebeziumPg(config);\n      case \"io.debezium.connector.mysql.MySqlConnector\" -> extractDebeziumMysql(config);\n      default -> new ConnectorInfo(\n          extractInputs(type, topicsFromApi, config, topicOddrnBuilder),\n          extractOutputs(type, topicsFromApi, config, topicOddrnBuilder)\n      );\n    };\n  }\n\n  private static ConnectorInfo extractFileIoConnector(ConnectorTypeDTO type,\n                                                      List<String> topics,\n                                                      Map<String, Object> config,\n                                                      Function<String, String> topicOddrnBuilder) {\n    return new ConnectorInfo(\n        extractInputs(type, topics, config, topicOddrnBuilder),\n        extractOutputs(type, topics, config, topicOddrnBuilder)\n    );\n  }\n\n  private static ConnectorInfo extractJdbcSink(ConnectorTypeDTO type,\n                                               List<String> topics,\n                                               Map<String, Object> config,\n                                               Function<String, String> topicOddrnBuilder) {\n    String tableNameFormat = (String) config.getOrDefault(\"table.name.format\", \"${topic}\");\n    List<String> targetTables = extractTopicNamesBestEffort(topics, config)\n        .map(topic -> tableNameFormat.replace(\"${kafka}\", topic))\n        .toList();\n\n    String connectionUrl = (String) config.get(\"connection.url\");\n    List<String> outputs = new ArrayList<>();\n    @Nullable var knownJdbcPath = new JdbcUrlParser().parse(connectionUrl);\n    if (knownJdbcPath instanceof PostgreSqlPath p) {\n      targetTables.forEach(t -> outputs.add(p.toBuilder().table(t).build().oddrn()));\n    }\n    if (knownJdbcPath instanceof MysqlPath p) {\n      targetTables.forEach(t -> outputs.add(p.toBuilder().table(t).build().oddrn()));\n    }\n    if (knownJdbcPath instanceof HivePath p) {\n      targetTables.forEach(t -> outputs.add(p.toBuilder().table(t).build().oddrn()));\n    }\n    if (knownJdbcPath instanceof SnowflakePath p) {\n      targetTables.forEach(t -> outputs.add(p.toBuilder().table(t).build().oddrn()));\n    }\n    return new ConnectorInfo(\n        extractInputs(type, topics, config, topicOddrnBuilder),\n        outputs\n    );\n  }\n\n  private static ConnectorInfo extractDebeziumPg(Map<String, Object> config) {\n    String host = (String) config.get(\"database.hostname\");\n    String dbName = (String) config.get(\"database.dbname\");\n    var inputs = List.of(\n        PostgreSqlPath.builder()\n            .host(host)\n            .database(dbName)\n            .build().oddrn()\n    );\n    return new ConnectorInfo(inputs, List.of());\n  }\n\n  private static ConnectorInfo extractDebeziumMysql(Map<String, Object> config) {\n    String host = (String) config.get(\"database.hostname\");\n    var inputs = List.of(\n        MysqlPath.builder()\n            .host(host)\n            .build()\n            .oddrn()\n    );\n    return new ConnectorInfo(inputs, List.of());\n  }\n\n  private static ConnectorInfo extractS3Sink(ConnectorTypeDTO type,\n                                             List<String> topics,\n                                             Map<String, Object> config,\n                                             Function<String, String> topicOrrdnBuilder) {\n    String bucketName = (String) config.get(\"s3.bucket.name\");\n    String topicsDir = (String) config.getOrDefault(\"topics.dir\", \"topics\");\n    String directoryDelim = (String) config.getOrDefault(\"directory.delim\", \"/\");\n    List<String> outputs = extractTopicNamesBestEffort(topics, config)\n        .map(topic -> Oddrn.awsS3Oddrn(bucketName, topicsDir + directoryDelim + topic))\n        .toList();\n    return new ConnectorInfo(\n        extractInputs(type, topics, config, topicOrrdnBuilder),\n        outputs\n    );\n  }\n\n  private static List<String> extractInputs(ConnectorTypeDTO type,\n                                            List<String> topicsFromApi,\n                                            Map<String, Object> config,\n                                            Function<String, String> topicOrrdnBuilder) {\n    return type == ConnectorTypeDTO.SINK\n        ? extractTopicsOddrns(config, topicsFromApi, topicOrrdnBuilder)\n        : List.of();\n  }\n\n  private static List<String> extractOutputs(ConnectorTypeDTO type,\n                                             List<String> topicsFromApi,\n                                             Map<String, Object> config,\n                                             Function<String, String> topicOrrdnBuilder) {\n    return type == ConnectorTypeDTO.SOURCE\n        ? extractTopicsOddrns(config, topicsFromApi, topicOrrdnBuilder)\n        : List.of();\n  }\n\n  private static Stream<String> extractTopicNamesBestEffort(\n      // topic list can be empty for old Connect API versions\n      List<String> topicsFromApi,\n      Map<String, Object> config\n  ) {\n    if (CollectionUtils.isNotEmpty(topicsFromApi)) {\n      return topicsFromApi.stream();\n    }\n\n    // trying to extract topic names from config\n    String topicsString = (String) config.get(\"topics\");\n    String topicString = (String) config.get(\"topic\");\n    return Stream.of(topicsString, topicString)\n        .filter(Objects::nonNull)\n        .flatMap(str -> Stream.of(str.split(\",\")))\n        .map(String::trim)\n        .filter(s -> !s.isBlank());\n  }\n\n  private static List<String> extractTopicsOddrns(Map<String, Object> config,\n                                                  List<String> topicsFromApi,\n                                                  Function<String, String> topicOrrdnBuilder) {\n    return extractTopicNamesBestEffort(topicsFromApi, config)\n        .map(topicOrrdnBuilder)\n        .toList();\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/ConnectorsExporter.java",
    "content": "package com.provectus.kafka.ui.service.integration.odd;\n\nimport com.provectus.kafka.ui.connect.model.ConnectorTopics;\nimport com.provectus.kafka.ui.model.ConnectDTO;\nimport com.provectus.kafka.ui.model.ConnectorDTO;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.service.KafkaConnectService;\nimport java.net.URI;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.RequiredArgsConstructor;\nimport org.opendatadiscovery.client.model.DataEntity;\nimport org.opendatadiscovery.client.model.DataEntityList;\nimport org.opendatadiscovery.client.model.DataEntityType;\nimport org.opendatadiscovery.client.model.DataSource;\nimport org.opendatadiscovery.client.model.DataTransformer;\nimport org.opendatadiscovery.client.model.MetadataExtension;\nimport reactor.core.publisher.Flux;\n\n@RequiredArgsConstructor\nclass ConnectorsExporter {\n\n  private final KafkaConnectService kafkaConnectService;\n\n  Flux<DataEntityList> export(KafkaCluster cluster) {\n    return kafkaConnectService.getConnects(cluster)\n        .flatMap(connect -> kafkaConnectService.getConnectorNamesWithErrorsSuppress(cluster, connect.getName())\n            .flatMap(connectorName -> kafkaConnectService.getConnector(cluster, connect.getName(), connectorName))\n            .flatMap(connectorDTO ->\n                kafkaConnectService.getConnectorTopics(cluster, connect.getName(), connectorDTO.getName())\n                    .map(topics -> createConnectorDataEntity(cluster, connect, connectorDTO, topics)))\n            .buffer(100)\n            .map(connectDataEntities -> {\n              String dsOddrn = Oddrn.connectDataSourceOddrn(connect.getAddress());\n              return new DataEntityList()\n                  .dataSourceOddrn(dsOddrn)\n                  .items(connectDataEntities);\n            })\n        );\n  }\n\n  Flux<DataSource> getConnectDataSources(KafkaCluster cluster) {\n    return kafkaConnectService.getConnects(cluster)\n        .map(ConnectorsExporter::toDataSource);\n  }\n\n  private static DataSource toDataSource(ConnectDTO connect) {\n    return new DataSource()\n        .oddrn(Oddrn.connectDataSourceOddrn(connect.getAddress()))\n        .name(connect.getName())\n        .description(\"Kafka Connect\");\n  }\n\n  private static DataEntity createConnectorDataEntity(KafkaCluster cluster,\n                                                      ConnectDTO connect,\n                                                      ConnectorDTO connector,\n                                                      ConnectorTopics connectorTopics) {\n    var metadata = new HashMap<>(extractMetadata(connector));\n    metadata.put(\"type\", connector.getType().name());\n\n    var info = extractConnectorInfo(cluster, connector, connectorTopics);\n    DataTransformer transformer = new DataTransformer();\n    transformer.setInputs(info.inputs());\n    transformer.setOutputs(info.outputs());\n\n    return new DataEntity()\n        .oddrn(Oddrn.connectorOddrn(connect.getAddress(), connector.getName()))\n        .name(connector.getName())\n        .description(\"Kafka Connector \\\"%s\\\" (%s)\".formatted(connector.getName(), connector.getType()))\n        .type(DataEntityType.JOB)\n        .dataTransformer(transformer)\n        .metadata(List.of(\n            new MetadataExtension()\n                .schemaUrl(URI.create(\"wontbeused.oops\"))\n                .metadata(metadata)));\n  }\n\n  private static Map<String, Object> extractMetadata(ConnectorDTO connector) {\n    // will be sanitized by KafkaConfigSanitizer (if it's enabled)\n    return connector.getConfig();\n  }\n\n  private static ConnectorInfo extractConnectorInfo(KafkaCluster cluster,\n                                                    ConnectorDTO connector,\n                                                    ConnectorTopics topics) {\n    return ConnectorInfo.extract(\n        (String) connector.getConfig().get(\"connector.class\"),\n        connector.getType(),\n        connector.getConfig(),\n        topics.getTopics(),\n        topic -> Oddrn.topicOddrn(cluster, topic)\n    );\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddExporter.java",
    "content": "package com.provectus.kafka.ui.service.integration.odd;\n\nimport com.google.common.annotations.VisibleForTesting;\nimport com.google.common.base.Preconditions;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.service.KafkaConnectService;\nimport com.provectus.kafka.ui.service.StatisticsCache;\nimport java.util.function.Predicate;\nimport java.util.regex.Pattern;\nimport org.opendatadiscovery.client.ApiClient;\nimport org.opendatadiscovery.client.api.OpenDataDiscoveryIngestionApi;\nimport org.opendatadiscovery.client.model.DataEntityList;\nimport org.opendatadiscovery.client.model.DataSource;\nimport org.opendatadiscovery.client.model.DataSourceList;\nimport org.springframework.http.HttpHeaders;\nimport reactor.core.publisher.Mono;\n\nclass OddExporter {\n\n  private final OpenDataDiscoveryIngestionApi oddApi;\n  private final TopicsExporter topicsExporter;\n  private final ConnectorsExporter connectorsExporter;\n\n  public OddExporter(StatisticsCache statisticsCache,\n                     KafkaConnectService connectService,\n                     OddIntegrationProperties oddIntegrationProperties) {\n    this(\n        createApiClient(oddIntegrationProperties),\n        new TopicsExporter(createTopicsFilter(oddIntegrationProperties), statisticsCache),\n        new ConnectorsExporter(connectService)\n    );\n  }\n\n  @VisibleForTesting\n  OddExporter(OpenDataDiscoveryIngestionApi oddApi,\n              TopicsExporter topicsExporter,\n              ConnectorsExporter connectorsExporter) {\n    this.oddApi = oddApi;\n    this.topicsExporter = topicsExporter;\n    this.connectorsExporter = connectorsExporter;\n  }\n\n  private static Predicate<String> createTopicsFilter(OddIntegrationProperties properties) {\n    if (properties.getTopicsRegex() == null) {\n      return topic -> !topic.startsWith(\"_\");\n    }\n    Pattern pattern = Pattern.compile(properties.getTopicsRegex());\n    return topic -> pattern.matcher(topic).matches();\n  }\n\n  private static OpenDataDiscoveryIngestionApi createApiClient(OddIntegrationProperties properties) {\n    Preconditions.checkNotNull(properties.getUrl(), \"ODD url not set\");\n    Preconditions.checkNotNull(properties.getToken(), \"ODD token not set\");\n    var apiClient = new ApiClient()\n        .setBasePath(properties.getUrl())\n        .addDefaultHeader(HttpHeaders.AUTHORIZATION, \"Bearer \" + properties.getToken());\n    return new OpenDataDiscoveryIngestionApi(apiClient);\n  }\n\n  public Mono<Void> export(KafkaCluster cluster) {\n    return exportTopics(cluster)\n        .then(exportKafkaConnects(cluster));\n  }\n\n  private Mono<Void> exportTopics(KafkaCluster c) {\n    return createKafkaDataSource(c)\n        .thenMany(topicsExporter.export(c))\n        .concatMap(this::sendDataEntities)\n        .then();\n  }\n\n  private Mono<Void> exportKafkaConnects(KafkaCluster cluster) {\n    return createConnectDataSources(cluster)\n        .thenMany(connectorsExporter.export(cluster))\n        .concatMap(this::sendDataEntities)\n        .then();\n  }\n\n  private Mono<Void> createConnectDataSources(KafkaCluster cluster) {\n    return connectorsExporter.getConnectDataSources(cluster)\n        .buffer(100)\n        .concatMap(dataSources -> oddApi.createDataSource(new DataSourceList().items(dataSources)))\n        .then();\n  }\n\n  private Mono<Void> createKafkaDataSource(KafkaCluster cluster) {\n    String clusterOddrn = Oddrn.clusterOddrn(cluster);\n    return oddApi.createDataSource(\n        new DataSourceList()\n            .addItemsItem(\n                new DataSource()\n                    .oddrn(clusterOddrn)\n                    .name(cluster.getName())\n                    .description(\"Kafka cluster\")\n            )\n    );\n  }\n\n  private Mono<Void> sendDataEntities(DataEntityList dataEntityList) {\n    return oddApi.postDataEntityList(dataEntityList);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddExporterScheduler.java",
    "content": "package com.provectus.kafka.ui.service.integration.odd;\n\nimport com.provectus.kafka.ui.service.ClustersStorage;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport reactor.core.publisher.Flux;\nimport reactor.core.scheduler.Schedulers;\n\n@RequiredArgsConstructor\nclass OddExporterScheduler {\n\n  private final ClustersStorage clustersStorage;\n  private final OddExporter oddExporter;\n\n  @Scheduled(fixedRateString = \"${kafka.send-stats-to-odd-millis:30000}\")\n  public void sendMetricsToOdd() {\n    Flux.fromIterable(clustersStorage.getKafkaClusters())\n        .parallel()\n        .runOn(Schedulers.parallel())\n        .flatMap(oddExporter::export)\n        .then()\n        .block();\n  }\n\n\n}\n\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddIntegrationConfig.java",
    "content": "package com.provectus.kafka.ui.service.integration.odd;\n\nimport com.provectus.kafka.ui.service.ClustersStorage;\nimport com.provectus.kafka.ui.service.KafkaConnectService;\nimport com.provectus.kafka.ui.service.StatisticsCache;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n@Configuration\n@ConditionalOnProperty(value = \"integration.odd.url\")\nclass OddIntegrationConfig {\n\n  @Bean\n  OddIntegrationProperties oddIntegrationProperties() {\n    return new OddIntegrationProperties();\n  }\n\n  @Bean\n  OddExporter oddExporter(StatisticsCache statisticsCache,\n                          KafkaConnectService connectService,\n                          OddIntegrationProperties oddIntegrationProperties) {\n    return new OddExporter(statisticsCache, connectService, oddIntegrationProperties);\n  }\n\n  @Bean\n  OddExporterScheduler oddExporterScheduler(ClustersStorage storage, OddExporter exporter) {\n    return new OddExporterScheduler(storage, exporter);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddIntegrationProperties.java",
    "content": "package com.provectus.kafka.ui.service.integration.odd;\n\nimport lombok.Data;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\n\n\n@Data\n@ConfigurationProperties(\"integration.odd\")\npublic class OddIntegrationProperties {\n\n  String url;\n  String token;\n  String topicsRegex;\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/Oddrn.java",
    "content": "package com.provectus.kafka.ui.service.integration.odd;\n\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport java.net.URI;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport org.opendatadiscovery.oddrn.model.AwsS3Path;\nimport org.opendatadiscovery.oddrn.model.KafkaConnectorPath;\nimport org.opendatadiscovery.oddrn.model.KafkaPath;\n\npublic final class Oddrn {\n\n  private Oddrn() {\n  }\n\n  static String clusterOddrn(KafkaCluster cluster) {\n    return KafkaPath.builder()\n        .cluster(bootstrapServersForOddrn(cluster.getBootstrapServers()))\n        .build()\n        .oddrn();\n  }\n\n  static KafkaPath topicOddrnPath(KafkaCluster cluster, String topic) {\n    return KafkaPath.builder()\n        .cluster(bootstrapServersForOddrn(cluster.getBootstrapServers()))\n        .topic(topic)\n        .build();\n  }\n\n  static String topicOddrn(KafkaCluster cluster, String topic) {\n    return topicOddrnPath(cluster, topic).oddrn();\n  }\n\n  static String awsS3Oddrn(String bucket, String key) {\n    return AwsS3Path.builder()\n        .bucket(bucket)\n        .key(key)\n        .build()\n        .oddrn();\n  }\n\n  static String connectDataSourceOddrn(String connectUrl) {\n    return KafkaConnectorPath.builder()\n        .host(normalizedConnectHosts(connectUrl))\n        .build()\n        .oddrn();\n  }\n\n  private static String normalizedConnectHosts(String connectUrlStr) {\n    return Stream.of(connectUrlStr.split(\",\"))\n        .map(String::trim)\n        .sorted()\n        .map(url -> {\n          var uri = URI.create(url);\n          String host = uri.getHost();\n          String portSuffix = (uri.getPort() > 0 ? (\":\" + uri.getPort()) : \"\");\n          return host + portSuffix;\n        })\n        .collect(Collectors.joining(\",\"));\n  }\n\n  static String connectorOddrn(String connectUrl, String connectorName) {\n    return KafkaConnectorPath.builder()\n        .host(normalizedConnectHosts(connectUrl))\n        .connector(connectorName)\n        .build()\n        .oddrn();\n  }\n\n  private static String bootstrapServersForOddrn(String bootstrapServers) {\n    return Stream.of(bootstrapServers.split(\",\"))\n        .map(String::trim)\n        .sorted()\n        .collect(Collectors.joining(\",\"));\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/SchemaReferencesResolver.java",
    "content": "package com.provectus.kafka.ui.service.integration.odd;\n\nimport com.google.common.collect.ImmutableMap;\nimport com.google.common.collect.ImmutableSet;\nimport com.provectus.kafka.ui.sr.api.KafkaSrClientApi;\nimport com.provectus.kafka.ui.sr.model.SchemaReference;\nimport java.util.List;\nimport java.util.Optional;\nimport javax.annotation.Nullable;\nimport reactor.core.publisher.Mono;\n\n// logic copied from AbstractSchemaProvider:resolveReferences\n// https://github.com/confluentinc/schema-registry/blob/fd59613e2c5adf62e36705307f420712e4c8c1ea/client/src/main/java/io/confluent/kafka/schemaregistry/AbstractSchemaProvider.java#L54\nclass SchemaReferencesResolver {\n\n  private final KafkaSrClientApi client;\n\n  SchemaReferencesResolver(KafkaSrClientApi client) {\n    this.client = client;\n  }\n\n  Mono<ImmutableMap<String, String>> resolve(List<SchemaReference> refs) {\n    return resolveReferences(refs, new Resolving(ImmutableMap.of(), ImmutableSet.of()))\n        .map(Resolving::resolved);\n  }\n\n  private record Resolving(ImmutableMap<String, String> resolved, ImmutableSet<String> visited) {\n\n    Resolving visit(String name) {\n      return new Resolving(resolved, ImmutableSet.<String>builder().addAll(visited).add(name).build());\n    }\n\n    Resolving resolve(String ref, String schema) {\n      return new Resolving(ImmutableMap.<String, String>builder().putAll(resolved).put(ref, schema).build(), visited);\n    }\n  }\n\n  private Mono<Resolving> resolveReferences(@Nullable List<SchemaReference> refs, Resolving initState) {\n    Mono<Resolving> result = Mono.just(initState);\n    for (SchemaReference reference : Optional.ofNullable(refs).orElse(List.of())) {\n      result = result.flatMap(state -> {\n        if (state.visited().contains(reference.getName())) {\n          return Mono.just(state);\n        } else {\n          final var newState = state.visit(reference.getName());\n          return client.getSubjectVersion(reference.getSubject(), String.valueOf(reference.getVersion()), true)\n              .flatMap(subj ->\n                  resolveReferences(subj.getReferences(), newState)\n                      .map(withNewRefs -> withNewRefs.resolve(reference.getName(), subj.getSchema())));\n        }\n      });\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/TopicsExporter.java",
    "content": "package com.provectus.kafka.ui.service.integration.odd;\n\nimport com.google.common.collect.ImmutableMap;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.Statistics;\nimport com.provectus.kafka.ui.service.StatisticsCache;\nimport com.provectus.kafka.ui.service.integration.odd.schema.DataSetFieldsExtractors;\nimport com.provectus.kafka.ui.sr.model.SchemaSubject;\nimport java.net.URI;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.kafka.clients.admin.ConfigEntry;\nimport org.apache.kafka.clients.admin.TopicDescription;\nimport org.opendatadiscovery.client.model.DataEntity;\nimport org.opendatadiscovery.client.model.DataEntityList;\nimport org.opendatadiscovery.client.model.DataEntityType;\nimport org.opendatadiscovery.client.model.DataSet;\nimport org.opendatadiscovery.client.model.DataSetField;\nimport org.opendatadiscovery.client.model.MetadataExtension;\nimport org.opendatadiscovery.oddrn.model.KafkaPath;\nimport org.springframework.web.reactive.function.client.WebClientResponseException;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.util.function.Tuple2;\nimport reactor.util.function.Tuples;\n\n@Slf4j\n@RequiredArgsConstructor\nclass TopicsExporter {\n\n  private final Predicate<String> topicFilter;\n  private final StatisticsCache statisticsCache;\n\n  Flux<DataEntityList> export(KafkaCluster cluster) {\n    String clusterOddrn = Oddrn.clusterOddrn(cluster);\n    Statistics stats = statisticsCache.get(cluster);\n    return Flux.fromIterable(stats.getTopicDescriptions().keySet())\n        .filter(topicFilter)\n        .flatMap(topic -> createTopicDataEntity(cluster, topic, stats))\n        .onErrorContinue(\n            (th, topic) -> log.warn(\"Error exporting data for topic {}, cluster {}\", topic, cluster.getName(), th))\n        .buffer(100)\n        .map(topicsEntities ->\n            new DataEntityList()\n                .dataSourceOddrn(clusterOddrn)\n                .items(topicsEntities));\n  }\n\n  private Mono<DataEntity> createTopicDataEntity(KafkaCluster cluster, String topic, Statistics stats) {\n    KafkaPath topicOddrnPath = Oddrn.topicOddrnPath(cluster, topic);\n    return\n        Mono.zip(\n                getTopicSchema(cluster, topic, topicOddrnPath, true),\n                getTopicSchema(cluster, topic, topicOddrnPath, false)\n            )\n            .map(keyValueFields -> {\n                  var dataset = new DataSet();\n                  keyValueFields.getT1().forEach(dataset::addFieldListItem);\n                  keyValueFields.getT2().forEach(dataset::addFieldListItem);\n                  return new DataEntity()\n                      .name(topic)\n                      .description(\"Kafka topic \\\"%s\\\"\".formatted(topic))\n                      .oddrn(Oddrn.topicOddrn(cluster, topic))\n                      .type(DataEntityType.KAFKA_TOPIC)\n                      .dataset(dataset)\n                      .addMetadataItem(\n                          new MetadataExtension()\n                              .schemaUrl(URI.create(\"wontbeused.oops\"))\n                              .metadata(getTopicMetadata(topic, stats)));\n                }\n            );\n  }\n\n  private Map<String, Object> getNonDefaultConfigs(String topic, Statistics stats) {\n    List<ConfigEntry> config = stats.getTopicConfigs().get(topic);\n    if (config == null) {\n      return Map.of();\n    }\n    return config.stream()\n        .filter(c -> c.source() == ConfigEntry.ConfigSource.DYNAMIC_TOPIC_CONFIG)\n        .collect(Collectors.toMap(ConfigEntry::name, ConfigEntry::value));\n  }\n\n  private Map<String, Object> getTopicMetadata(String topic, Statistics stats) {\n    TopicDescription topicDescription = stats.getTopicDescriptions().get(topic);\n    return ImmutableMap.<String, Object>builder()\n        .put(\"partitions\", topicDescription.partitions().size())\n        .put(\"replication_factor\", topicDescription.partitions().get(0).replicas().size())\n        .putAll(getNonDefaultConfigs(topic, stats))\n        .build();\n  }\n\n  //returns empty list if schemaRegistry is not configured or assumed subject not found\n  private Mono<List<DataSetField>> getTopicSchema(KafkaCluster cluster,\n                                                  String topic,\n                                                  KafkaPath topicOddrn,\n                                                  boolean isKey) {\n    if (cluster.getSchemaRegistryClient() == null) {\n      return Mono.just(List.of());\n    }\n    String subject = topic + (isKey ? \"-key\" : \"-value\");\n    return getSubjWithResolvedRefs(cluster, subject)\n        .map(t -> DataSetFieldsExtractors.extract(t.getT1(), t.getT2(), topicOddrn, isKey))\n        .onErrorResume(WebClientResponseException.NotFound.class, th -> Mono.just(List.of()))\n        .onErrorMap(WebClientResponseException.class, err ->\n            new IllegalStateException(\"Error retrieving subject %s\".formatted(subject), err));\n  }\n\n  private Mono<Tuple2<SchemaSubject, Map<String, String>>> getSubjWithResolvedRefs(KafkaCluster cluster,\n                                                                                   String subjectName) {\n    return cluster.getSchemaRegistryClient()\n        .mono(client ->\n            client.getSubjectVersion(subjectName, \"latest\", false)\n                .flatMap(subj -> new SchemaReferencesResolver(client).resolve(subj.getReferences())\n                    .map(resolvedRefs -> Tuples.of(subj, resolvedRefs))));\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/AvroExtractor.java",
    "content": "package com.provectus.kafka.ui.service.integration.odd.schema;\n\nimport com.google.common.collect.ImmutableSet;\nimport io.confluent.kafka.schemaregistry.avro.AvroSchema;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.apache.avro.Schema;\nimport org.opendatadiscovery.client.model.DataSetField;\nimport org.opendatadiscovery.client.model.DataSetFieldType;\nimport org.opendatadiscovery.oddrn.model.KafkaPath;\n\nfinal class AvroExtractor {\n\n  private AvroExtractor() {\n  }\n\n  static List<DataSetField> extract(AvroSchema avroSchema, KafkaPath topicOddrn, boolean isKey) {\n    var schema = avroSchema.rawSchema();\n    List<DataSetField> result = new ArrayList<>();\n    result.add(DataSetFieldsExtractors.rootField(topicOddrn, isKey));\n    extract(\n        schema,\n        topicOddrn.oddrn() + \"/columns/\" + (isKey ? \"key\" : \"value\"),\n        null,\n        null,\n        null,\n        false,\n        ImmutableSet.of(),\n        result\n    );\n    return result;\n  }\n\n  private static void extract(Schema schema,\n                              String parentOddr,\n                              String oddrn, //null for root\n                              String name,\n                              String doc,\n                              Boolean nullable,\n                              ImmutableSet<String> registeredRecords,\n                              List<DataSetField> sink\n  ) {\n    switch (schema.getType()) {\n      case RECORD -> extractRecord(schema, parentOddr, oddrn, name, doc, nullable, registeredRecords, sink);\n      case UNION -> extractUnion(schema, parentOddr, oddrn, name, doc, registeredRecords, sink);\n      case ARRAY -> extractArray(schema, parentOddr, oddrn, name, doc, nullable, registeredRecords, sink);\n      case MAP -> extractMap(schema, parentOddr, oddrn, name, doc, nullable, registeredRecords, sink);\n      default -> extractPrimitive(schema, parentOddr, oddrn, name, doc, nullable, sink);\n    }\n  }\n\n  private static DataSetField createDataSetField(String name,\n                                                 String doc,\n                                                 String parentOddrn,\n                                                 String oddrn,\n                                                 Schema schema,\n                                                 Boolean nullable) {\n    return new DataSetField()\n        .name(name)\n        .description(doc)\n        .parentFieldOddrn(parentOddrn)\n        .oddrn(oddrn)\n        .type(mapSchema(schema, nullable));\n  }\n\n  private static void extractRecord(Schema schema,\n                                    String parentOddr,\n                                    String oddrn, //null for root\n                                    String name,\n                                    String doc,\n                                    Boolean nullable,\n                                    ImmutableSet<String> registeredRecords,\n                                    List<DataSetField> sink) {\n    boolean isRoot = oddrn == null;\n    if (!isRoot) {\n      sink.add(createDataSetField(name, doc, parentOddr, oddrn, schema, nullable));\n      if (registeredRecords.contains(schema.getFullName())) {\n        // avoiding recursion by checking if record already registered in parsing chain\n        return;\n      }\n    }\n    var newRegisteredRecords = ImmutableSet.<String>builder()\n        .addAll(registeredRecords)\n        .add(schema.getFullName())\n        .build();\n\n    schema.getFields().forEach(f ->\n        extract(\n            f.schema(),\n            isRoot ? parentOddr : oddrn,\n            isRoot\n                ? parentOddr + \"/\" + f.name()\n                : oddrn + \"/fields/\" + f.name(),\n            f.name(),\n            f.doc(),\n            false,\n            newRegisteredRecords,\n            sink\n        ));\n  }\n\n  private static void extractUnion(Schema schema,\n                                   String parentOddr,\n                                   String oddrn, //null for root\n                                   String name,\n                                   String doc,\n                                   ImmutableSet<String> registeredRecords,\n                                   List<DataSetField> sink) {\n    boolean isRoot = oddrn == null;\n    boolean containsNull = schema.getTypes().stream().map(Schema::getType).anyMatch(t -> t == Schema.Type.NULL);\n    // if it is not root and there is only 2 values for union (null and smth else)\n    // we registering this field as optional without mentioning union\n    if (!isRoot && containsNull && schema.getTypes().size() == 2) {\n      var nonNullSchema = schema.getTypes().stream()\n          .filter(s -> s.getType() != Schema.Type.NULL)\n          .findFirst()\n          .orElseThrow(IllegalStateException::new);\n      extract(\n          nonNullSchema,\n          parentOddr,\n          oddrn,\n          name,\n          doc,\n          true,\n          registeredRecords,\n          sink\n      );\n      return;\n    }\n    oddrn = isRoot ? parentOddr + \"/union\" : oddrn;\n    if (isRoot) {\n      sink.add(createDataSetField(\"Avro root union\", doc, parentOddr, oddrn, schema, containsNull));\n    } else {\n      sink.add(createDataSetField(name, doc, parentOddr, oddrn, schema, containsNull));\n    }\n    for (Schema t : schema.getTypes()) {\n      if (t.getType() != Schema.Type.NULL) {\n        extract(\n            t,\n            oddrn,\n            oddrn + \"/values/\" + t.getName(),\n            t.getName(),\n            t.getDoc(),\n            containsNull,\n            registeredRecords,\n            sink\n        );\n      }\n    }\n  }\n\n  private static void extractArray(Schema schema,\n                                   String parentOddr,\n                                   String oddrn, //null for root\n                                   String name,\n                                   String doc,\n                                   Boolean nullable,\n                                   ImmutableSet<String> registeredRecords,\n                                   List<DataSetField> sink) {\n    boolean isRoot = oddrn == null;\n    oddrn = isRoot ? parentOddr + \"/array\" : oddrn;\n    if (isRoot) {\n      sink.add(createDataSetField(\"Avro root Array\", doc, parentOddr, oddrn, schema, nullable));\n    } else {\n      sink.add(createDataSetField(name, doc, parentOddr, oddrn, schema, nullable));\n    }\n    extract(\n        schema.getElementType(),\n        oddrn,\n        oddrn + \"/items/\" + schema.getElementType().getName(),\n        schema.getElementType().getName(),\n        schema.getElementType().getDoc(),\n        false,\n        registeredRecords,\n        sink\n    );\n  }\n\n  private static void extractMap(Schema schema,\n                                 String parentOddr,\n                                 String oddrn, //null for root\n                                 String name,\n                                 String doc,\n                                 Boolean nullable,\n                                 ImmutableSet<String> registeredRecords,\n                                 List<DataSetField> sink) {\n    boolean isRoot = oddrn == null;\n    oddrn = isRoot ? parentOddr + \"/map\" : oddrn;\n    if (isRoot) {\n      sink.add(createDataSetField(\"Avro root map\", doc, parentOddr, oddrn, schema, nullable));\n    } else {\n      sink.add(createDataSetField(name, doc, parentOddr, oddrn, schema, nullable));\n    }\n    extract(\n        new Schema.Parser().parse(\"\\\"string\\\"\"),\n        oddrn,\n        oddrn + \"/key\",\n        \"key\",\n        null,\n        nullable,\n        registeredRecords,\n        sink\n    );\n    extract(\n        schema.getValueType(),\n        oddrn,\n        oddrn + \"/value\",\n        \"value\",\n        null,\n        nullable,\n        registeredRecords,\n        sink\n    );\n  }\n\n\n  private static void extractPrimitive(Schema schema,\n                                       String parentOddr,\n                                       String oddrn, //null for root\n                                       String name,\n                                       String doc,\n                                       Boolean nullable,\n                                       List<DataSetField> sink) {\n    boolean isRoot = oddrn == null;\n    String primOddrn = isRoot ? (parentOddr + \"/\" + schema.getType()) : oddrn;\n    if (isRoot) {\n      sink.add(createDataSetField(\"Root avro \" + schema.getType(),\n          doc, parentOddr, primOddrn, schema, nullable));\n    } else {\n      sink.add(createDataSetField(name, doc, parentOddr, primOddrn, schema, nullable));\n    }\n  }\n\n  private static DataSetFieldType.TypeEnum mapType(Schema.Type type) {\n    return switch (type) {\n      case INT, LONG -> DataSetFieldType.TypeEnum.INTEGER;\n      case FLOAT, DOUBLE, FIXED -> DataSetFieldType.TypeEnum.NUMBER;\n      case STRING, ENUM -> DataSetFieldType.TypeEnum.STRING;\n      case BOOLEAN -> DataSetFieldType.TypeEnum.BOOLEAN;\n      case BYTES -> DataSetFieldType.TypeEnum.BINARY;\n      case ARRAY -> DataSetFieldType.TypeEnum.LIST;\n      case RECORD -> DataSetFieldType.TypeEnum.STRUCT;\n      case MAP -> DataSetFieldType.TypeEnum.MAP;\n      case UNION -> DataSetFieldType.TypeEnum.UNION;\n      case NULL -> DataSetFieldType.TypeEnum.UNKNOWN;\n    };\n  }\n\n  private static DataSetFieldType mapSchema(Schema schema, Boolean nullable) {\n    return new DataSetFieldType()\n        .logicalType(logicalType(schema))\n        .isNullable(nullable)\n        .type(mapType(schema.getType()));\n  }\n\n  private static String logicalType(Schema schema) {\n    return schema.getType() == Schema.Type.RECORD\n        ? schema.getFullName()\n        : schema.getType().toString().toLowerCase();\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/DataSetFieldsExtractors.java",
    "content": "package com.provectus.kafka.ui.service.integration.odd.schema;\n\nimport com.provectus.kafka.ui.sr.model.SchemaSubject;\nimport com.provectus.kafka.ui.sr.model.SchemaType;\nimport io.confluent.kafka.schemaregistry.avro.AvroSchema;\nimport io.confluent.kafka.schemaregistry.json.JsonSchema;\nimport io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.opendatadiscovery.client.model.DataSetField;\nimport org.opendatadiscovery.client.model.DataSetFieldType;\nimport org.opendatadiscovery.oddrn.model.KafkaPath;\n\npublic final class DataSetFieldsExtractors {\n\n  public static List<DataSetField> extract(SchemaSubject subject,\n                                           Map<String, String> resolvedRefs,\n                                           KafkaPath topicOddrn,\n                                           boolean isKey) {\n    SchemaType schemaType = Optional.ofNullable(subject.getSchemaType()).orElse(SchemaType.AVRO);\n    return switch (schemaType) {\n      case AVRO -> AvroExtractor.extract(\n          new AvroSchema(subject.getSchema(), List.of(), resolvedRefs, null), topicOddrn, isKey);\n      case JSON -> JsonSchemaExtractor.extract(\n          new JsonSchema(subject.getSchema(), List.of(), resolvedRefs, null), topicOddrn, isKey);\n      case PROTOBUF -> ProtoExtractor.extract(\n          new ProtobufSchema(subject.getSchema(), List.of(), resolvedRefs, null, null), topicOddrn, isKey);\n    };\n  }\n\n\n  static DataSetField rootField(KafkaPath topicOddrn, boolean isKey) {\n    var rootOddrn = topicOddrn.oddrn() + \"/columns/\" + (isKey ? \"key\" : \"value\");\n    return new DataSetField()\n        .name(isKey ? \"key\" : \"value\")\n        .description(\"Topic's \" + (isKey ? \"key\" : \"value\") + \" schema\")\n        .parentFieldOddrn(topicOddrn.oddrn())\n        .oddrn(rootOddrn)\n        .type(new DataSetFieldType()\n            .type(DataSetFieldType.TypeEnum.STRUCT)\n            .isNullable(true));\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/JsonSchemaExtractor.java",
    "content": "package com.provectus.kafka.ui.service.integration.odd.schema;\n\nimport com.google.common.collect.ImmutableSet;\nimport com.provectus.kafka.ui.sr.model.SchemaSubject;\nimport io.confluent.kafka.schemaregistry.json.JsonSchema;\nimport java.net.URI;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport javax.annotation.Nullable;\nimport org.everit.json.schema.ArraySchema;\nimport org.everit.json.schema.BooleanSchema;\nimport org.everit.json.schema.CombinedSchema;\nimport org.everit.json.schema.FalseSchema;\nimport org.everit.json.schema.NullSchema;\nimport org.everit.json.schema.NumberSchema;\nimport org.everit.json.schema.ObjectSchema;\nimport org.everit.json.schema.ReferenceSchema;\nimport org.everit.json.schema.Schema;\nimport org.everit.json.schema.StringSchema;\nimport org.everit.json.schema.TrueSchema;\nimport org.opendatadiscovery.client.model.DataSetField;\nimport org.opendatadiscovery.client.model.DataSetFieldType;\nimport org.opendatadiscovery.client.model.MetadataExtension;\nimport org.opendatadiscovery.oddrn.model.KafkaPath;\n\nfinal class JsonSchemaExtractor {\n\n  private JsonSchemaExtractor() {\n  }\n\n  static List<DataSetField> extract(JsonSchema jsonSchema, KafkaPath topicOddrn, boolean isKey) {\n    Schema schema = jsonSchema.rawSchema();\n    List<DataSetField> result = new ArrayList<>();\n    result.add(DataSetFieldsExtractors.rootField(topicOddrn, isKey));\n    extract(\n        schema,\n        topicOddrn.oddrn() + \"/columns/\" + (isKey ? \"key\" : \"value\"),\n        null,\n        null,\n        null,\n        ImmutableSet.of(),\n        result\n    );\n    return result;\n  }\n\n  private static void extract(Schema schema,\n                              String parentOddr,\n                              String oddrn, //null for root\n                              String name,\n                              Boolean nullable,\n                              ImmutableSet<String> registeredRecords,\n                              List<DataSetField> sink) {\n    if (schema instanceof ReferenceSchema s) {\n      Optional.ofNullable(s.getReferredSchema())\n          .ifPresent(refSchema -> extract(refSchema, parentOddr, oddrn, name, nullable, registeredRecords, sink));\n    } else if (schema instanceof ObjectSchema s) {\n      extractObject(s, parentOddr, oddrn, name, nullable, registeredRecords, sink);\n    } else if (schema instanceof ArraySchema s) {\n      extractArray(s, parentOddr, oddrn, name, nullable, registeredRecords, sink);\n    } else if (schema instanceof CombinedSchema cs) {\n      extractCombined(cs, parentOddr, oddrn, name, nullable, registeredRecords, sink);\n    } else if (schema instanceof BooleanSchema\n        || schema instanceof NumberSchema\n        || schema instanceof StringSchema\n        || schema instanceof NullSchema\n    ) {\n      extractPrimitive(schema, parentOddr, oddrn, name, nullable, sink);\n    } else {\n      extractUnknown(schema, parentOddr, oddrn, name, nullable, sink);\n    }\n  }\n\n  private static void extractPrimitive(Schema schema,\n                                       String parentOddr,\n                                       String oddrn, //null for root\n                                       String name,\n                                       Boolean nullable,\n                                       List<DataSetField> sink) {\n    boolean isRoot = oddrn == null;\n    sink.add(\n        createDataSetField(\n            schema,\n            isRoot ? \"Root JSON primitive\" : name,\n            parentOddr,\n            isRoot ? (parentOddr + \"/\" + logicalTypeName(schema)) : oddrn,\n            mapType(schema),\n            logicalTypeName(schema),\n            nullable\n        )\n    );\n  }\n\n  private static void extractUnknown(Schema schema,\n                                     String parentOddr,\n                                     String oddrn, //null for root\n                                     String name,\n                                     Boolean nullable,\n                                     List<DataSetField> sink) {\n    boolean isRoot = oddrn == null;\n    sink.add(\n        createDataSetField(\n            schema,\n            isRoot ? \"Root type \" + logicalTypeName(schema) : name,\n            parentOddr,\n            isRoot ? (parentOddr + \"/\" + logicalTypeName(schema)) : oddrn,\n            DataSetFieldType.TypeEnum.UNKNOWN,\n            logicalTypeName(schema),\n            nullable\n        )\n    );\n  }\n\n  private static void extractObject(ObjectSchema schema,\n                                    String parentOddr,\n                                    String oddrn, //null for root\n                                    String name,\n                                    Boolean nullable,\n                                    ImmutableSet<String> registeredRecords,\n                                    List<DataSetField> sink) {\n    boolean isRoot = oddrn == null;\n    // schemaLocation can be null for empty object schemas (like if it used in anyOf)\n    @Nullable var schemaLocation = schema.getSchemaLocation();\n    if (!isRoot) {\n      sink.add(createDataSetField(\n          schema,\n          name,\n          parentOddr,\n          oddrn,\n          DataSetFieldType.TypeEnum.STRUCT,\n          logicalTypeName(schema),\n          nullable\n      ));\n      if (schemaLocation != null && registeredRecords.contains(schemaLocation)) {\n        // avoiding recursion by checking if record already registered in parsing chain\n        return;\n      }\n    }\n\n    var newRegisteredRecords = schemaLocation == null\n        ? registeredRecords\n        : ImmutableSet.<String>builder()\n        .addAll(registeredRecords)\n        .add(schemaLocation)\n        .build();\n\n    schema.getPropertySchemas().forEach((propertyName, propertySchema) -> {\n      boolean required = schema.getRequiredProperties().contains(propertyName);\n      extract(\n          propertySchema,\n          isRoot ? parentOddr : oddrn,\n          isRoot\n              ? parentOddr + \"/\" + propertyName\n              : oddrn + \"/fields/\" + propertyName,\n          propertyName,\n          !required,\n          newRegisteredRecords,\n          sink\n      );\n    });\n  }\n\n  private static void extractArray(ArraySchema schema,\n                                   String parentOddr,\n                                   String oddrn, //null for root\n                                   String name,\n                                   Boolean nullable,\n                                   ImmutableSet<String> registeredRecords,\n                                   List<DataSetField> sink) {\n    boolean isRoot = oddrn == null;\n    oddrn = isRoot ? parentOddr + \"/array\" : oddrn;\n    if (isRoot) {\n      sink.add(\n          createDataSetField(\n              schema,\n              \"Json array root\",\n              parentOddr,\n              oddrn,\n              DataSetFieldType.TypeEnum.LIST,\n              \"array\",\n              nullable\n          ));\n    } else {\n      sink.add(\n          createDataSetField(\n              schema,\n              name,\n              parentOddr,\n              oddrn,\n              DataSetFieldType.TypeEnum.LIST,\n              \"array\",\n              nullable\n          ));\n    }\n    @Nullable var itemsSchema = schema.getAllItemSchema();\n    if (itemsSchema != null) {\n      extract(\n          itemsSchema,\n          oddrn,\n          oddrn + \"/items/\" + logicalTypeName(itemsSchema),\n          logicalTypeName(itemsSchema),\n          false,\n          registeredRecords,\n          sink\n      );\n    }\n  }\n\n  private static void extractCombined(CombinedSchema schema,\n                                      String parentOddr,\n                                      String oddrn, //null for root\n                                      String name,\n                                      Boolean nullable,\n                                      ImmutableSet<String> registeredRecords,\n                                      List<DataSetField> sink) {\n    String combineType = \"unknown\";\n    if (schema.getCriterion() == CombinedSchema.ALL_CRITERION) {\n      combineType = \"allOf\";\n    }\n    if (schema.getCriterion() == CombinedSchema.ANY_CRITERION) {\n      combineType = \"anyOf\";\n    }\n    if (schema.getCriterion() == CombinedSchema.ONE_CRITERION) {\n      combineType = \"oneOf\";\n    }\n\n    boolean isRoot = oddrn == null;\n    oddrn = isRoot ? (parentOddr + \"/\" + combineType) : (oddrn + \"/\" + combineType);\n    sink.add(\n        createDataSetField(\n            schema,\n            isRoot ? \"Root %s\".formatted(combineType) : name,\n            parentOddr,\n            oddrn,\n            DataSetFieldType.TypeEnum.UNION,\n            combineType,\n            nullable\n        ).addMetadataItem(new MetadataExtension()\n            .schemaUrl(URI.create(\"wontbeused.oops\"))\n            .metadata(Map.of(\"criterion\", combineType)))\n    );\n\n    for (Schema subschema : schema.getSubschemas()) {\n      extract(\n          subschema,\n          oddrn,\n          oddrn + \"/values/\" + logicalTypeName(subschema),\n          logicalTypeName(subschema),\n          nullable,\n          registeredRecords,\n          sink\n      );\n    }\n  }\n\n  private static String getDescription(Schema schema) {\n    return Optional.ofNullable(schema.getTitle())\n        .orElse(schema.getDescription());\n  }\n\n  private static String logicalTypeName(Schema schema) {\n    return schema.getClass()\n        .getSimpleName()\n        .replace(\"Schema\", \"\");\n  }\n\n  private static DataSetField createDataSetField(Schema schema,\n                                                 String name,\n                                                 String parentOddrn,\n                                                 String oddrn,\n                                                 DataSetFieldType.TypeEnum type,\n                                                 String logicalType,\n                                                 Boolean nullable) {\n    return new DataSetField()\n        .name(name)\n        .parentFieldOddrn(parentOddrn)\n        .oddrn(oddrn)\n        .description(getDescription(schema))\n        .type(\n            new DataSetFieldType()\n                .isNullable(nullable)\n                .logicalType(logicalType)\n                .type(type)\n        );\n  }\n\n  private static DataSetFieldType.TypeEnum mapType(Schema type) {\n    if (type instanceof NumberSchema) {\n      return DataSetFieldType.TypeEnum.NUMBER;\n    }\n    if (type instanceof StringSchema) {\n      return DataSetFieldType.TypeEnum.STRING;\n    }\n    if (type instanceof BooleanSchema || type instanceof TrueSchema || type instanceof FalseSchema) {\n      return DataSetFieldType.TypeEnum.BOOLEAN;\n    }\n    if (type instanceof ObjectSchema) {\n      return DataSetFieldType.TypeEnum.STRUCT;\n    }\n    if (type instanceof ReferenceSchema s) {\n      return mapType(s.getReferredSchema());\n    }\n    if (type instanceof CombinedSchema) {\n      return DataSetFieldType.TypeEnum.UNION;\n    }\n    return DataSetFieldType.TypeEnum.UNKNOWN;\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/schema/ProtoExtractor.java",
    "content": "package com.provectus.kafka.ui.service.integration.odd.schema;\n\nimport com.google.common.collect.ImmutableSet;\nimport com.google.protobuf.BoolValue;\nimport com.google.protobuf.BytesValue;\nimport com.google.protobuf.Descriptors;\nimport com.google.protobuf.Descriptors.Descriptor;\nimport com.google.protobuf.DoubleValue;\nimport com.google.protobuf.Duration;\nimport com.google.protobuf.FloatValue;\nimport com.google.protobuf.Int32Value;\nimport com.google.protobuf.Int64Value;\nimport com.google.protobuf.StringValue;\nimport com.google.protobuf.Timestamp;\nimport com.google.protobuf.UInt32Value;\nimport com.google.protobuf.UInt64Value;\nimport com.google.protobuf.Value;\nimport io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Set;\nimport org.opendatadiscovery.client.model.DataSetField;\nimport org.opendatadiscovery.client.model.DataSetFieldType;\nimport org.opendatadiscovery.client.model.DataSetFieldType.TypeEnum;\nimport org.opendatadiscovery.oddrn.model.KafkaPath;\n\nfinal class ProtoExtractor {\n\n  private static final Set<String> PRIMITIVES_WRAPPER_TYPE_NAMES = Set.of(\n      BoolValue.getDescriptor().getFullName(),\n      Int32Value.getDescriptor().getFullName(),\n      UInt32Value.getDescriptor().getFullName(),\n      Int64Value.getDescriptor().getFullName(),\n      UInt64Value.getDescriptor().getFullName(),\n      StringValue.getDescriptor().getFullName(),\n      BytesValue.getDescriptor().getFullName(),\n      FloatValue.getDescriptor().getFullName(),\n      DoubleValue.getDescriptor().getFullName()\n  );\n\n  private ProtoExtractor() {\n  }\n\n  static List<DataSetField> extract(ProtobufSchema protobufSchema, KafkaPath topicOddrn, boolean isKey) {\n    Descriptor schema = protobufSchema.toDescriptor();\n    List<DataSetField> result = new ArrayList<>();\n    result.add(DataSetFieldsExtractors.rootField(topicOddrn, isKey));\n    var rootOddrn = topicOddrn.oddrn() + \"/columns/\" + (isKey ? \"key\" : \"value\");\n    schema.getFields().forEach(f ->\n        extract(f,\n            rootOddrn,\n            rootOddrn + \"/\" + f.getName(),\n            f.getName(),\n            !f.isRequired(),\n            f.isRepeated(),\n            ImmutableSet.of(schema.getFullName()),\n            result\n        ));\n    return result;\n  }\n\n  private static void extract(Descriptors.FieldDescriptor field,\n                              String parentOddr,\n                              String oddrn, //null for root\n                              String name,\n                              boolean nullable,\n                              boolean repeated,\n                              ImmutableSet<String> registeredRecords,\n                              List<DataSetField> sink) {\n    if (repeated) {\n      extractRepeated(field, parentOddr, oddrn, name, nullable, registeredRecords, sink);\n    } else if (field.getType() == Descriptors.FieldDescriptor.Type.MESSAGE) {\n      extractMessage(field, parentOddr, oddrn, name, nullable, registeredRecords, sink);\n    } else {\n      extractPrimitive(field, parentOddr, oddrn, name, nullable, sink);\n    }\n  }\n\n  // converts some(!) Protobuf Well-known type (from google.protobuf.* packages)\n  // see JsonFormat::buildWellKnownTypePrinters for impl details\n  private static boolean extractProtoWellKnownType(Descriptors.FieldDescriptor field,\n                                                   String parentOddr,\n                                                   String oddrn, //null for root\n                                                   String name,\n                                                   boolean nullable,\n                                                   List<DataSetField> sink) {\n    // all well-known types are messages\n    if (field.getType() != Descriptors.FieldDescriptor.Type.MESSAGE) {\n      return false;\n    }\n    String typeName = field.getMessageType().getFullName();\n    if (typeName.equals(Timestamp.getDescriptor().getFullName())) {\n      sink.add(createDataSetField(name, parentOddr, oddrn, TypeEnum.DATETIME, typeName, nullable));\n      return true;\n    }\n    if (typeName.equals(Duration.getDescriptor().getFullName())) {\n      sink.add(createDataSetField(name, parentOddr, oddrn, TypeEnum.DURATION, typeName, nullable));\n      return true;\n    }\n    if (typeName.equals(Value.getDescriptor().getFullName())) {\n      //TODO: use ANY type when it will appear in ODD\n      sink.add(createDataSetField(name, parentOddr, oddrn, TypeEnum.UNKNOWN, typeName, nullable));\n      return true;\n    }\n    if (PRIMITIVES_WRAPPER_TYPE_NAMES.contains(typeName)) {\n      var wrapped = field.getMessageType().findFieldByName(\"value\");\n      sink.add(createDataSetField(name, parentOddr, oddrn, mapType(wrapped.getType()), typeName, true));\n      return true;\n    }\n    return false;\n  }\n\n  private static void extractRepeated(Descriptors.FieldDescriptor field,\n                                      String parentOddr,\n                                      String oddrn, //null for root\n                                      String name,\n                                      boolean nullable,\n                                      ImmutableSet<String> registeredRecords,\n                                      List<DataSetField> sink) {\n    sink.add(createDataSetField(name, parentOddr, oddrn, TypeEnum.LIST, \"repeated\", nullable));\n\n    String itemName = field.getType() == Descriptors.FieldDescriptor.Type.MESSAGE\n        ? field.getMessageType().getName()\n        : field.getType().name().toLowerCase();\n\n    extract(\n        field,\n        oddrn,\n        oddrn + \"/items/\" + itemName,\n        itemName,\n        nullable,\n        false,\n        registeredRecords,\n        sink\n    );\n  }\n\n  private static void extractMessage(Descriptors.FieldDescriptor field,\n                                     String parentOddr,\n                                     String oddrn, //null for root\n                                     String name,\n                                     boolean nullable,\n                                     ImmutableSet<String> registeredRecords,\n                                     List<DataSetField> sink) {\n    if (extractProtoWellKnownType(field, parentOddr, oddrn, name, nullable, sink)) {\n      return;\n    }\n    sink.add(createDataSetField(name, parentOddr, oddrn, TypeEnum.STRUCT, getLogicalTypeName(field), nullable));\n\n    String msgTypeName = field.getMessageType().getFullName();\n    if (registeredRecords.contains(msgTypeName)) {\n      // avoiding recursion by checking if record already registered in parsing chain\n      return;\n    }\n    var newRegisteredRecords = ImmutableSet.<String>builder()\n        .addAll(registeredRecords)\n        .add(msgTypeName)\n        .build();\n\n    field.getMessageType()\n        .getFields()\n        .forEach(f -> {\n          extract(f,\n              oddrn,\n              oddrn + \"/fields/\" + f.getName(),\n              f.getName(),\n              !f.isRequired(),\n              f.isRepeated(),\n              newRegisteredRecords,\n              sink\n          );\n        });\n  }\n\n  private static void extractPrimitive(Descriptors.FieldDescriptor field,\n                                       String parentOddr,\n                                       String oddrn,\n                                       String name,\n                                       boolean nullable,\n                                       List<DataSetField> sink) {\n    sink.add(\n        createDataSetField(\n            name,\n            parentOddr,\n            oddrn,\n            mapType(field.getType()),\n            getLogicalTypeName(field),\n            nullable\n        )\n    );\n  }\n\n  private static String getLogicalTypeName(Descriptors.FieldDescriptor f) {\n    return f.getType() == Descriptors.FieldDescriptor.Type.MESSAGE\n        ? f.getMessageType().getFullName()\n        : f.getType().name().toLowerCase();\n  }\n\n  private static DataSetField createDataSetField(String name,\n                                                 String parentOddrn,\n                                                 String oddrn,\n                                                 TypeEnum type,\n                                                 String logicalType,\n                                                 Boolean nullable) {\n    return new DataSetField()\n        .name(name)\n        .parentFieldOddrn(parentOddrn)\n        .oddrn(oddrn)\n        .type(\n            new DataSetFieldType()\n                .isNullable(nullable)\n                .logicalType(logicalType)\n                .type(type)\n        );\n  }\n\n\n  private static TypeEnum mapType(Descriptors.FieldDescriptor.Type type) {\n    return switch (type) {\n      case INT32, INT64, SINT32, SFIXED32, SINT64, UINT32, UINT64, FIXED32, FIXED64, SFIXED64 -> TypeEnum.INTEGER;\n      case FLOAT, DOUBLE -> TypeEnum.NUMBER;\n      case STRING, ENUM -> TypeEnum.STRING;\n      case BOOL -> TypeEnum.BOOLEAN;\n      case BYTES -> TypeEnum.BINARY;\n      case MESSAGE, GROUP -> TypeEnum.STRUCT;\n    };\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlApiClient.java",
    "content": "package com.provectus.kafka.ui.service.ksql;\n\nimport static ksql.KsqlGrammarParser.DefineVariableContext;\nimport static ksql.KsqlGrammarParser.PrintTopicContext;\nimport static ksql.KsqlGrammarParser.SingleStatementContext;\nimport static ksql.KsqlGrammarParser.UndefineVariableContext;\nimport static org.springframework.http.MediaType.APPLICATION_JSON;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport com.fasterxml.jackson.databind.node.TextNode;\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.service.ksql.response.ResponseParser;\nimport com.provectus.kafka.ui.util.WebClientConfigurator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport javax.annotation.Nullable;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.core.codec.DecodingException;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.codec.json.Jackson2JsonDecoder;\nimport org.springframework.http.codec.json.Jackson2JsonEncoder;\nimport org.springframework.util.MimeType;\nimport org.springframework.util.MimeTypeUtils;\nimport org.springframework.util.unit.DataSize;\nimport org.springframework.web.reactive.function.client.WebClient;\nimport org.springframework.web.reactive.function.client.WebClientResponseException;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\n@Slf4j\npublic class KsqlApiClient {\n\n  private static final MimeType KQL_API_MIME_TYPE = MimeTypeUtils.parseMimeType(\"application/vnd.ksql.v1+json\");\n\n  private static final Set<Class<?>> UNSUPPORTED_STMT_TYPES = Set.of(\n      PrintTopicContext.class,\n      DefineVariableContext.class,\n      UndefineVariableContext.class\n  );\n\n  @Builder(toBuilder = true)\n  @Value\n  public static class KsqlResponseTable {\n    String header;\n    List<String> columnNames;\n    List<List<JsonNode>> values;\n    boolean error;\n\n    public Optional<JsonNode> getColumnValue(List<JsonNode> row, String column) {\n      int colIdx = columnNames.indexOf(column);\n      return colIdx >= 0\n          ? Optional.ofNullable(row.get(colIdx))\n          : Optional.empty();\n    }\n  }\n\n  @Value\n  private static class KsqlRequest {\n    String ksql;\n    Map<String, String> streamsProperties;\n  }\n\n  //--------------------------------------------------------------------------------------------\n\n  private final String baseUrl;\n  private final WebClient webClient;\n\n  public KsqlApiClient(String baseUrl,\n                       @Nullable ClustersProperties.KsqldbServerAuth ksqldbServerAuth,\n                       @Nullable ClustersProperties.TruststoreConfig ksqldbServerSsl,\n                       @Nullable ClustersProperties.KeystoreConfig keystoreConfig,\n                       @Nullable DataSize maxBuffSize) {\n    this.baseUrl = baseUrl;\n    this.webClient = webClient(ksqldbServerAuth, ksqldbServerSsl, keystoreConfig, maxBuffSize);\n  }\n\n  private static WebClient webClient(@Nullable ClustersProperties.KsqldbServerAuth ksqldbServerAuth,\n                                     @Nullable ClustersProperties.TruststoreConfig truststoreConfig,\n                                     @Nullable ClustersProperties.KeystoreConfig keystoreConfig,\n                                     @Nullable DataSize maxBuffSize) {\n    ksqldbServerAuth = Optional.ofNullable(ksqldbServerAuth).orElse(new ClustersProperties.KsqldbServerAuth());\n    maxBuffSize = Optional.ofNullable(maxBuffSize).orElse(DataSize.ofMegabytes(20));\n\n    return new WebClientConfigurator()\n        .configureSsl(truststoreConfig, keystoreConfig)\n        .configureBasicAuth(\n            ksqldbServerAuth.getUsername(),\n            ksqldbServerAuth.getPassword()\n        )\n        .configureBufferSize(maxBuffSize)\n        .configureCodecs(codecs -> {\n          var mapper = new JsonMapper();\n          codecs.defaultCodecs()\n              .jackson2JsonEncoder(new Jackson2JsonEncoder(mapper, KQL_API_MIME_TYPE, APPLICATION_JSON));\n          // some ksqldb versions do not set content-type header in response,\n          // but we still need to use JsonDecoder for it\n          codecs.defaultCodecs()\n              .jackson2JsonDecoder(new Jackson2JsonDecoder(mapper, MimeTypeUtils.ALL));\n        })\n        .build();\n  }\n\n  private KsqlRequest ksqlRequest(String ksql, Map<String, String> streamProperties) {\n    return new KsqlRequest(ksql, streamProperties);\n  }\n\n  private Flux<KsqlResponseTable> executeSelect(String ksql, Map<String, String> streamProperties) {\n    return webClient\n        .post()\n        .uri(baseUrl + \"/query\")\n        .accept(new MediaType(KQL_API_MIME_TYPE))\n        .contentType(new MediaType(KQL_API_MIME_TYPE))\n        .bodyValue(ksqlRequest(ksql, streamProperties))\n        .retrieve()\n        .bodyToFlux(JsonNode.class)\n        .onErrorResume(this::isUnexpectedJsonArrayEndCharException, th -> Mono.empty())\n        .map(ResponseParser::parseSelectResponse)\n        .filter(Optional::isPresent)\n        .map(Optional::get)\n        .onErrorResume(WebClientResponseException.class,\n            e -> Flux.just(ResponseParser.parseErrorResponse(e)));\n  }\n\n  /**\n   * Some version of ksqldb (?..0.24) can cut off json streaming without respect proper array ending like <p/>\n   * <code>[{\"header\":{\"queryId\":\"...\",\"schema\":\"...\"}}, ]</code>\n   * which will cause json parsing error and will be propagated to UI.\n   * This is a know issue(https://github.com/confluentinc/ksql/issues/8746), but we don't know when it will be fixed.\n   * To workaround this we need to check DecodingException err msg.\n   */\n  private boolean isUnexpectedJsonArrayEndCharException(Throwable th) {\n    return th instanceof DecodingException\n        && th.getMessage().contains(\"Unexpected character (']'\");\n  }\n\n  private Flux<KsqlResponseTable> executeStatement(String ksql,\n                                                   Map<String, String> streamProperties) {\n    return webClient\n        .post()\n        .uri(baseUrl + \"/ksql\")\n        .accept(new MediaType(KQL_API_MIME_TYPE))\n        .contentType(APPLICATION_JSON)\n        .bodyValue(ksqlRequest(ksql, streamProperties))\n        .exchangeToFlux(\n            resp -> {\n              if (resp.statusCode().isError()) {\n                return resp.createException().flux().map(ResponseParser::parseErrorResponse);\n              }\n              return resp.bodyToFlux(JsonNode.class)\n                  .flatMap(body ->\n                      // body can be an array or single object\n                      (body.isArray() ? Flux.fromIterable(body) : Flux.just(body))\n                          .flatMapIterable(ResponseParser::parseStatementResponse))\n                  // body can be empty for some statements like INSERT\n                  .switchIfEmpty(\n                      Flux.just(KsqlResponseTable.builder()\n                          .header(\"Query Result\")\n                          .columnNames(List.of(\"Result\"))\n                          .values(List.of(List.of(new TextNode(\"Success\"))))\n                          .build()));\n            }\n        );\n  }\n\n  public Flux<KsqlResponseTable> execute(String ksql, Map<String, String> streamProperties) {\n    var parsedStatements = KsqlGrammar.parse(ksql);\n    if (parsedStatements.isEmpty()) {\n      return errorTableFlux(\"Sql statement is invalid or unsupported\");\n    }\n    var statements = parsedStatements.get().getStatements();\n    if (statements.size() > 1) {\n      return errorTableFlux(\"Only single statement supported now\");\n    }\n    if (statements.size() == 0) {\n      return errorTableFlux(\"No valid ksql statement found\");\n    }\n    if (isUnsupportedStatementType(statements.get(0))) {\n      return errorTableFlux(\"Unsupported statement type\");\n    }\n    Flux<KsqlResponseTable> outputFlux;\n    if (KsqlGrammar.isSelect(statements.get(0))) {\n      outputFlux = executeSelect(ksql, streamProperties);\n    } else {\n      outputFlux = executeStatement(ksql, streamProperties);\n    }\n    return outputFlux.onErrorResume(Exception.class,\n        e -> {\n          log.error(\"Unexpected error while execution ksql: {}\", ksql, e);\n          return errorTableFlux(\"Unexpected error: \" + e.getMessage());\n        });\n  }\n\n  private Flux<KsqlResponseTable> errorTableFlux(String errorText) {\n    return Flux.just(ResponseParser.errorTableWithTextMsg(errorText));\n  }\n\n  private boolean isUnsupportedStatementType(SingleStatementContext context) {\n    var ctxClass = context.statement().getClass();\n    return UNSUPPORTED_STMT_TYPES.contains(ctxClass);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlGrammar.java",
    "content": "package com.provectus.kafka.ui.service.ksql;\n\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport java.util.List;\nimport java.util.Optional;\nimport ksql.KsqlGrammarLexer;\nimport ksql.KsqlGrammarParser;\nimport lombok.RequiredArgsConstructor;\nimport lombok.Value;\nimport lombok.experimental.Delegate;\nimport org.antlr.v4.runtime.BaseErrorListener;\nimport org.antlr.v4.runtime.CharStream;\nimport org.antlr.v4.runtime.CharStreams;\nimport org.antlr.v4.runtime.CommonTokenStream;\nimport org.antlr.v4.runtime.IntStream;\nimport org.antlr.v4.runtime.RecognitionException;\nimport org.antlr.v4.runtime.Recognizer;\nimport org.antlr.v4.runtime.atn.PredictionMode;\n\nclass KsqlGrammar {\n\n  private KsqlGrammar() {\n  }\n\n  @Value\n  static class KsqlStatements {\n    List<KsqlGrammarParser.SingleStatementContext> statements;\n  }\n\n  // returns Empty if no valid statements found\n  static Optional<KsqlStatements> parse(String ksql) {\n    var parsed = parseStatements(ksql);\n    if (parsed.singleStatement().stream()\n        .anyMatch(s -> s.statement().exception != null)) {\n      return Optional.empty();\n    }\n    return Optional.of(new KsqlStatements(parsed.singleStatement()));\n  }\n\n  static boolean isSelect(KsqlGrammarParser.SingleStatementContext statement) {\n    return statement.statement() instanceof ksql.KsqlGrammarParser.QueryStatementContext;\n  }\n\n  private static ksql.KsqlGrammarParser.StatementsContext parseStatements(final String sql) {\n    var lexer = new KsqlGrammarLexer(CaseInsensitiveStream.from(CharStreams.fromString(sql)));\n    var tokenStream = new CommonTokenStream(lexer);\n    var grammarParser = new ksql.KsqlGrammarParser(tokenStream);\n\n    lexer.addErrorListener(new BaseErrorListener() {\n      @Override\n      public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol,\n                              int line, int charPositionInLine,\n                              String msg, RecognitionException e) {\n        throw new ValidationException(\"Invalid syntax: \" + msg);\n      }\n    });\n    grammarParser.getInterpreter().setPredictionMode(PredictionMode.LL);\n    try {\n      return grammarParser.statements();\n    } catch (Exception e) {\n      throw new ValidationException(\"Error parsing ksql query: \" + e.getMessage());\n    }\n  }\n\n  // impl copied from https://github.com/confluentinc/ksql/blob/master/ksqldb-parser/src/main/java/io/confluent/ksql/parser/CaseInsensitiveStream.java\n  @RequiredArgsConstructor\n  private static class CaseInsensitiveStream implements CharStream {\n    @Delegate\n    final CharStream stream;\n\n    public static CaseInsensitiveStream from(CharStream stream) {\n      // we only need to override LA method\n      return new CaseInsensitiveStream(stream) {\n        @Override\n        public int LA(final int i) {\n          final int result = stream.LA(i);\n          switch (result) {\n            case 0:\n            case IntStream.EOF:\n              return result;\n            default:\n              return Character.toUpperCase(result);\n          }\n        }\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2.java",
    "content": "package com.provectus.kafka.ui.service.ksql;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.google.common.cache.Cache;\nimport com.google.common.cache.CacheBuilder;\nimport com.provectus.kafka.ui.exception.KsqlApiException;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.KsqlStreamDescriptionDTO;\nimport com.provectus.kafka.ui.model.KsqlTableDescriptionDTO;\nimport com.provectus.kafka.ui.service.ksql.KsqlApiClient.KsqlResponseTable;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.TimeUnit;\nimport java.util.stream.Collectors;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Service;\nimport reactor.core.publisher.Flux;\n\n@Slf4j\n@Service\npublic class KsqlServiceV2 {\n\n  @lombok.Value\n  private static class KsqlExecuteCommand {\n    KafkaCluster cluster;\n    String ksql;\n    Map<String, String> streamProperties;\n  }\n\n  private final Cache<String, KsqlExecuteCommand> registeredCommands =\n      CacheBuilder.newBuilder()\n          .expireAfterWrite(1, TimeUnit.MINUTES)\n          .build();\n\n  public String registerCommand(KafkaCluster cluster,\n                                String ksql,\n                                Map<String, String> streamProperties) {\n    String uuid = UUID.randomUUID().toString();\n    registeredCommands.put(uuid, new KsqlExecuteCommand(cluster, ksql, streamProperties));\n    return uuid;\n  }\n\n  public Flux<KsqlResponseTable> execute(String commandId) {\n    var cmd = registeredCommands.getIfPresent(commandId);\n    if (cmd == null) {\n      throw new ValidationException(\"No command registered with id \" + commandId);\n    }\n    registeredCommands.invalidate(commandId);\n    return cmd.cluster.getKsqlClient()\n        .flux(client -> client.execute(cmd.ksql, cmd.streamProperties));\n  }\n\n  public Flux<KsqlTableDescriptionDTO> listTables(KafkaCluster cluster) {\n    return cluster.getKsqlClient()\n        .flux(client -> client.execute(\"LIST TABLES;\", Map.of()))\n        .flatMap(resp -> {\n          if (!resp.getHeader().equals(\"Tables\")) {\n            log.error(\"Unexpected result header: {}\", resp.getHeader());\n            log.debug(\"Unexpected result {}\", resp);\n            return Flux.error(new KsqlApiException(\"Error retrieving tables list\"));\n          }\n          return Flux.fromIterable(resp.getValues()\n              .stream()\n              .map(row ->\n                  new KsqlTableDescriptionDTO()\n                      .name(resp.getColumnValue(row, \"name\").map(JsonNode::asText).orElse(null))\n                      .topic(resp.getColumnValue(row, \"topic\").map(JsonNode::asText).orElse(null))\n                      .keyFormat(resp.getColumnValue(row, \"keyFormat\").map(JsonNode::asText).orElse(null))\n                      .valueFormat(resp.getColumnValue(row, \"valueFormat\").map(JsonNode::asText).orElse(null))\n                      .isWindowed(resp.getColumnValue(row, \"isWindowed\").map(JsonNode::asBoolean).orElse(null)))\n              .collect(Collectors.toList()));\n        });\n  }\n\n  public Flux<KsqlStreamDescriptionDTO> listStreams(KafkaCluster cluster) {\n    return cluster.getKsqlClient()\n        .flux(client -> client.execute(\"LIST STREAMS;\", Map.of()))\n        .flatMap(resp -> {\n          if (!resp.getHeader().equals(\"Streams\")) {\n            log.error(\"Unexpected result header: {}\", resp.getHeader());\n            log.debug(\"Unexpected result {}\", resp);\n            return Flux.error(new KsqlApiException(\"Error retrieving streams list\"));\n          }\n          return Flux.fromIterable(resp.getValues()\n              .stream()\n              .map(row ->\n                  new KsqlStreamDescriptionDTO()\n                      .name(resp.getColumnValue(row, \"name\").map(JsonNode::asText).orElse(null))\n                      .topic(resp.getColumnValue(row, \"topic\").map(JsonNode::asText).orElse(null))\n                      .keyFormat(resp.getColumnValue(row, \"keyFormat\").map(JsonNode::asText).orElse(null))\n                      .valueFormat(\n                          // for old versions (<0.13) \"format\" column is filled,\n                          // for new version \"keyFormat\" & \"valueFormat\" columns should be filled\n                          resp.getColumnValue(row, \"valueFormat\")\n                              .or(() -> resp.getColumnValue(row, \"format\"))\n                              .map(JsonNode::asText)\n                              .orElse(null))\n              )\n              .collect(Collectors.toList()));\n        });\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/response/DynamicParser.java",
    "content": "package com.provectus.kafka.ui.service.ksql.response;\n\nimport static com.provectus.kafka.ui.service.ksql.KsqlApiClient.KsqlResponseTable;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.google.common.collect.Lists;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.StreamSupport;\n\n\nclass DynamicParser {\n\n  private DynamicParser() {\n  }\n\n  static KsqlResponseTable parseArray(String tableName, JsonNode array) {\n    return parseArray(tableName, getFieldNamesFromArray(array), array);\n  }\n\n  static KsqlResponseTable parseArray(String tableName,\n                                      List<String> columnNames,\n                                      JsonNode array) {\n    return KsqlResponseTable.builder()\n        .header(tableName)\n        .columnNames(columnNames)\n        .values(\n            StreamSupport.stream(array.spliterator(), false)\n                .map(node ->\n                    columnNames.stream()\n                        .map(node::get)\n                        .collect(Collectors.toList()))\n                .collect(Collectors.toList())\n        ).build();\n  }\n\n  private static List<String> getFieldNamesFromArray(JsonNode array) {\n    List<String> fields = new ArrayList<>();\n    array.forEach(node -> node.fieldNames().forEachRemaining(f -> {\n      if (!fields.contains(f)) {\n        fields.add(f);\n      }\n    }));\n    return fields;\n  }\n\n  static KsqlResponseTable parseObject(String tableName, JsonNode node) {\n    if (!node.isObject()) {\n      return KsqlResponseTable.builder()\n          .header(tableName)\n          .columnNames(List.of(\"value\"))\n          .values(List.of(List.of(node)))\n          .build();\n    }\n    return parseObject(tableName, Lists.newArrayList(node.fieldNames()), node);\n  }\n\n  static KsqlResponseTable parseObject(String tableName, List<String> columnNames, JsonNode node) {\n    return KsqlResponseTable.builder()\n        .header(tableName)\n        .columnNames(columnNames)\n        .values(\n            List.of(\n                columnNames.stream()\n                    .map(node::get)\n                    .collect(Collectors.toList()))\n        )\n        .build();\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ksql/response/ResponseParser.java",
    "content": "package com.provectus.kafka.ui.service.ksql.response;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport com.fasterxml.jackson.databind.node.TextNode;\nimport com.google.common.annotations.VisibleForTesting;\nimport com.google.common.collect.Lists;\nimport com.provectus.kafka.ui.exception.KsqlApiException;\nimport com.provectus.kafka.ui.service.ksql.KsqlApiClient;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\nimport org.springframework.web.reactive.function.client.WebClientResponseException;\n\npublic class ResponseParser {\n\n  private ResponseParser() {\n  }\n\n  public static Optional<KsqlApiClient.KsqlResponseTable> parseSelectResponse(JsonNode jsonNode) {\n    // in response, we're getting either header record or row data\n    if (arrayFieldNonEmpty(jsonNode, \"header\")) {\n      return Optional.of(\n          KsqlApiClient.KsqlResponseTable.builder()\n              .header(\"Schema\")\n              .columnNames(parseSelectHeadersString(jsonNode.get(\"header\").get(\"schema\").asText()))\n              .build());\n    }\n    if (arrayFieldNonEmpty(jsonNode, \"row\")) {\n      return Optional.of(\n          KsqlApiClient.KsqlResponseTable.builder()\n              .header(\"Row\")\n              .values(\n                  List.of(Lists.newArrayList(jsonNode.get(\"row\").get(\"columns\"))))\n              .build());\n    }\n    if (jsonNode.hasNonNull(\"errorMessage\")) {\n      throw new KsqlApiException(\"Error: \" + jsonNode.get(\"errorMessage\"));\n    }\n    // remaining events can be skipped\n    return Optional.empty();\n  }\n\n  @VisibleForTesting\n  static List<String> parseSelectHeadersString(String str) {\n    List<String> headers = new ArrayList<>();\n    int structNesting = 0;\n    boolean quotes = false;\n    var headerBuilder = new StringBuilder();\n    for (char ch : str.toCharArray()) {\n      if (ch == '<') {\n        structNesting++;\n      } else if (ch == '>') {\n        structNesting--;\n      } else if (ch == '`') {\n        quotes = !quotes;\n      } else if (ch == ' ' && headerBuilder.isEmpty()) {\n        continue; //skipping leading & training whitespaces\n      } else if (ch == ',' && structNesting == 0 && !quotes) {\n        headers.add(headerBuilder.toString());\n        headerBuilder = new StringBuilder();\n        continue;\n      }\n      headerBuilder.append(ch);\n    }\n    if (!headerBuilder.isEmpty()) {\n      headers.add(headerBuilder.toString());\n    }\n    return headers;\n  }\n\n  public static KsqlApiClient.KsqlResponseTable errorTableWithTextMsg(String errorText) {\n    return KsqlApiClient.KsqlResponseTable.builder()\n        .header(\"Execution error\")\n        .columnNames(List.of(\"message\"))\n        .values(List.of(List.of(new TextNode(errorText))))\n        .error(true)\n        .build();\n  }\n\n  public static KsqlApiClient.KsqlResponseTable parseErrorResponse(WebClientResponseException e) {\n    try {\n      var errBody = new JsonMapper().readTree(e.getResponseBodyAsString());\n      return DynamicParser.parseObject(\"Execution error\", errBody)\n          .toBuilder()\n          .error(true)\n          .build();\n    } catch (Exception ex) {\n      return errorTableWithTextMsg(\n          String.format(\n              \"Unparsable error response from ksqdb, status:'%s', body: '%s'\",\n              e.getStatusCode(), e.getResponseBodyAsString()));\n    }\n  }\n\n  public static List<KsqlApiClient.KsqlResponseTable> parseStatementResponse(JsonNode jsonNode) {\n    var type = Optional.ofNullable(jsonNode.get(\"@type\"))\n        .map(JsonNode::asText)\n        .orElse(\"unknown\");\n\n    // messages structure can be inferred from https://github.com/confluentinc/ksql/blob/master/ksqldb-rest-model/src/main/java/io/confluent/ksql/rest/entity/KsqlEntity.java\n    switch (type) {\n      case \"currentStatus\":\n        return parseObject(\n            \"Status\",\n            List.of(\"status\", \"message\"),\n            jsonNode.get(\"commandStatus\")\n        );\n      case \"properties\":\n        return parseProperties(jsonNode);\n      case \"queries\":\n        return parseArray(\"Queries\", \"queries\", jsonNode);\n      case \"sourceDescription\":\n        return parseObjectDynamically(\"Source Description\", jsonNode.get(\"sourceDescription\"));\n      case \"queryDescription\":\n        return parseObjectDynamically(\"Queries Description\", jsonNode.get(\"queryDescription\"));\n      case \"topicDescription\":\n        return parseObject(\n            \"Topic Description\",\n            List.of(\"name\", \"kafkaTopic\", \"format\", \"schemaString\"),\n            jsonNode\n        );\n      case \"streams\":\n        return parseArray(\"Streams\", \"streams\", jsonNode);\n      case \"tables\":\n        return parseArray(\"Tables\", \"tables\", jsonNode);\n      case \"kafka_topics\":\n        return parseArray(\"Topics\", \"topics\", jsonNode);\n      case \"kafka_topics_extended\":\n        return parseArray(\"Topics extended\", \"topics\", jsonNode);\n      case \"executionPlan\":\n        return parseObject(\"Execution plan\", List.of(\"executionPlanText\"), jsonNode);\n      case \"source_descriptions\":\n        return parseArray(\"Source descriptions\", \"sourceDescriptions\", jsonNode);\n      case \"query_descriptions\":\n        return parseArray(\"Queries\", \"queryDescriptions\", jsonNode);\n      case \"describe_function\":\n        return parseObject(\"Function description\",\n            List.of(\"name\", \"author\", \"version\", \"description\", \"functions\", \"path\", \"type\"),\n            jsonNode\n        );\n      case \"function_names\":\n        return parseArray(\"Function Names\", \"functions\", jsonNode);\n      case \"connector_info\":\n        return parseObjectDynamically(\"Connector Info\", jsonNode.get(\"info\"));\n      case \"drop_connector\":\n        return parseObject(\"Dropped connector\", List.of(\"connectorName\"), jsonNode);\n      case \"connector_list\":\n        return parseArray(\"Connectors\", \"connectors\", jsonNode);\n      case \"connector_plugins_list\":\n        return parseArray(\"Connector Plugins\", \"connectorPlugins\", jsonNode);\n      case \"connector_description\":\n        return parseObject(\"Connector Description\",\n            List.of(\"connectorClass\", \"status\", \"sources\", \"topics\"),\n            jsonNode\n        );\n      default:\n        return parseUnknownResponse(jsonNode);\n    }\n  }\n\n  private static List<KsqlApiClient.KsqlResponseTable> parseObjectDynamically(\n      String tableName, JsonNode jsonNode) {\n    return List.of(DynamicParser.parseObject(tableName, jsonNode));\n  }\n\n  private static List<KsqlApiClient.KsqlResponseTable> parseObject(\n      String tableName, List<String> fields, JsonNode jsonNode) {\n    return List.of(DynamicParser.parseObject(tableName, fields, jsonNode));\n  }\n\n  private static List<KsqlApiClient.KsqlResponseTable> parseArray(\n      String tableName, String arrayField, JsonNode jsonNode) {\n    return List.of(DynamicParser.parseArray(tableName, jsonNode.get(arrayField)));\n  }\n\n  private static List<KsqlApiClient.KsqlResponseTable> parseProperties(JsonNode jsonNode) {\n    var tables = new ArrayList<KsqlApiClient.KsqlResponseTable>();\n    if (arrayFieldNonEmpty(jsonNode, \"properties\")) {\n      tables.add(DynamicParser.parseArray(\"properties\", jsonNode.get(\"properties\")));\n    }\n    if (arrayFieldNonEmpty(jsonNode, \"overwrittenProperties\")) {\n      tables.add(DynamicParser.parseArray(\"overwrittenProperties\",\n          jsonNode.get(\"overwrittenProperties\")));\n    }\n    return tables;\n  }\n\n  private static List<KsqlApiClient.KsqlResponseTable> parseUnknownResponse(JsonNode jsonNode) {\n    return List.of(DynamicParser.parseObject(\"Ksql Response\", jsonNode));\n  }\n\n  private static boolean arrayFieldNonEmpty(JsonNode json, String field) {\n    return json.hasNonNull(field) && !json.get(field).isEmpty();\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/DataMasking.java",
    "content": "package com.provectus.kafka.ui.service.masking;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport com.fasterxml.jackson.databind.node.ContainerNode;\nimport com.google.common.annotations.VisibleForTesting;\nimport com.google.common.base.Preconditions;\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.model.TopicMessageDTO;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport com.provectus.kafka.ui.service.masking.policies.MaskingPolicy;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.function.UnaryOperator;\nimport java.util.regex.Pattern;\nimport javax.annotation.Nullable;\nimport lombok.Value;\nimport org.apache.commons.lang3.StringUtils;\n\npublic class DataMasking {\n\n  private static final JsonMapper JSON_MAPPER = new JsonMapper();\n\n  @Value\n  static class Mask {\n    @Nullable\n    Pattern topicKeysPattern;\n    @Nullable\n    Pattern topicValuesPattern;\n\n    MaskingPolicy policy;\n\n    boolean shouldBeApplied(String topic, Serde.Target target) {\n      return target == Serde.Target.KEY\n          ? topicKeysPattern != null && topicKeysPattern.matcher(topic).matches()\n          : topicValuesPattern != null && topicValuesPattern.matcher(topic).matches();\n    }\n  }\n\n  private final List<Mask> masks;\n\n  public static DataMasking create(@Nullable List<ClustersProperties.Masking> config) {\n    return new DataMasking(\n        Optional.ofNullable(config).orElse(List.of()).stream().map(property -> {\n          Preconditions.checkNotNull(property.getType(), \"masking type not specified\");\n          Preconditions.checkArgument(\n              StringUtils.isNotEmpty(property.getTopicKeysPattern())\n                  || StringUtils.isNotEmpty(property.getTopicValuesPattern()),\n              \"topicKeysPattern or topicValuesPattern (or both) should be set for masking policy\");\n          return new Mask(\n              Optional.ofNullable(property.getTopicKeysPattern()).map(Pattern::compile).orElse(null),\n              Optional.ofNullable(property.getTopicValuesPattern()).map(Pattern::compile).orElse(null),\n              MaskingPolicy.create(property)\n          );\n        }).toList()\n    );\n  }\n\n  @VisibleForTesting\n  DataMasking(List<Mask> masks) {\n    this.masks = masks;\n  }\n\n  public UnaryOperator<TopicMessageDTO> getMaskerForTopic(String topic) {\n    var keyMasker = getMaskingFunction(topic, Serde.Target.KEY);\n    var valMasker = getMaskingFunction(topic, Serde.Target.VALUE);\n    return msg -> msg\n        .key(keyMasker.apply(msg.getKey()))\n        .content(valMasker.apply(msg.getContent()));\n  }\n\n  @VisibleForTesting\n  UnaryOperator<String> getMaskingFunction(String topic, Serde.Target target) {\n    var targetMasks = masks.stream().filter(m -> m.shouldBeApplied(topic, target)).toList();\n    if (targetMasks.isEmpty()) {\n      return UnaryOperator.identity();\n    }\n    return inputStr -> {\n      if (inputStr == null) {\n        return null;\n      }\n      try {\n        JsonNode json = JSON_MAPPER.readTree(inputStr);\n        if (json.isContainerNode()) {\n          for (Mask targetMask : targetMasks) {\n            json = targetMask.policy.applyToJsonContainer((ContainerNode<?>) json);\n          }\n          return json.toString();\n        }\n      } catch (JsonProcessingException jsonException) {\n        //just ignore\n      }\n      // if we can't parse input as json or parsed json is not object/array\n      // we just apply first found policy\n      // (there is no need to apply all of them, because they will just override each other)\n      return targetMasks.get(0).policy.applyToString(inputStr);\n    };\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/FieldsSelector.java",
    "content": "package com.provectus.kafka.ui.service.masking.policies;\n\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport java.util.regex.Pattern;\nimport org.springframework.util.CollectionUtils;\nimport org.springframework.util.StringUtils;\n\ninterface FieldsSelector {\n\n  static FieldsSelector create(ClustersProperties.Masking property) {\n    if (StringUtils.hasText(property.getFieldsNamePattern()) && !CollectionUtils.isEmpty(property.getFields())) {\n      throw new ValidationException(\"You can't provide both fieldNames & fieldsNamePattern for masking\");\n    }\n    if (StringUtils.hasText(property.getFieldsNamePattern())) {\n      Pattern pattern = Pattern.compile(property.getFieldsNamePattern());\n      return f -> pattern.matcher(f).matches();\n    }\n    if (!CollectionUtils.isEmpty(property.getFields())) {\n      return f -> property.getFields().contains(f);\n    }\n    //no pattern, no field names - mean all fields should be masked\n    return fieldName -> true;\n  }\n\n  boolean shouldBeMasked(String fieldName);\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Mask.java",
    "content": "package com.provectus.kafka.ui.service.masking.policies;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.fasterxml.jackson.databind.node.ContainerNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.fasterxml.jackson.databind.node.TextNode;\nimport com.google.common.base.Preconditions;\nimport java.util.List;\nimport java.util.function.UnaryOperator;\n\nclass Mask extends MaskingPolicy {\n\n  static final List<String> DEFAULT_PATTERN = List.of(\"X\", \"x\", \"n\", \"-\");\n\n  private final UnaryOperator<String> masker;\n\n  Mask(FieldsSelector fieldsSelector, List<String> maskingChars) {\n    super(fieldsSelector);\n    this.masker = createMasker(maskingChars);\n  }\n\n  @Override\n  public ContainerNode<?> applyToJsonContainer(ContainerNode<?> node) {\n    return (ContainerNode<?>) maskWithFieldsCheck(node);\n  }\n\n  @Override\n  public String applyToString(String str) {\n    return masker.apply(str);\n  }\n\n  private static UnaryOperator<String> createMasker(List<String> maskingChars) {\n    Preconditions.checkNotNull(maskingChars);\n    Preconditions.checkArgument(maskingChars.size() == 4, \"mask pattern should contain 4 elements\");\n    return input -> {\n      StringBuilder sb = new StringBuilder(input.length());\n      for (int i = 0; i < input.length(); i++) {\n        int cp = input.codePointAt(i);\n        switch (Character.getType(cp)) {\n          case Character.SPACE_SEPARATOR,\n              Character.LINE_SEPARATOR,\n              Character.PARAGRAPH_SEPARATOR -> sb.appendCodePoint(cp); // keeping separators as-is\n          case Character.UPPERCASE_LETTER -> sb.append(maskingChars.get(0));\n          case Character.LOWERCASE_LETTER -> sb.append(maskingChars.get(1));\n          case Character.DECIMAL_DIGIT_NUMBER -> sb.append(maskingChars.get(2));\n          default -> sb.append(maskingChars.get(3));\n        }\n      }\n      return sb.toString();\n    };\n  }\n\n  private JsonNode maskWithFieldsCheck(JsonNode node) {\n    if (node.isObject()) {\n      ObjectNode obj = ((ObjectNode) node).objectNode();\n      node.fields().forEachRemaining(f -> {\n        String fieldName = f.getKey();\n        JsonNode fieldVal = f.getValue();\n        if (fieldShouldBeMasked(fieldName)) {\n          obj.set(fieldName, maskNodeRecursively(fieldVal));\n        } else {\n          obj.set(fieldName, maskWithFieldsCheck(fieldVal));\n        }\n      });\n      return obj;\n    } else if (node.isArray()) {\n      ArrayNode arr = ((ArrayNode) node).arrayNode(node.size());\n      node.elements().forEachRemaining(e -> arr.add(maskWithFieldsCheck(e)));\n      return arr;\n    }\n    return node;\n  }\n\n  private JsonNode maskNodeRecursively(JsonNode node) {\n    if (node.isObject()) {\n      ObjectNode obj = ((ObjectNode) node).objectNode();\n      node.fields().forEachRemaining(f -> obj.set(f.getKey(), maskNodeRecursively(f.getValue())));\n      return obj;\n    } else if (node.isArray()) {\n      ArrayNode arr = ((ArrayNode) node).arrayNode(node.size());\n      node.elements().forEachRemaining(e -> arr.add(maskNodeRecursively(e)));\n      return arr;\n    }\n    return new TextNode(masker.apply(node.asText()));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/MaskingPolicy.java",
    "content": "package com.provectus.kafka.ui.service.masking.policies;\n\nimport com.fasterxml.jackson.databind.node.ContainerNode;\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport lombok.RequiredArgsConstructor;\n\n@RequiredArgsConstructor\npublic abstract class MaskingPolicy {\n\n  public static MaskingPolicy create(ClustersProperties.Masking property) {\n    FieldsSelector fieldsSelector = FieldsSelector.create(property);\n    return switch (property.getType()) {\n      case REMOVE -> new Remove(fieldsSelector);\n      case REPLACE -> new Replace(\n          fieldsSelector,\n          property.getReplacement() == null\n              ? Replace.DEFAULT_REPLACEMENT\n              : property.getReplacement()\n      );\n      case MASK -> new Mask(\n          fieldsSelector,\n          property.getMaskingCharsReplacement() == null\n              ? Mask.DEFAULT_PATTERN\n              : property.getMaskingCharsReplacement()\n      );\n    };\n  }\n\n  //----------------------------------------------------------------\n\n  private final FieldsSelector fieldsSelector;\n\n  protected boolean fieldShouldBeMasked(String fieldName) {\n    return fieldsSelector.shouldBeMasked(fieldName);\n  }\n\n  public abstract ContainerNode<?> applyToJsonContainer(ContainerNode<?> node);\n\n  public abstract String applyToString(String str);\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Remove.java",
    "content": "package com.provectus.kafka.ui.service.masking.policies;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.fasterxml.jackson.databind.node.ContainerNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\n\n\nclass Remove extends MaskingPolicy {\n\n  Remove(FieldsSelector fieldsSelector) {\n    super(fieldsSelector);\n  }\n\n  @Override\n  public String applyToString(String str) {\n    return \"null\";\n  }\n\n  @Override\n  public ContainerNode<?> applyToJsonContainer(ContainerNode<?> node) {\n    return (ContainerNode<?>) removeFields(node);\n  }\n\n  private JsonNode removeFields(JsonNode node) {\n    if (node.isObject()) {\n      ObjectNode obj = ((ObjectNode) node).objectNode();\n      node.fields().forEachRemaining(f -> {\n        String fieldName = f.getKey();\n        JsonNode fieldVal = f.getValue();\n        if (!fieldShouldBeMasked(fieldName)) {\n          obj.set(fieldName, removeFields(fieldVal));\n        }\n      });\n      return obj;\n    } else if (node.isArray()) {\n      var arr = ((ArrayNode) node).arrayNode(node.size());\n      node.elements().forEachRemaining(e -> arr.add(removeFields(e)));\n      return arr;\n    }\n    return node;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Replace.java",
    "content": "package com.provectus.kafka.ui.service.masking.policies;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.fasterxml.jackson.databind.node.ContainerNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.fasterxml.jackson.databind.node.TextNode;\nimport com.google.common.base.Preconditions;\n\nclass Replace extends MaskingPolicy {\n\n  static final String DEFAULT_REPLACEMENT = \"***DATA_MASKED***\";\n\n  private final String replacement;\n\n  Replace(FieldsSelector fieldsSelector, String replacementString) {\n    super(fieldsSelector);\n    this.replacement = Preconditions.checkNotNull(replacementString);\n  }\n\n  @Override\n  public String applyToString(String str) {\n    return replacement;\n  }\n\n  @Override\n  public ContainerNode<?> applyToJsonContainer(ContainerNode<?> node) {\n    return (ContainerNode<?>) replaceWithFieldsCheck(node);\n  }\n\n  private JsonNode replaceWithFieldsCheck(JsonNode node) {\n    if (node.isObject()) {\n      ObjectNode obj = ((ObjectNode) node).objectNode();\n      node.fields().forEachRemaining(f -> {\n        String fieldName = f.getKey();\n        JsonNode fieldVal = f.getValue();\n        if (fieldShouldBeMasked(fieldName)) {\n          obj.set(fieldName, replaceRecursive(fieldVal));\n        } else {\n          obj.set(fieldName, replaceWithFieldsCheck(fieldVal));\n        }\n      });\n      return obj;\n    } else if (node.isArray()) {\n      ArrayNode arr = ((ArrayNode) node).arrayNode(node.size());\n      node.elements().forEachRemaining(e -> arr.add(replaceWithFieldsCheck(e)));\n      return arr;\n    }\n    // if it is not an object or array - we have nothing to replace here\n    return node;\n  }\n\n  private JsonNode replaceRecursive(JsonNode node) {\n    if (node.isObject()) {\n      ObjectNode obj = ((ObjectNode) node).objectNode();\n      node.fields().forEachRemaining(f -> obj.set(f.getKey(), replaceRecursive(f.getValue())));\n      return obj;\n    } else if (node.isArray()) {\n      ArrayNode arr = ((ArrayNode) node).arrayNode(node.size());\n      node.elements().forEachRemaining(e -> arr.add(replaceRecursive(e)));\n      return arr;\n    }\n    return new TextNode(replacement);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxMetricsFormatter.java",
    "content": "package com.provectus.kafka.ui.service.metrics;\n\nimport java.math.BigDecimal;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport javax.management.MBeanAttributeInfo;\nimport javax.management.ObjectName;\n\n/**\n * Converts JMX metrics into JmxExporter prometheus format: <a href=\"https://github.com/prometheus/jmx_exporter#default-format\">format</a>.\n */\nclass JmxMetricsFormatter {\n\n  // copied from https://github.com/prometheus/jmx_exporter/blob/b6b811b4aae994e812e902b26dd41f29364c0e2b/collector/src/main/java/io/prometheus/jmx/JmxMBeanPropertyCache.java#L15\n  private static final Pattern PROPERTY_PATTERN = Pattern.compile(\n      \"([^,=:\\\\*\\\\?]+)=(\\\"(?:[^\\\\\\\\\\\"]*(?:\\\\\\\\.)?)*\\\"|[^,=:\\\"]*)\");\n\n  static List<RawMetric> constructMetricsList(ObjectName jmxMetric,\n                                              MBeanAttributeInfo[] attributes,\n                                              Object[] attrValues) {\n    String domain = fixIllegalChars(jmxMetric.getDomain());\n    LinkedHashMap<String, String> labels = getLabelsMap(jmxMetric);\n    String firstLabel = labels.keySet().iterator().next();\n    String firstLabelValue = fixIllegalChars(labels.get(firstLabel));\n    labels.remove(firstLabel); //removing first label since it's value will be in name\n\n    List<RawMetric> result = new ArrayList<>(attributes.length);\n    for (int i = 0; i < attributes.length; i++) {\n      String attrName = fixIllegalChars(attributes[i].getName());\n      convertNumericValue(attrValues[i]).ifPresent(convertedValue -> {\n        String name = String.format(\"%s_%s_%s\", domain, firstLabelValue, attrName);\n        var metric = RawMetric.create(name, labels, convertedValue);\n        result.add(metric);\n      });\n    }\n    return result;\n  }\n\n  private static String fixIllegalChars(String str) {\n    return str\n        .replace('.', '_')\n        .replace('-', '_');\n  }\n\n  private static Optional<BigDecimal> convertNumericValue(Object value) {\n    if (!(value instanceof Number)) {\n      return Optional.empty();\n    }\n    try {\n      if (value instanceof Long) {\n        return Optional.of(new BigDecimal((Long) value));\n      } else if (value instanceof Integer) {\n        return Optional.of(new BigDecimal((Integer) value));\n      }\n      return Optional.of(new BigDecimal(value.toString()));\n    } catch (NumberFormatException nfe) {\n      return Optional.empty();\n    }\n  }\n\n  /**\n   * Converts Mbean properties to map keeping order (copied from jmx_exporter repo).\n   */\n  private static LinkedHashMap<String, String> getLabelsMap(ObjectName mbeanName) {\n    LinkedHashMap<String, String> keyProperties = new LinkedHashMap<>();\n    String properties = mbeanName.getKeyPropertyListString();\n    Matcher match = PROPERTY_PATTERN.matcher(properties);\n    while (match.lookingAt()) {\n      String labelName = fixIllegalChars(match.group(1)); // label names should be fixed\n      String labelValue = match.group(2);\n      keyProperties.put(labelName, labelValue);\n      properties = properties.substring(match.end());\n      if (properties.startsWith(\",\")) {\n        properties = properties.substring(1);\n      }\n      match.reset(properties);\n    }\n    return keyProperties;\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxMetricsRetriever.java",
    "content": "package com.provectus.kafka.ui.service.metrics;\n\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport java.io.Closeable;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\nimport javax.management.MBeanAttributeInfo;\nimport javax.management.MBeanServerConnection;\nimport javax.management.ObjectName;\nimport javax.management.remote.JMXConnector;\nimport javax.management.remote.JMXConnectorFactory;\nimport javax.management.remote.JMXServiceURL;\nimport lombok.SneakyThrows;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.kafka.common.Node;\nimport org.springframework.stereotype.Service;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Schedulers;\n\n\n@Service\n@Slf4j\nclass JmxMetricsRetriever implements MetricsRetriever, Closeable {\n\n  private static final boolean SSL_JMX_SUPPORTED;\n\n  static {\n    // see JmxSslSocketFactory doc for details\n    SSL_JMX_SUPPORTED = JmxSslSocketFactory.initialized();\n  }\n\n  private static final String JMX_URL = \"service:jmx:rmi:///jndi/rmi://\";\n  private static final String JMX_SERVICE_TYPE = \"jmxrmi\";\n  private static final String CANONICAL_NAME_PATTERN = \"kafka.server*:*\";\n\n  @Override\n  public void close() {\n    JmxSslSocketFactory.clearFactoriesCache();\n  }\n\n  @Override\n  public Flux<RawMetric> retrieve(KafkaCluster c, Node node) {\n    if (isSslJmxEndpoint(c) && !SSL_JMX_SUPPORTED) {\n      log.warn(\"Cluster {} has jmx ssl configured, but it is not supported\", c.getName());\n      return Flux.empty();\n    }\n    return Mono.fromSupplier(() -> retrieveSync(c, node))\n        .subscribeOn(Schedulers.boundedElastic())\n        .flatMapMany(Flux::fromIterable);\n  }\n\n  private boolean isSslJmxEndpoint(KafkaCluster cluster) {\n    return cluster.getMetricsConfig().getKeystoreLocation() != null;\n  }\n\n  @SneakyThrows\n  private List<RawMetric> retrieveSync(KafkaCluster c, Node node) {\n    String jmxUrl = JMX_URL + node.host() + \":\" + c.getMetricsConfig().getPort() + \"/\" + JMX_SERVICE_TYPE;\n    log.debug(\"Collection JMX metrics for {}\", jmxUrl);\n    List<RawMetric> result = new ArrayList<>();\n    withJmxConnector(jmxUrl, c, jmxConnector -> getMetricsFromJmx(jmxConnector, result));\n    log.debug(\"{} metrics collected for {}\", result.size(), jmxUrl);\n    return result;\n  }\n\n  private void withJmxConnector(String jmxUrl,\n                                KafkaCluster c,\n                                Consumer<JMXConnector> consumer) {\n    var env = prepareJmxEnvAndSetThreadLocal(c);\n    try (JMXConnector connector = JMXConnectorFactory.newJMXConnector(new JMXServiceURL(jmxUrl), env)) {\n      try {\n        connector.connect(env);\n      } catch (Exception exception) {\n        log.error(\"Error connecting to {}\", jmxUrl, exception);\n        return;\n      }\n      consumer.accept(connector);\n    } catch (Exception e) {\n      log.error(\"Error getting jmx metrics from {}\", jmxUrl, e);\n    } finally {\n      JmxSslSocketFactory.clearThreadLocalContext();\n    }\n  }\n\n  private Map<String, Object> prepareJmxEnvAndSetThreadLocal(KafkaCluster cluster) {\n    var metricsConfig = cluster.getMetricsConfig();\n    Map<String, Object> env = new HashMap<>();\n    if (isSslJmxEndpoint(cluster)) {\n      var clusterSsl = cluster.getOriginalProperties().getSsl();\n      JmxSslSocketFactory.setSslContextThreadLocal(\n          clusterSsl != null ? clusterSsl.getTruststoreLocation() : null,\n          clusterSsl != null ? clusterSsl.getTruststorePassword() : null,\n          metricsConfig.getKeystoreLocation(),\n          metricsConfig.getKeystorePassword()\n      );\n      JmxSslSocketFactory.editJmxConnectorEnv(env);\n    }\n\n    if (StringUtils.isNotEmpty(metricsConfig.getUsername())\n        && StringUtils.isNotEmpty(metricsConfig.getPassword())) {\n      env.put(\n          JMXConnector.CREDENTIALS,\n          new String[] {metricsConfig.getUsername(), metricsConfig.getPassword()}\n      );\n    }\n    return env;\n  }\n\n  @SneakyThrows\n  private void getMetricsFromJmx(JMXConnector jmxConnector, List<RawMetric> sink) {\n    MBeanServerConnection msc = jmxConnector.getMBeanServerConnection();\n    var jmxMetrics = msc.queryNames(new ObjectName(CANONICAL_NAME_PATTERN), null);\n    for (ObjectName jmxMetric : jmxMetrics) {\n      sink.addAll(extractObjectMetrics(jmxMetric, msc));\n    }\n  }\n\n  @SneakyThrows\n  private List<RawMetric> extractObjectMetrics(ObjectName objectName, MBeanServerConnection msc) {\n    MBeanAttributeInfo[] attrNames = msc.getMBeanInfo(objectName).getAttributes();\n    Object[] attrValues = new Object[attrNames.length];\n    for (int i = 0; i < attrNames.length; i++) {\n      attrValues[i] = msc.getAttribute(objectName, attrNames[i].getName());\n    }\n    return JmxMetricsFormatter.constructMetricsList(objectName, attrNames, attrValues);\n  }\n\n}\n\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxSslSocketFactory.java",
    "content": "package com.provectus.kafka.ui.service.metrics;\n\nimport com.google.common.base.Preconditions;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.lang.reflect.Field;\nimport java.net.InetAddress;\nimport java.net.Socket;\nimport java.net.UnknownHostException;\nimport java.security.KeyStore;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport javax.annotation.Nullable;\nimport javax.net.ssl.KeyManagerFactory;\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.TrustManagerFactory;\nimport javax.rmi.ssl.SslRMIClientSocketFactory;\nimport lombok.SneakyThrows;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.util.ResourceUtils;\n\n/*\n * Purpose of this class to provide an ability to connect to different JMX endpoints using different keystores.\n *\n * Usually, when you want to establish SSL JMX connection you set \"com.sun.jndi.rmi.factory.socket\" env\n * property to SslRMIClientSocketFactory instance. SslRMIClientSocketFactory itself uses SSLSocketFactory.getDefault()\n * as a socket factory implementation. Problem here is that when ones SslRMIClientSocketFactory instance is created,\n * the same cached SSLSocketFactory instance will be used to establish connection with *all* JMX endpoints.\n * Moreover, even if we submit custom SslRMIClientSocketFactory implementation which takes specific ssl context\n * into account, SslRMIClientSocketFactory is\n * internally created during RMI calls.\n *\n * So, the only way we found to deal with it is to change internal field ('defaultSocketFactory') of\n * SslRMIClientSocketFactory to our custom impl, and left all internal RMI code work as is.\n * Since RMI code is synchronous, we can pass parameters (which are truststore/keystore) to our custom factory\n * that we want to use when creating ssl socket via ThreadLocal variables.\n *\n * NOTE 1: Theoretically we could avoid using reflection to set internal field set by\n * setting \"ssl.SocketFactory.provider\" security property (see code in SSLSocketFactory.getDefault()),\n * but that code uses systemClassloader which is not working right when we're creating executable spring boot jar\n * (https://docs.spring.io/spring-boot/docs/current/reference/html/executable-jar.html#appendix.executable-jar.restrictions).\n * We can use this if we swith to other jar-packing solutions in the future.\n *\n * NOTE 2: There are two paths from which socket factory is called - when jmx connection if established (we manage this\n * by passing ThreadLocal vars) and from DGCClient in background thread - we deal with that we cache created factories\n * for specific host+port.\n *\n */\n@Slf4j\nclass JmxSslSocketFactory extends javax.net.ssl.SSLSocketFactory {\n\n  private static final boolean SSL_JMX_SUPPORTED;\n\n  static {\n    boolean sslJmxSupported = false;\n    try {\n      Field defaultSocketFactoryField = SslRMIClientSocketFactory.class.getDeclaredField(\"defaultSocketFactory\");\n      defaultSocketFactoryField.setAccessible(true);\n      defaultSocketFactoryField.set(null, new JmxSslSocketFactory());\n      sslJmxSupported = true;\n    } catch (Exception e) {\n      log.error(\"----------------------------------\");\n      log.error(\"SSL can't be enabled for JMX retrieval. \"\n              + \"Make sure your java app run with '--add-opens java.rmi/javax.rmi.ssl=ALL-UNNAMED' arg. Err: {}\",\n          e.getMessage());\n      log.trace(\"SSL can't be enabled for JMX retrieval\", e);\n      log.error(\"----------------------------------\");\n    }\n    SSL_JMX_SUPPORTED = sslJmxSupported;\n  }\n\n  public static boolean initialized() {\n    return SSL_JMX_SUPPORTED;\n  }\n\n  private static final ThreadLocal<Ssl> SSL_CONTEXT_THREAD_LOCAL = new ThreadLocal<>();\n\n  private static final Map<HostAndPort, javax.net.ssl.SSLSocketFactory> CACHED_FACTORIES = new ConcurrentHashMap<>();\n\n  private record HostAndPort(String host, int port) {\n  }\n\n  private record Ssl(@Nullable String truststoreLocation,\n                     @Nullable String truststorePassword,\n                     @Nullable String keystoreLocation,\n                     @Nullable String keystorePassword) {\n  }\n\n  public static void setSslContextThreadLocal(@Nullable String truststoreLocation,\n                                              @Nullable String truststorePassword,\n                                              @Nullable String keystoreLocation,\n                                              @Nullable String keystorePassword) {\n    SSL_CONTEXT_THREAD_LOCAL.set(\n        new Ssl(truststoreLocation, truststorePassword, keystoreLocation, keystorePassword));\n  }\n\n  // should be called when (host:port) -> factory cache should be invalidated (ex. on app config reload)\n  public static void clearFactoriesCache() {\n    CACHED_FACTORIES.clear();\n  }\n\n  public static void clearThreadLocalContext() {\n    SSL_CONTEXT_THREAD_LOCAL.set(null);\n  }\n\n  public static void editJmxConnectorEnv(Map<String, Object> env) {\n    env.put(\"com.sun.jndi.rmi.factory.socket\", new SslRMIClientSocketFactory());\n  }\n\n  //-----------------------------------------------------------------------------------------------\n\n  private final javax.net.ssl.SSLSocketFactory defaultSocketFactory;\n\n  @SneakyThrows\n  public JmxSslSocketFactory() {\n    this.defaultSocketFactory = SSLContext.getDefault().getSocketFactory();\n  }\n\n  @SneakyThrows\n  private javax.net.ssl.SSLSocketFactory createFactoryFromThreadLocalCtx() {\n    Ssl ssl = Preconditions.checkNotNull(SSL_CONTEXT_THREAD_LOCAL.get());\n\n    var trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());\n    if (ssl.truststoreLocation() != null && ssl.truststorePassword() != null) {\n      KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());\n      trustStore.load(\n          new FileInputStream((ResourceUtils.getFile(ssl.truststoreLocation()))),\n          ssl.truststorePassword().toCharArray()\n      );\n      trustManagerFactory.init(trustStore);\n    }\n\n    var keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());\n    if (ssl.keystoreLocation() != null && ssl.keystorePassword() != null) {\n      KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());\n      keyStore.load(\n          new FileInputStream(ResourceUtils.getFile(ssl.keystoreLocation())),\n          ssl.keystorePassword().toCharArray()\n      );\n      keyManagerFactory.init(keyStore, ssl.keystorePassword().toCharArray());\n    }\n\n    SSLContext ctx = SSLContext.getInstance(\"TLS\");\n    ctx.init(\n        keyManagerFactory.getKeyManagers(),\n        trustManagerFactory.getTrustManagers(),\n        null\n    );\n    return ctx.getSocketFactory();\n  }\n\n  private boolean threadLocalContextSet() {\n    return SSL_CONTEXT_THREAD_LOCAL.get() != null;\n  }\n\n  @Override\n  public Socket createSocket(String host, int port) throws IOException {\n    var hostAndPort = new HostAndPort(host, port);\n    if (CACHED_FACTORIES.containsKey(hostAndPort)) {\n      return CACHED_FACTORIES.get(hostAndPort).createSocket(host, port);\n    } else if (threadLocalContextSet()) {\n      var factory = createFactoryFromThreadLocalCtx();\n      CACHED_FACTORIES.put(hostAndPort, factory);\n      return factory.createSocket(host, port);\n    }\n    return defaultSocketFactory.createSocket(host, port);\n  }\n\n  /// FOLLOWING METHODS WON'T BE USED DURING JMX INTERACTION, IMPLEMENTING THEM JUST FOR CONSISTENCY ->>>>>\n\n  @Override\n  public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {\n    if (threadLocalContextSet()) {\n      return createFactoryFromThreadLocalCtx().createSocket(s, host, port, autoClose);\n    }\n    return defaultSocketFactory.createSocket(s, host, port, autoClose);\n  }\n\n  @Override\n  public Socket createSocket(String host, int port, InetAddress localHost, int localPort)\n      throws IOException, UnknownHostException {\n    if (threadLocalContextSet()) {\n      return createFactoryFromThreadLocalCtx().createSocket(host, port, localHost, localPort);\n    }\n    return defaultSocketFactory.createSocket(host, port, localHost, localPort);\n  }\n\n  @Override\n  public Socket createSocket(InetAddress host, int port) throws IOException {\n    if (threadLocalContextSet()) {\n      return createFactoryFromThreadLocalCtx().createSocket(host, port);\n    }\n    return defaultSocketFactory.createSocket(host, port);\n  }\n\n  @Override\n  public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)\n      throws IOException {\n    if (threadLocalContextSet()) {\n      return createFactoryFromThreadLocalCtx().createSocket(address, port, localAddress, localPort);\n    }\n    return defaultSocketFactory.createSocket(address, port, localAddress, localPort);\n  }\n\n  @Override\n  public String[] getDefaultCipherSuites() {\n    if (threadLocalContextSet()) {\n      return createFactoryFromThreadLocalCtx().getDefaultCipherSuites();\n    }\n    return defaultSocketFactory.getDefaultCipherSuites();\n  }\n\n  @Override\n  public String[] getSupportedCipherSuites() {\n    if (threadLocalContextSet()) {\n      return createFactoryFromThreadLocalCtx().getSupportedCipherSuites();\n    }\n    return defaultSocketFactory.getSupportedCipherSuites();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/MetricsCollector.java",
    "content": "package com.provectus.kafka.ui.service.metrics;\n\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.Metrics;\nimport com.provectus.kafka.ui.model.MetricsConfig;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.kafka.common.Node;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.util.function.Tuple2;\nimport reactor.util.function.Tuples;\n\n@Component\n@Slf4j\n@RequiredArgsConstructor\npublic class MetricsCollector {\n\n  private final JmxMetricsRetriever jmxMetricsRetriever;\n  private final PrometheusMetricsRetriever prometheusMetricsRetriever;\n\n  public Mono<Metrics> getBrokerMetrics(KafkaCluster cluster, Collection<Node> nodes) {\n    return Flux.fromIterable(nodes)\n        .flatMap(n -> getMetrics(cluster, n).map(lst -> Tuples.of(n, lst)))\n        .collectMap(Tuple2::getT1, Tuple2::getT2)\n        .map(nodeMetrics -> collectMetrics(cluster, nodeMetrics))\n        .defaultIfEmpty(Metrics.empty());\n  }\n\n  private Mono<List<RawMetric>> getMetrics(KafkaCluster kafkaCluster, Node node) {\n    Flux<RawMetric> metricFlux = Flux.empty();\n    if (kafkaCluster.getMetricsConfig() != null) {\n      String type = kafkaCluster.getMetricsConfig().getType();\n      if (type == null || type.equalsIgnoreCase(MetricsConfig.JMX_METRICS_TYPE)) {\n        metricFlux = jmxMetricsRetriever.retrieve(kafkaCluster, node);\n      } else if (type.equalsIgnoreCase(MetricsConfig.PROMETHEUS_METRICS_TYPE)) {\n        metricFlux = prometheusMetricsRetriever.retrieve(kafkaCluster, node);\n      }\n    }\n    return metricFlux.collectList();\n  }\n\n  public Metrics collectMetrics(KafkaCluster cluster, Map<Node, List<RawMetric>> perBrokerMetrics) {\n    Metrics.MetricsBuilder builder = Metrics.builder()\n        .perBrokerMetrics(\n            perBrokerMetrics.entrySet()\n                .stream()\n                .collect(Collectors.toMap(e -> e.getKey().id(), Map.Entry::getValue)));\n\n    populateWellknowMetrics(cluster, perBrokerMetrics)\n        .apply(builder);\n\n    return builder.build();\n  }\n\n  private WellKnownMetrics populateWellknowMetrics(KafkaCluster cluster, Map<Node, List<RawMetric>> perBrokerMetrics) {\n    WellKnownMetrics wellKnownMetrics = new WellKnownMetrics();\n    perBrokerMetrics.forEach((node, metrics) ->\n        metrics.forEach(metric ->\n            wellKnownMetrics.populate(node, metric)));\n    return wellKnownMetrics;\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/MetricsRetriever.java",
    "content": "package com.provectus.kafka.ui.service.metrics;\n\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport org.apache.kafka.common.Node;\nimport reactor.core.publisher.Flux;\n\ninterface MetricsRetriever {\n  Flux<RawMetric> retrieve(KafkaCluster c, Node node);\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/PrometheusEndpointMetricsParser.java",
    "content": "package com.provectus.kafka.ui.service.metrics;\n\nimport java.math.BigDecimal;\nimport java.util.Arrays;\nimport java.util.Optional;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.math.NumberUtils;\n\n@Slf4j\nclass PrometheusEndpointMetricsParser {\n\n  /**\n   * Matches openmetrics format. For example, string:\n   * kafka_server_BrokerTopicMetrics_FiveMinuteRate{name=\"BytesInPerSec\",topic=\"__consumer_offsets\",} 16.94886650744339\n   * will produce:\n   * name=kafka_server_BrokerTopicMetrics_FiveMinuteRate\n   * value=16.94886650744339\n   * labels={name=\"BytesInPerSec\", topic=\"__consumer_offsets\"}\",\n   */\n  private static final Pattern PATTERN = Pattern.compile(\n      \"(?<metricName>^\\\\w+)([ \\t]*\\\\{*(?<properties>.*)}*)[ \\\\t]+(?<value>[\\\\d]+\\\\.?[\\\\d]+)?\");\n\n  static Optional<RawMetric> parse(String s) {\n    Matcher matcher = PATTERN.matcher(s);\n    if (matcher.matches()) {\n      String value = matcher.group(\"value\");\n      String metricName = matcher.group(\"metricName\");\n      if (metricName == null || !NumberUtils.isCreatable(value)) {\n        return Optional.empty();\n      }\n      var labels = Arrays.stream(matcher.group(\"properties\").split(\",\"))\n          .filter(str -> !\"\".equals(str))\n          .map(str -> str.split(\"=\"))\n          .filter(spit -> spit.length == 2)\n          .collect(Collectors.toUnmodifiableMap(\n              str -> str[0].trim(),\n              str -> str[1].trim().replace(\"\\\"\", \"\")));\n\n      return Optional.of(RawMetric.create(metricName, labels, new BigDecimal(value)));\n    }\n    return Optional.empty();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/PrometheusMetricsRetriever.java",
    "content": "package com.provectus.kafka.ui.service.metrics;\n\nimport com.google.common.annotations.VisibleForTesting;\nimport com.google.common.base.Strings;\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.MetricsConfig;\nimport com.provectus.kafka.ui.util.WebClientConfigurator;\nimport java.util.Arrays;\nimport java.util.Optional;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.kafka.common.Node;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.unit.DataSize;\nimport org.springframework.web.reactive.function.client.WebClient;\nimport org.springframework.web.util.UriComponentsBuilder;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\n@Service\n@Slf4j\nclass PrometheusMetricsRetriever implements MetricsRetriever {\n\n  private static final String METRICS_ENDPOINT_PATH = \"/metrics\";\n  private static final int DEFAULT_EXPORTER_PORT = 11001;\n\n  @Override\n  public Flux<RawMetric> retrieve(KafkaCluster c, Node node) {\n    log.debug(\"Retrieving metrics from prometheus exporter: {}:{}\", node.host(), c.getMetricsConfig().getPort());\n\n    MetricsConfig metricsConfig = c.getMetricsConfig();\n    var webClient = new WebClientConfigurator()\n        .configureBufferSize(DataSize.ofMegabytes(20))\n        .configureBasicAuth(metricsConfig.getUsername(), metricsConfig.getPassword())\n        .configureSsl(\n            c.getOriginalProperties().getSsl(),\n            new ClustersProperties.KeystoreConfig(\n                metricsConfig.getKeystoreLocation(),\n                metricsConfig.getKeystorePassword()))\n        .build();\n\n    return retrieve(webClient, node.host(), c.getMetricsConfig());\n  }\n\n  @VisibleForTesting\n  Flux<RawMetric> retrieve(WebClient webClient, String host, MetricsConfig metricsConfig) {\n    int port = Optional.ofNullable(metricsConfig.getPort()).orElse(DEFAULT_EXPORTER_PORT);\n    boolean sslEnabled = metricsConfig.isSsl() || metricsConfig.getKeystoreLocation() != null;\n    var request = webClient.get()\n        .uri(UriComponentsBuilder.newInstance()\n            .scheme(sslEnabled ? \"https\" : \"http\")\n            .host(host)\n            .port(port)\n            .path(METRICS_ENDPOINT_PATH).build().toUri());\n\n    WebClient.ResponseSpec responseSpec = request.retrieve();\n    return responseSpec.bodyToMono(String.class)\n        .doOnError(e -> log.error(\"Error while getting metrics from {}\", host, e))\n        .onErrorResume(th -> Mono.empty())\n        .flatMapMany(body ->\n            Flux.fromStream(\n                Arrays.stream(body.split(\"\\\\n\"))\n                    .filter(str -> !Strings.isNullOrEmpty(str) && !str.startsWith(\"#\")) // skipping comments strings\n                    .map(PrometheusEndpointMetricsParser::parse)\n                    .filter(Optional::isPresent)\n                    .map(Optional::get)\n            )\n        );\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/RawMetric.java",
    "content": "package com.provectus.kafka.ui.service.metrics;\n\nimport java.math.BigDecimal;\nimport java.util.Map;\nimport lombok.AllArgsConstructor;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\n\npublic interface RawMetric {\n\n  String name();\n\n  Map<String, String> labels();\n\n  BigDecimal value();\n\n  // Key, that can be used for metrics reductions\n  default Object identityKey() {\n    return name() + \"_\" + labels();\n  }\n\n  RawMetric copyWithValue(BigDecimal newValue);\n\n  //--------------------------------------------------\n\n  static RawMetric create(String name, Map<String, String> labels, BigDecimal value) {\n    return new SimpleMetric(name, labels, value);\n  }\n\n  @AllArgsConstructor\n  @EqualsAndHashCode\n  @ToString\n  class SimpleMetric implements RawMetric {\n\n    private final String name;\n    private final Map<String, String> labels;\n    private final BigDecimal value;\n\n    @Override\n    public String name() {\n      return name;\n    }\n\n    @Override\n    public Map<String, String> labels() {\n      return labels;\n    }\n\n    @Override\n    public BigDecimal value() {\n      return value;\n    }\n\n    @Override\n    public RawMetric copyWithValue(BigDecimal newValue) {\n      return new SimpleMetric(name, labels, newValue);\n    }\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/WellKnownMetrics.java",
    "content": "package com.provectus.kafka.ui.service.metrics;\n\nimport static org.apache.commons.lang3.StringUtils.containsIgnoreCase;\nimport static org.apache.commons.lang3.StringUtils.endsWithIgnoreCase;\n\nimport com.provectus.kafka.ui.model.Metrics;\nimport java.math.BigDecimal;\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.apache.kafka.common.Node;\n\nclass WellKnownMetrics {\n\n  private static final String BROKER_TOPIC_METRICS = \"BrokerTopicMetrics\";\n  private static final String FIFTEEN_MINUTE_RATE = \"FifteenMinuteRate\";\n\n  // per broker\n  final Map<Integer, BigDecimal> brokerBytesInFifteenMinuteRate = new HashMap<>();\n  final Map<Integer, BigDecimal> brokerBytesOutFifteenMinuteRate = new HashMap<>();\n\n  // per topic\n  final Map<String, BigDecimal> bytesInFifteenMinuteRate = new HashMap<>();\n  final Map<String, BigDecimal> bytesOutFifteenMinuteRate = new HashMap<>();\n\n  void populate(Node node, RawMetric rawMetric) {\n    updateBrokerIOrates(node, rawMetric);\n    updateTopicsIOrates(rawMetric);\n  }\n\n  void apply(Metrics.MetricsBuilder metricsBuilder) {\n    metricsBuilder.topicBytesInPerSec(bytesInFifteenMinuteRate);\n    metricsBuilder.topicBytesOutPerSec(bytesOutFifteenMinuteRate);\n    metricsBuilder.brokerBytesInPerSec(brokerBytesInFifteenMinuteRate);\n    metricsBuilder.brokerBytesOutPerSec(brokerBytesOutFifteenMinuteRate);\n  }\n\n  private void updateBrokerIOrates(Node node, RawMetric rawMetric) {\n    String name = rawMetric.name();\n    if (!brokerBytesInFifteenMinuteRate.containsKey(node.id())\n        && rawMetric.labels().size() == 1\n        && \"BytesInPerSec\".equalsIgnoreCase(rawMetric.labels().get(\"name\"))\n        && containsIgnoreCase(name, BROKER_TOPIC_METRICS)\n        && endsWithIgnoreCase(name, FIFTEEN_MINUTE_RATE)) {\n      brokerBytesInFifteenMinuteRate.put(node.id(),  rawMetric.value());\n    }\n    if (!brokerBytesOutFifteenMinuteRate.containsKey(node.id())\n        && rawMetric.labels().size() == 1\n        && \"BytesOutPerSec\".equalsIgnoreCase(rawMetric.labels().get(\"name\"))\n        && containsIgnoreCase(name, BROKER_TOPIC_METRICS)\n        && endsWithIgnoreCase(name, FIFTEEN_MINUTE_RATE)) {\n      brokerBytesOutFifteenMinuteRate.put(node.id(), rawMetric.value());\n    }\n  }\n\n  private void updateTopicsIOrates(RawMetric rawMetric) {\n    String name = rawMetric.name();\n    String topic = rawMetric.labels().get(\"topic\");\n    if (topic != null\n        && containsIgnoreCase(name, BROKER_TOPIC_METRICS)\n        && endsWithIgnoreCase(name, FIFTEEN_MINUTE_RATE)) {\n      String nameProperty = rawMetric.labels().get(\"name\");\n      if (\"BytesInPerSec\".equalsIgnoreCase(nameProperty)) {\n        bytesInFifteenMinuteRate.compute(topic, (k, v) -> v == null ? rawMetric.value() : v.add(rawMetric.value()));\n      } else if (\"BytesOutPerSec\".equalsIgnoreCase(nameProperty)) {\n        bytesOutFifteenMinuteRate.compute(topic, (k, v) -> v == null ? rawMetric.value() : v.add(rawMetric.value()));\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AbstractProviderCondition.java",
    "content": "package com.provectus.kafka.ui.service.rbac;\n\nimport com.provectus.kafka.ui.config.auth.OAuthProperties;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.boot.context.properties.bind.Bindable;\nimport org.springframework.boot.context.properties.bind.Binder;\nimport org.springframework.core.env.Environment;\n\npublic abstract class AbstractProviderCondition {\n  private static final Bindable<Map<String, OAuthProperties.OAuth2Provider>> OAUTH2_PROPERTIES = Bindable\n      .mapOf(String.class, OAuthProperties.OAuth2Provider.class);\n\n  protected Set<String> getRegisteredProvidersTypes(final Environment env) {\n    final Map<String, OAuthProperties.OAuth2Provider> properties = Binder.get(env)\n        .bind(\"auth.oauth2.client\", OAUTH2_PROPERTIES)\n        .orElse(Map.of());\n    return properties.values().stream()\n        .map(OAuthProperties.OAuth2Provider::getCustomParams)\n        .filter(Objects::nonNull)\n        .filter(Predicate.not(Map::isEmpty))\n        .map(params -> params.get(\"type\"))\n        .filter(Objects::nonNull)\n        .filter(StringUtils::isNotEmpty)\n        .collect(Collectors.toSet());\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java",
    "content": "package com.provectus.kafka.ui.service.rbac;\n\nimport static com.provectus.kafka.ui.model.rbac.Resource.APPLICATIONCONFIG;\n\nimport com.provectus.kafka.ui.config.auth.AuthenticatedUser;\nimport com.provectus.kafka.ui.config.auth.RbacUser;\nimport com.provectus.kafka.ui.config.auth.RoleBasedAccessControlProperties;\nimport com.provectus.kafka.ui.model.ClusterDTO;\nimport com.provectus.kafka.ui.model.ConnectDTO;\nimport com.provectus.kafka.ui.model.InternalTopic;\nimport com.provectus.kafka.ui.model.rbac.AccessContext;\nimport com.provectus.kafka.ui.model.rbac.Permission;\nimport com.provectus.kafka.ui.model.rbac.Resource;\nimport com.provectus.kafka.ui.model.rbac.Role;\nimport com.provectus.kafka.ui.model.rbac.Subject;\nimport com.provectus.kafka.ui.model.rbac.permission.ConnectAction;\nimport com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction;\nimport com.provectus.kafka.ui.model.rbac.permission.SchemaAction;\nimport com.provectus.kafka.ui.model.rbac.permission.TopicAction;\nimport com.provectus.kafka.ui.service.rbac.extractor.CognitoAuthorityExtractor;\nimport com.provectus.kafka.ui.service.rbac.extractor.GithubAuthorityExtractor;\nimport com.provectus.kafka.ui.service.rbac.extractor.GoogleAuthorityExtractor;\nimport com.provectus.kafka.ui.service.rbac.extractor.OauthAuthorityExtractor;\nimport com.provectus.kafka.ui.service.rbac.extractor.ProviderAuthorityExtractor;\nimport jakarta.annotation.PostConstruct;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.function.Predicate;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\nimport javax.annotation.Nullable;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.collections4.CollectionUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.core.env.Environment;\nimport org.springframework.security.access.AccessDeniedException;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.Assert;\nimport reactor.core.publisher.Mono;\n\n@Service\n@RequiredArgsConstructor\n@EnableConfigurationProperties(RoleBasedAccessControlProperties.class)\n@Slf4j\npublic class AccessControlService {\n\n  private static final String ACCESS_DENIED = \"Access denied\";\n  private static final String ACTIONS_ARE_EMPTY = \"actions are empty\";\n\n  @Nullable\n  private final InMemoryReactiveClientRegistrationRepository clientRegistrationRepository;\n  private final RoleBasedAccessControlProperties properties;\n  private final Environment environment;\n\n  private boolean rbacEnabled = false;\n  private Set<ProviderAuthorityExtractor> oauthExtractors = Collections.emptySet();\n\n  @PostConstruct\n  public void init() {\n    if (CollectionUtils.isEmpty(properties.getRoles())) {\n      log.trace(\"No roles provided, disabling RBAC\");\n      return;\n    }\n    rbacEnabled = true;\n\n    this.oauthExtractors = properties.getRoles()\n        .stream()\n        .map(role -> role.getSubjects()\n            .stream()\n            .map(Subject::getProvider)\n            .distinct()\n            .map(provider -> switch (provider) {\n              case OAUTH_COGNITO -> new CognitoAuthorityExtractor();\n              case OAUTH_GOOGLE -> new GoogleAuthorityExtractor();\n              case OAUTH_GITHUB -> new GithubAuthorityExtractor();\n              case OAUTH -> new OauthAuthorityExtractor();\n              default -> null;\n            })\n            .filter(Objects::nonNull)\n            .collect(Collectors.toSet()))\n        .flatMap(Set::stream)\n        .collect(Collectors.toSet());\n\n    if (!properties.getRoles().isEmpty()\n        && \"oauth2\".equalsIgnoreCase(environment.getProperty(\"auth.type\"))\n        && (clientRegistrationRepository == null || !clientRegistrationRepository.iterator().hasNext())) {\n      log.error(\"Roles are configured but no authentication methods are present. Authentication might fail.\");\n    }\n  }\n\n  public Mono<Void> validateAccess(AccessContext context) {\n    if (!rbacEnabled) {\n      return Mono.empty();\n    }\n\n    if (CollectionUtils.isNotEmpty(context.getApplicationConfigActions())) {\n      return getUser()\n          .doOnNext(user -> {\n            boolean accessGranted = isApplicationConfigAccessible(context, user);\n\n            if (!accessGranted) {\n              throw new AccessDeniedException(ACCESS_DENIED);\n            }\n          }).then();\n    }\n\n    return getUser()\n        .doOnNext(user -> {\n          boolean accessGranted =\n              isApplicationConfigAccessible(context, user)\n                  && isClusterAccessible(context, user)\n                  && isClusterConfigAccessible(context, user)\n                  && isTopicAccessible(context, user)\n                  && isConsumerGroupAccessible(context, user)\n                  && isConnectAccessible(context, user)\n                  && isConnectorAccessible(context, user) // TODO connector selectors\n                  && isSchemaAccessible(context, user)\n                  && isKsqlAccessible(context, user)\n                  && isAclAccessible(context, user)\n                  && isAuditAccessible(context, user);\n\n          if (!accessGranted) {\n            throw new AccessDeniedException(ACCESS_DENIED);\n          }\n        })\n        .then();\n  }\n\n  public Mono<AuthenticatedUser> getUser() {\n    return ReactiveSecurityContextHolder.getContext()\n        .map(SecurityContext::getAuthentication)\n        .filter(authentication -> authentication.getPrincipal() instanceof RbacUser)\n        .map(authentication -> ((RbacUser) authentication.getPrincipal()))\n        .map(user -> new AuthenticatedUser(user.name(), user.groups()));\n  }\n\n  public boolean isApplicationConfigAccessible(AccessContext context, AuthenticatedUser user) {\n    if (!rbacEnabled) {\n      return true;\n    }\n    if (CollectionUtils.isEmpty(context.getApplicationConfigActions())) {\n      return true;\n    }\n    Set<String> requiredActions = context.getApplicationConfigActions()\n        .stream()\n        .map(a -> a.toString().toUpperCase())\n        .collect(Collectors.toSet());\n    return isAccessible(APPLICATIONCONFIG, null, user, context, requiredActions);\n  }\n\n  private boolean isClusterAccessible(AccessContext context, AuthenticatedUser user) {\n    if (!rbacEnabled) {\n      return true;\n    }\n\n    Assert.isTrue(StringUtils.isNotEmpty(context.getCluster()), \"cluster value is empty\");\n\n    return properties.getRoles()\n        .stream()\n        .filter(filterRole(user))\n        .anyMatch(filterCluster(context.getCluster()));\n  }\n\n  public Mono<Boolean> isClusterAccessible(ClusterDTO cluster) {\n    if (!rbacEnabled) {\n      return Mono.just(true);\n    }\n\n    AccessContext accessContext = AccessContext\n        .builder()\n        .cluster(cluster.getName())\n        .build();\n\n    return getUser().map(u -> isClusterAccessible(accessContext, u));\n  }\n\n  public boolean isClusterConfigAccessible(AccessContext context, AuthenticatedUser user) {\n    if (!rbacEnabled) {\n      return true;\n    }\n\n    if (CollectionUtils.isEmpty(context.getClusterConfigActions())) {\n      return true;\n    }\n    Assert.isTrue(StringUtils.isNotEmpty(context.getCluster()), \"cluster value is empty\");\n\n    Set<String> requiredActions = context.getClusterConfigActions()\n        .stream()\n        .map(a -> a.toString().toUpperCase())\n        .collect(Collectors.toSet());\n\n    return isAccessible(Resource.CLUSTERCONFIG, context.getCluster(), user, context, requiredActions);\n  }\n\n  public boolean isTopicAccessible(AccessContext context, AuthenticatedUser user) {\n    if (!rbacEnabled) {\n      return true;\n    }\n\n    if (context.getTopic() == null && context.getTopicActions().isEmpty()) {\n      return true;\n    }\n    Assert.isTrue(!context.getTopicActions().isEmpty(), ACTIONS_ARE_EMPTY);\n\n    Set<String> requiredActions = context.getTopicActions()\n        .stream()\n        .map(a -> a.toString().toUpperCase())\n        .collect(Collectors.toSet());\n\n    return isAccessible(Resource.TOPIC, context.getTopic(), user, context, requiredActions);\n  }\n\n  public Mono<List<InternalTopic>> filterViewableTopics(List<InternalTopic> topics, String clusterName) {\n    if (!rbacEnabled) {\n      return Mono.just(topics);\n    }\n\n    return getUser()\n        .map(user -> topics.stream()\n            .filter(topic -> {\n                  var accessContext = AccessContext\n                      .builder()\n                      .cluster(clusterName)\n                      .topic(topic.getName())\n                      .topicActions(TopicAction.VIEW)\n                      .build();\n                  return isTopicAccessible(accessContext, user);\n                }\n            ).toList());\n  }\n\n  private boolean isConsumerGroupAccessible(AccessContext context, AuthenticatedUser user) {\n    if (!rbacEnabled) {\n      return true;\n    }\n\n    if (context.getConsumerGroup() == null && context.getConsumerGroupActions().isEmpty()) {\n      return true;\n    }\n    Assert.isTrue(!context.getConsumerGroupActions().isEmpty(), ACTIONS_ARE_EMPTY);\n\n    Set<String> requiredActions = context.getConsumerGroupActions()\n        .stream()\n        .map(a -> a.toString().toUpperCase())\n        .collect(Collectors.toSet());\n\n    return isAccessible(Resource.CONSUMER, context.getConsumerGroup(), user, context, requiredActions);\n  }\n\n  public Mono<Boolean> isConsumerGroupAccessible(String groupId, String clusterName) {\n    if (!rbacEnabled) {\n      return Mono.just(true);\n    }\n\n    AccessContext accessContext = AccessContext\n        .builder()\n        .cluster(clusterName)\n        .consumerGroup(groupId)\n        .consumerGroupActions(ConsumerGroupAction.VIEW)\n        .build();\n\n    return getUser().map(u -> isConsumerGroupAccessible(accessContext, u));\n  }\n\n  public boolean isSchemaAccessible(AccessContext context, AuthenticatedUser user) {\n    if (!rbacEnabled) {\n      return true;\n    }\n\n    if (context.getSchema() == null && context.getSchemaActions().isEmpty()) {\n      return true;\n    }\n    Assert.isTrue(!context.getSchemaActions().isEmpty(), ACTIONS_ARE_EMPTY);\n\n    Set<String> requiredActions = context.getSchemaActions()\n        .stream()\n        .map(a -> a.toString().toUpperCase())\n        .collect(Collectors.toSet());\n\n    return isAccessible(Resource.SCHEMA, context.getSchema(), user, context, requiredActions);\n  }\n\n  public Mono<Boolean> isSchemaAccessible(String schema, String clusterName) {\n    if (!rbacEnabled) {\n      return Mono.just(true);\n    }\n\n    AccessContext accessContext = AccessContext\n        .builder()\n        .cluster(clusterName)\n        .schema(schema)\n        .schemaActions(SchemaAction.VIEW)\n        .build();\n\n    return getUser().map(u -> isSchemaAccessible(accessContext, u));\n  }\n\n  public boolean isConnectAccessible(AccessContext context, AuthenticatedUser user) {\n    if (!rbacEnabled) {\n      return true;\n    }\n\n    if (context.getConnect() == null && context.getConnectActions().isEmpty()) {\n      return true;\n    }\n    Assert.isTrue(!context.getConnectActions().isEmpty(), ACTIONS_ARE_EMPTY);\n\n    Set<String> requiredActions = context.getConnectActions()\n        .stream()\n        .map(a -> a.toString().toUpperCase())\n        .collect(Collectors.toSet());\n\n    return isAccessible(Resource.CONNECT, context.getConnect(), user, context, requiredActions);\n  }\n\n  public Mono<Boolean> isConnectAccessible(ConnectDTO dto, String clusterName) {\n    if (!rbacEnabled) {\n      return Mono.just(true);\n    }\n\n    return isConnectAccessible(dto.getName(), clusterName);\n  }\n\n  public Mono<Boolean> isConnectAccessible(String connectName, String clusterName) {\n    if (!rbacEnabled) {\n      return Mono.just(true);\n    }\n\n    AccessContext accessContext = AccessContext\n        .builder()\n        .cluster(clusterName)\n        .connect(connectName)\n        .connectActions(ConnectAction.VIEW)\n        .build();\n\n    return getUser().map(u -> isConnectAccessible(accessContext, u));\n  }\n\n  public boolean isConnectorAccessible(AccessContext context, AuthenticatedUser user) {\n    if (!rbacEnabled) {\n      return true;\n    }\n\n    return isConnectAccessible(context, user);\n  }\n\n  public Mono<Boolean> isConnectorAccessible(String connectName, String connectorName, String clusterName) {\n    if (!rbacEnabled) {\n      return Mono.just(true);\n    }\n\n    AccessContext accessContext = AccessContext\n        .builder()\n        .cluster(clusterName)\n        .connect(connectName)\n        .connectActions(ConnectAction.VIEW)\n        .connector(connectorName)\n        .build();\n\n    return getUser().map(u -> isConnectorAccessible(accessContext, u));\n  }\n\n  private boolean isKsqlAccessible(AccessContext context, AuthenticatedUser user) {\n    if (!rbacEnabled) {\n      return true;\n    }\n\n    if (context.getKsqlActions().isEmpty()) {\n      return true;\n    }\n\n    Set<String> requiredActions = context.getKsqlActions()\n        .stream()\n        .map(a -> a.toString().toUpperCase())\n        .collect(Collectors.toSet());\n\n    return isAccessible(Resource.KSQL, null, user, context, requiredActions);\n  }\n\n  private boolean isAclAccessible(AccessContext context, AuthenticatedUser user) {\n    if (!rbacEnabled) {\n      return true;\n    }\n\n    if (context.getAclActions().isEmpty()) {\n      return true;\n    }\n\n    Set<String> requiredActions = context.getAclActions()\n        .stream()\n        .map(a -> a.toString().toUpperCase())\n        .collect(Collectors.toSet());\n\n    return isAccessible(Resource.ACL, null, user, context, requiredActions);\n  }\n\n  private boolean isAuditAccessible(AccessContext context, AuthenticatedUser user) {\n    if (!rbacEnabled) {\n      return true;\n    }\n\n    if (context.getAuditAction().isEmpty()) {\n      return true;\n    }\n\n    Set<String> requiredActions = context.getAuditAction()\n        .stream()\n        .map(a -> a.toString().toUpperCase())\n        .collect(Collectors.toSet());\n\n    return isAccessible(Resource.AUDIT, null, user, context, requiredActions);\n  }\n\n  public Set<ProviderAuthorityExtractor> getOauthExtractors() {\n    return oauthExtractors;\n  }\n\n  public List<Role> getRoles() {\n    if (!rbacEnabled) {\n      return Collections.emptyList();\n    }\n    return Collections.unmodifiableList(properties.getRoles());\n  }\n\n  private boolean isAccessible(Resource resource, @Nullable String resourceValue,\n                               AuthenticatedUser user, AccessContext context, Set<String> requiredActions) {\n    Set<String> grantedActions = properties.getRoles()\n        .stream()\n        .filter(filterRole(user))\n        .filter(filterCluster(resource, context.getCluster()))\n        .flatMap(grantedRole -> grantedRole.getPermissions().stream())\n        .filter(filterResource(resource))\n        .filter(filterResourceValue(resourceValue))\n        .flatMap(grantedPermission -> grantedPermission.getActions().stream())\n        .map(String::toUpperCase)\n        .collect(Collectors.toSet());\n\n    return grantedActions.containsAll(requiredActions);\n  }\n\n  private Predicate<Role> filterRole(AuthenticatedUser user) {\n    return role -> user.groups().contains(role.getName());\n  }\n\n  private Predicate<Role> filterCluster(String cluster) {\n    return grantedRole -> grantedRole.getClusters()\n        .stream()\n        .anyMatch(cluster::equalsIgnoreCase);\n  }\n\n  private Predicate<Role> filterCluster(Resource resource, String cluster) {\n    if (resource == APPLICATIONCONFIG) {\n      return role -> true;\n    }\n    return filterCluster(cluster);\n  }\n\n  private Predicate<Permission> filterResource(Resource resource) {\n    return grantedPermission -> resource == grantedPermission.getResource();\n  }\n\n  private Predicate<Permission> filterResourceValue(@Nullable String resourceValue) {\n\n    if (resourceValue == null) {\n      return grantedPermission -> true;\n    }\n    return grantedPermission -> {\n      Pattern valuePattern = grantedPermission.getCompiledValuePattern();\n      if (valuePattern == null) {\n        return true;\n      }\n      return valuePattern.matcher(resourceValue).matches();\n    };\n  }\n\n  public boolean isRbacEnabled() {\n    return rbacEnabled;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/CognitoAuthorityExtractor.java",
    "content": "package com.provectus.kafka.ui.service.rbac.extractor;\n\nimport static com.provectus.kafka.ui.model.rbac.provider.Provider.Name.COGNITO;\n\nimport com.google.common.collect.Sets;\nimport com.provectus.kafka.ui.model.rbac.Role;\nimport com.provectus.kafka.ui.model.rbac.provider.Provider;\nimport com.provectus.kafka.ui.service.rbac.AccessControlService;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.oauth2.core.user.DefaultOAuth2User;\nimport reactor.core.publisher.Mono;\n\n@Slf4j\npublic class CognitoAuthorityExtractor implements ProviderAuthorityExtractor {\n\n  private static final String COGNITO_GROUPS_ATTRIBUTE_NAME = \"cognito:groups\";\n\n  @Override\n  public boolean isApplicable(String provider, Map<String, String> customParams) {\n    return COGNITO.equalsIgnoreCase(provider) || COGNITO.equalsIgnoreCase(customParams.get(TYPE));\n  }\n\n  @Override\n  public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams) {\n    log.debug(\"Extracting cognito user authorities\");\n\n    DefaultOAuth2User principal;\n    try {\n      principal = (DefaultOAuth2User) value;\n    } catch (ClassCastException e) {\n      log.error(\"Can't cast value to DefaultOAuth2User\", e);\n      throw new RuntimeException();\n    }\n\n    Set<String> groupsByUsername = acs.getRoles()\n        .stream()\n        .filter(r -> r.getSubjects()\n            .stream()\n            .filter(s -> s.getProvider().equals(Provider.OAUTH_COGNITO))\n            .filter(s -> s.getType().equals(\"user\"))\n            .anyMatch(s -> s.getValue().equals(principal.getName())))\n        .map(Role::getName)\n        .collect(Collectors.toSet());\n\n    List<String> groups = principal.getAttribute(COGNITO_GROUPS_ATTRIBUTE_NAME);\n    if (groups == null) {\n      log.debug(\"Cognito groups param is not present\");\n      return Mono.just(groupsByUsername);\n    }\n\n    Set<String> groupsByGroups = acs.getRoles()\n        .stream()\n        .filter(role -> role.getSubjects()\n            .stream()\n            .filter(s -> s.getProvider().equals(Provider.OAUTH_COGNITO))\n            .filter(s -> s.getType().equals(\"group\"))\n            .anyMatch(subject -> groups\n                .stream()\n                .anyMatch(cognitoGroup -> cognitoGroup.equals(subject.getValue()))\n            ))\n        .map(Role::getName)\n        .collect(Collectors.toSet());\n\n    return Mono.just(Sets.union(groupsByUsername, groupsByGroups));\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GithubAuthorityExtractor.java",
    "content": "package com.provectus.kafka.ui.service.rbac.extractor;\n\nimport static com.provectus.kafka.ui.model.rbac.provider.Provider.Name.GITHUB;\n\nimport com.provectus.kafka.ui.model.rbac.Role;\nimport com.provectus.kafka.ui.model.rbac.provider.Provider;\nimport com.provectus.kafka.ui.service.rbac.AccessControlService;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.core.ParameterizedTypeReference;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.security.config.oauth2.client.CommonOAuth2Provider;\nimport org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;\nimport org.springframework.security.oauth2.core.user.DefaultOAuth2User;\nimport org.springframework.web.reactive.function.client.WebClient;\nimport reactor.core.publisher.Mono;\n\n@Slf4j\npublic class GithubAuthorityExtractor implements ProviderAuthorityExtractor {\n\n  private static final String ORGANIZATION_ATTRIBUTE_NAME = \"organizations_url\";\n  private static final String USERNAME_ATTRIBUTE_NAME = \"login\";\n  private static final String ORGANIZATION_NAME = \"login\";\n  private static final String ORGANIZATION = \"organization\";\n  private static final String TEAM_NAME = \"slug\";\n  private static final String GITHUB_ACCEPT_HEADER = \"application/vnd.github+json\";\n  private static final String DUMMY = \"dummy\";\n  // The number of results (max 100) per page of list organizations for authenticated user.\n  private static final Integer ORGANIZATIONS_PER_PAGE = 100;\n\n  @Override\n  public boolean isApplicable(String provider, Map<String, String> customParams) {\n    return GITHUB.equalsIgnoreCase(provider) || GITHUB.equalsIgnoreCase(customParams.get(TYPE));\n  }\n\n  @Override\n  public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams) {\n    DefaultOAuth2User principal;\n    try {\n      principal = (DefaultOAuth2User) value;\n    } catch (ClassCastException e) {\n      log.error(\"Can't cast value to DefaultOAuth2User\", e);\n      throw new RuntimeException();\n    }\n\n    Set<String> rolesByUsername = new HashSet<>();\n    String username = principal.getAttribute(USERNAME_ATTRIBUTE_NAME);\n    if (username == null) {\n      log.debug(\"Github username param is not present\");\n    } else {\n      acs.getRoles()\n          .stream()\n          .filter(r -> r.getSubjects()\n              .stream()\n              .filter(s -> s.getProvider().equals(Provider.OAUTH_GITHUB))\n              .filter(s -> s.getType().equals(\"user\"))\n              .anyMatch(s -> s.getValue().equals(username)))\n          .map(Role::getName)\n          .forEach(rolesByUsername::add);\n    }\n\n    OAuth2UserRequest req = (OAuth2UserRequest) additionalParams.get(\"request\");\n    String infoEndpoint = req.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri();\n\n    if (infoEndpoint == null) {\n      infoEndpoint = CommonOAuth2Provider.GITHUB\n          .getBuilder(DUMMY)\n          .clientId(DUMMY)\n          .build()\n          .getProviderDetails()\n          .getUserInfoEndpoint()\n          .getUri();\n    }\n    var webClient = WebClient.create(infoEndpoint);\n\n    Mono<Set<String>> rolesByOrganization = getOrganizationRoles(principal, additionalParams, acs, webClient);\n    Mono<Set<String>> rolesByTeams = getTeamRoles(webClient, additionalParams, acs);\n\n    return Mono.zip(rolesByOrganization, rolesByTeams)\n        .map((t) -> Stream.of(t.getT1(), t.getT2(), rolesByUsername)\n            .flatMap(Collection::stream)\n            .collect(Collectors.toSet()));\n  }\n\n  private Mono<Set<String>> getOrganizationRoles(DefaultOAuth2User principal, Map<String, Object> additionalParams,\n                                                 AccessControlService acs, WebClient webClient) {\n    String organization = principal.getAttribute(ORGANIZATION_ATTRIBUTE_NAME);\n    if (organization == null) {\n      log.debug(\"Github organization param is not present\");\n      return Mono.just(Collections.emptySet());\n    }\n\n    final Mono<List<Map<String, Object>>> userOrganizations = webClient\n        .get()\n        .uri(uriBuilder -> uriBuilder.path(\"/orgs\")\n            .queryParam(\"per_page\", ORGANIZATIONS_PER_PAGE)\n            .build())\n        .headers(headers -> {\n          headers.set(HttpHeaders.ACCEPT, GITHUB_ACCEPT_HEADER);\n          OAuth2UserRequest request = (OAuth2UserRequest) additionalParams.get(\"request\");\n          headers.setBearerAuth(request.getAccessToken().getTokenValue());\n        })\n        .retrieve()\n        //@formatter:off\n        .bodyToMono(new ParameterizedTypeReference<>() {});\n    //@formatter:on\n\n    return userOrganizations\n        .map(orgsMap -> acs.getRoles()\n            .stream()\n            .filter(role -> role.getSubjects()\n                .stream()\n                .filter(s -> s.getProvider().equals(Provider.OAUTH_GITHUB))\n                .filter(s -> s.getType().equals(ORGANIZATION))\n                .anyMatch(subject -> orgsMap.stream()\n                    .map(org -> org.get(ORGANIZATION_NAME).toString())\n                    .anyMatch(orgName -> orgName.equalsIgnoreCase(subject.getValue()))\n                ))\n            .map(Role::getName)\n            .collect(Collectors.toSet()));\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  private Mono<Set<String>> getTeamRoles(WebClient webClient, Map<String, Object> additionalParams,\n                                         AccessControlService acs) {\n\n    var requestedTeams = acs.getRoles()\n        .stream()\n        .filter(r -> r.getSubjects()\n            .stream()\n            .filter(s -> s.getProvider().equals(Provider.OAUTH_GITHUB))\n            .anyMatch(s -> s.getType().equals(\"team\")))\n        .collect(Collectors.toSet());\n\n    if (requestedTeams.isEmpty()) {\n      log.debug(\"No roles with github teams found, skipping\");\n      return Mono.just(Collections.emptySet());\n    }\n\n    final Mono<List<Map<String, Object>>> rawTeams = webClient\n        .get()\n        .uri(uriBuilder -> uriBuilder.path(\"/teams\")\n            .queryParam(\"per_page\", ORGANIZATIONS_PER_PAGE)\n            .build())\n        .headers(headers -> {\n          headers.set(HttpHeaders.ACCEPT, GITHUB_ACCEPT_HEADER);\n          OAuth2UserRequest request = (OAuth2UserRequest) additionalParams.get(\"request\");\n          headers.setBearerAuth(request.getAccessToken().getTokenValue());\n        })\n        .retrieve()\n        //@formatter:off\n        .bodyToMono(new ParameterizedTypeReference<>() {});\n    //@formatter:on\n\n    final Mono<List<String>> mappedTeams = rawTeams\n        .map(teams -> teams.stream()\n            .map(teamInfo -> {\n              var name = teamInfo.get(TEAM_NAME);\n              var orgInfo = (Map<String, Object>) teamInfo.get(ORGANIZATION);\n              var orgName = orgInfo.get(ORGANIZATION_NAME);\n              return orgName + \"/\" + name;\n            })\n            .map(Object::toString)\n            .collect(Collectors.toList())\n        );\n\n    return mappedTeams\n        .map(teams -> acs.getRoles()\n            .stream()\n            .filter(role -> role.getSubjects()\n                .stream()\n                .filter(s -> s.getProvider().equals(Provider.OAUTH_GITHUB))\n                .filter(s -> s.getType().equals(\"team\"))\n                .anyMatch(subject -> teams.stream()\n                    .anyMatch(teamName -> teamName.equalsIgnoreCase(subject.getValue()))\n                ))\n            .map(Role::getName)\n            .collect(Collectors.toSet()));\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GoogleAuthorityExtractor.java",
    "content": "package com.provectus.kafka.ui.service.rbac.extractor;\n\nimport static com.provectus.kafka.ui.model.rbac.provider.Provider.Name.GOOGLE;\n\nimport com.google.common.collect.Sets;\nimport com.provectus.kafka.ui.model.rbac.Role;\nimport com.provectus.kafka.ui.model.rbac.provider.Provider;\nimport com.provectus.kafka.ui.service.rbac.AccessControlService;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.oauth2.core.user.DefaultOAuth2User;\nimport reactor.core.publisher.Mono;\n\n@Slf4j\npublic class GoogleAuthorityExtractor implements ProviderAuthorityExtractor {\n\n  private static final String GOOGLE_DOMAIN_ATTRIBUTE_NAME = \"hd\";\n  public static final String EMAIL_ATTRIBUTE_NAME = \"email\";\n\n  @Override\n  public boolean isApplicable(String provider, Map<String, String> customParams) {\n    return GOOGLE.equalsIgnoreCase(provider) || GOOGLE.equalsIgnoreCase(customParams.get(TYPE));\n  }\n\n  @Override\n  public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams) {\n    log.debug(\"Extracting google user authorities\");\n\n    DefaultOAuth2User principal;\n    try {\n      principal = (DefaultOAuth2User) value;\n    } catch (ClassCastException e) {\n      log.error(\"Can't cast value to DefaultOAuth2User\", e);\n      throw new RuntimeException();\n    }\n\n    Set<String> groupsByUsername = acs.getRoles()\n        .stream()\n        .filter(r -> r.getSubjects()\n            .stream()\n            .filter(s -> s.getProvider().equals(Provider.OAUTH_GOOGLE))\n            .filter(s -> s.getType().equals(\"user\"))\n            .anyMatch(s -> s.getValue().equals(principal.getAttribute(EMAIL_ATTRIBUTE_NAME))))\n        .map(Role::getName)\n        .collect(Collectors.toSet());\n\n\n    String domain = principal.getAttribute(GOOGLE_DOMAIN_ATTRIBUTE_NAME);\n    if (domain == null) {\n      log.debug(\"Google domain param is not present\");\n      return Mono.just(groupsByUsername);\n    }\n\n    Set<String> groupsByDomain = acs.getRoles()\n        .stream()\n        .filter(r -> r.getSubjects()\n            .stream()\n            .filter(s -> s.getProvider().equals(Provider.OAUTH_GOOGLE))\n            .filter(s -> s.getType().equals(\"domain\"))\n            .anyMatch(s -> s.getValue().equals(domain)))\n        .map(Role::getName)\n        .collect(Collectors.toSet());\n\n    return Mono.just(Sets.union(groupsByUsername, groupsByDomain));\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/OauthAuthorityExtractor.java",
    "content": "package com.provectus.kafka.ui.service.rbac.extractor;\n\nimport static com.provectus.kafka.ui.model.rbac.provider.Provider.Name.OAUTH;\n\nimport com.google.common.collect.Sets;\nimport com.provectus.kafka.ui.config.auth.OAuthProperties;\nimport com.provectus.kafka.ui.model.rbac.Role;\nimport com.provectus.kafka.ui.model.rbac.provider.Provider;\nimport com.provectus.kafka.ui.service.rbac.AccessControlService;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.oauth2.core.user.DefaultOAuth2User;\nimport org.springframework.util.Assert;\nimport reactor.core.publisher.Mono;\n\n@Slf4j\npublic class OauthAuthorityExtractor implements ProviderAuthorityExtractor {\n\n  public static final String ROLES_FIELD_PARAM_NAME = \"roles-field\";\n\n  @Override\n  public boolean isApplicable(String provider, Map<String, String> customParams) {\n    var containsRolesFieldNameParam = customParams.containsKey(ROLES_FIELD_PARAM_NAME);\n    if (!containsRolesFieldNameParam) {\n      log.debug(\"Provider [{}] doesn't contain a roles field param name, mapping won't be performed\", provider);\n      return false;\n    }\n\n    return OAUTH.equalsIgnoreCase(provider) || OAUTH.equalsIgnoreCase(customParams.get(TYPE));\n  }\n\n  @Override\n  public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams) {\n    log.trace(\"Extracting OAuth2 user authorities\");\n\n    DefaultOAuth2User principal;\n    try {\n      principal = (DefaultOAuth2User) value;\n    } catch (ClassCastException e) {\n      log.error(\"Can't cast value to DefaultOAuth2User\", e);\n      throw new RuntimeException();\n    }\n\n    var provider = (OAuthProperties.OAuth2Provider) additionalParams.get(\"provider\");\n    Assert.notNull(provider, \"provider is null\");\n    var rolesFieldName = provider.getCustomParams().get(ROLES_FIELD_PARAM_NAME);\n\n    Set<String> rolesByUsername = acs.getRoles()\n        .stream()\n        .filter(r -> r.getSubjects()\n            .stream()\n            .filter(s -> s.getProvider().equals(Provider.OAUTH))\n            .filter(s -> s.getType().equals(\"user\"))\n            .anyMatch(s -> s.getValue().equals(principal.getName())))\n        .map(Role::getName)\n        .collect(Collectors.toSet());\n\n    Set<String> rolesByRolesField = acs.getRoles()\n        .stream()\n        .filter(role -> role.getSubjects()\n            .stream()\n            .filter(s -> s.getProvider().equals(Provider.OAUTH))\n            .filter(s -> s.getType().equals(\"role\"))\n            .anyMatch(subject -> {\n              var roleName = subject.getValue();\n              var principalRoles = convertRoles(principal.getAttribute(rolesFieldName));\n              var roleMatched = principalRoles.contains(roleName);\n\n              if (roleMatched) {\n                log.debug(\"Assigning role [{}] to user [{}]\", roleName, principal.getName());\n              } else {\n                log.trace(\"Role [{}] not found in user [{}] roles\", roleName, principal.getName());\n              }\n\n              return roleMatched;\n            })\n        )\n        .map(Role::getName)\n        .collect(Collectors.toSet());\n\n    return Mono.just(Sets.union(rolesByUsername, rolesByRolesField));\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  private Collection<String> convertRoles(Object roles) {\n    if (roles == null) {\n      log.debug(\"Param missing from attributes, skipping\");\n      return Collections.emptySet();\n    }\n\n    if ((roles instanceof List<?>) || (roles instanceof Set<?>)) {\n      log.trace(\"The field is either a set or a list, returning as is\");\n      return (Collection<String>) roles;\n    }\n\n    if (!(roles instanceof String)) {\n      log.debug(\"The field is not a string, skipping\");\n      return Collections.emptySet();\n    }\n\n    log.trace(\"Trying to deserialize the field value [{}] as a string\", roles);\n\n    return Arrays.stream(((String) roles).split(\",\"))\n        .collect(Collectors.toSet());\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/ProviderAuthorityExtractor.java",
    "content": "package com.provectus.kafka.ui.service.rbac.extractor;\n\nimport com.provectus.kafka.ui.service.rbac.AccessControlService;\nimport java.util.Map;\nimport java.util.Set;\nimport reactor.core.publisher.Mono;\n\npublic interface ProviderAuthorityExtractor {\n\n  String TYPE = \"type\";\n\n  boolean isApplicable(String provider, Map<String, String> customParams);\n\n  Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams);\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java",
    "content": "package com.provectus.kafka.ui.service.rbac.extractor;\n\nimport com.provectus.kafka.ui.config.auth.LdapProperties;\nimport com.provectus.kafka.ui.model.rbac.Role;\nimport com.provectus.kafka.ui.model.rbac.provider.Provider;\nimport com.provectus.kafka.ui.service.rbac.AccessControlService;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.ldap.core.DirContextOperations;\nimport org.springframework.ldap.core.support.BaseLdapPathContextSource;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.authority.SimpleGrantedAuthority;\nimport org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;\nimport org.springframework.util.Assert;\n\n@Slf4j\npublic class RbacLdapAuthoritiesExtractor extends DefaultLdapAuthoritiesPopulator {\n\n  private final AccessControlService acs;\n  private final LdapProperties props;\n\n  public RbacLdapAuthoritiesExtractor(ApplicationContext context,\n                                      BaseLdapPathContextSource contextSource, String groupFilterSearchBase) {\n    super(contextSource, groupFilterSearchBase);\n    this.acs = context.getBean(AccessControlService.class);\n    this.props = context.getBean(LdapProperties.class);\n  }\n\n  @Override\n  protected Set<GrantedAuthority> getAdditionalRoles(DirContextOperations user, String username) {\n    var ldapGroups = getRoles(user.getNameInNamespace(), username);\n\n    return acs.getRoles()\n        .stream()\n        .filter(r -> r.getSubjects()\n            .stream()\n            .filter(subject -> subject.getProvider().equals(Provider.LDAP))\n            .filter(subject -> subject.getType().equals(\"group\"))\n            .anyMatch(subject -> ldapGroups.contains(subject.getValue()))\n        )\n        .map(Role::getName)\n        .peek(role -> log.trace(\"Mapped role [{}] for user [{}]\", role, username))\n        .map(SimpleGrantedAuthority::new)\n        .collect(Collectors.toSet());\n  }\n\n  private Set<String> getRoles(String userDn, String username) {\n    var groupSearchBase = props.getGroupFilterSearchBase();\n    Assert.notNull(groupSearchBase, \"groupSearchBase is empty\");\n\n    var groupRoleAttribute = props.getGroupRoleAttribute();\n    if (groupRoleAttribute == null) {\n\n      groupRoleAttribute = \"cn\";\n    }\n\n    log.trace(\n        \"Searching for roles for user [{}] with DN [{}], groupRoleAttribute [{}] and filter [{}] in search base [{}]\",\n        username, userDn, groupRoleAttribute, getGroupSearchFilter(), groupSearchBase);\n\n    var ldapTemplate = getLdapTemplate();\n    ldapTemplate.setIgnoreNameNotFoundException(true);\n\n    Set<Map<String, List<String>>> userRoles = ldapTemplate.searchForMultipleAttributeValues(\n        groupSearchBase, getGroupSearchFilter(), new String[] {userDn, username},\n        new String[] {groupRoleAttribute});\n\n    return userRoles.stream()\n        .map(record -> record.get(getGroupRoleAttribute()).get(0))\n        .peek(group -> log.trace(\"Found LDAP group [{}] for user [{}]\", group, username))\n        .collect(Collectors.toSet());\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ApplicationMetrics.java",
    "content": "package com.provectus.kafka.ui.util;\n\nimport static lombok.AccessLevel.PRIVATE;\n\nimport com.google.common.annotations.VisibleForTesting;\nimport com.provectus.kafka.ui.emitter.PolledRecords;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport io.micrometer.core.instrument.Counter;\nimport io.micrometer.core.instrument.DistributionSummary;\nimport io.micrometer.core.instrument.Gauge;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport io.micrometer.core.instrument.Metrics;\nimport io.micrometer.core.instrument.Timer;\nimport io.micrometer.core.instrument.simple.SimpleMeterRegistry;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport lombok.RequiredArgsConstructor;\n\n@RequiredArgsConstructor(access = PRIVATE)\npublic class ApplicationMetrics {\n\n  // kafka-ui specific metrics prefix. Added to make it easier to distinguish kui metrics from\n  // other metrics, exposed by spring boot (like http stats, jvm, etc.)\n  private static final String COMMON_PREFIX = \"kui_\";\n\n  private final String clusterName;\n  private final MeterRegistry registry;\n\n  public static ApplicationMetrics forCluster(KafkaCluster cluster) {\n    return new ApplicationMetrics(cluster.getName(), Metrics.globalRegistry);\n  }\n\n  @VisibleForTesting\n  public static ApplicationMetrics noop() {\n    return new ApplicationMetrics(\"noop\", new SimpleMeterRegistry());\n  }\n\n  public void meterPolledRecords(String topic, PolledRecords polled, boolean throttled) {\n    pollTimer(topic).record(polled.elapsed());\n    polledRecords(topic).increment(polled.count());\n    polledBytes(topic).record(polled.bytes());\n    if (throttled) {\n      pollThrottlingActivations().increment();\n    }\n  }\n\n  private Counter polledRecords(String topic) {\n    return Counter.builder(COMMON_PREFIX + \"topic_records_polled\")\n        .description(\"Number of records polled from topic\")\n        .tag(\"cluster\", clusterName)\n        .tag(\"topic\", topic)\n        .register(registry);\n  }\n\n  private DistributionSummary polledBytes(String topic) {\n    return DistributionSummary.builder(COMMON_PREFIX + \"topic_polled_bytes\")\n        .description(\"Bytes polled from kafka topic\")\n        .tag(\"cluster\", clusterName)\n        .tag(\"topic\", topic)\n        .register(registry);\n  }\n\n  private Timer pollTimer(String topic) {\n    return Timer.builder(COMMON_PREFIX + \"topic_poll_time\")\n        .description(\"Time spend in polling for topic\")\n        .tag(\"cluster\", clusterName)\n        .tag(\"topic\", topic)\n        .register(registry);\n  }\n\n  private Counter pollThrottlingActivations() {\n    return Counter.builder(COMMON_PREFIX + \"poll_throttling_activations\")\n        .description(\"Number of poll throttling activations\")\n        .tag(\"cluster\", clusterName)\n        .register(registry);\n  }\n\n  public AtomicInteger activeConsumers() {\n    var count = new AtomicInteger();\n    Gauge.builder(COMMON_PREFIX + \"active_consumers\", () -> count)\n        .description(\"Number of active consumers\")\n        .tag(\"cluster\", clusterName)\n        .register(registry);\n    return count;\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ApplicationRestarter.java",
    "content": "package com.provectus.kafka.ui.util;\n\nimport com.provectus.kafka.ui.KafkaUiApplication;\nimport java.io.Closeable;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.boot.context.event.ApplicationStartedEvent;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.ApplicationListener;\nimport org.springframework.stereotype.Component;\n\n@Slf4j\n@Component\npublic class ApplicationRestarter implements ApplicationListener<ApplicationStartedEvent> {\n\n  private String[] applicationArgs;\n  private ApplicationContext applicationContext;\n\n  @Override\n  public void onApplicationEvent(ApplicationStartedEvent event) {\n    this.applicationArgs = event.getArgs();\n    this.applicationContext = event.getApplicationContext();\n  }\n\n  public void requestRestart() {\n    log.info(\"Restarting application\");\n    Thread thread = new Thread(() -> {\n      closeApplicationContext(applicationContext);\n      KafkaUiApplication.startApplication(applicationArgs);\n    });\n    thread.setName(\"restartedMain-\" + System.currentTimeMillis());\n    thread.setDaemon(false);\n    thread.start();\n  }\n\n  private void closeApplicationContext(ApplicationContext context) {\n    while (context instanceof Closeable) {\n      try {\n        ((Closeable) context).close();\n      } catch (Exception e) {\n        log.warn(\"Error stopping application before restart\", e);\n        throw new RuntimeException(e);\n      }\n      context = context.getParent();\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/DynamicConfigOperations.java",
    "content": "package com.provectus.kafka.ui.util;\n\n\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.config.WebclientProperties;\nimport com.provectus.kafka.ui.config.auth.OAuthProperties;\nimport com.provectus.kafka.ui.config.auth.RoleBasedAccessControlProperties;\nimport com.provectus.kafka.ui.exception.FileUploadException;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardOpenOption;\nimport java.time.Instant;\nimport java.util.Optional;\nimport javax.annotation.Nullable;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\nimport lombok.SneakyThrows;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.NoSuchBeanDefinitionException;\nimport org.springframework.boot.env.YamlPropertySourceLoader;\nimport org.springframework.context.ApplicationContextInitializer;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.springframework.core.env.CompositePropertySource;\nimport org.springframework.core.env.PropertySource;\nimport org.springframework.core.io.FileSystemResource;\nimport org.springframework.http.codec.multipart.FilePart;\nimport org.springframework.stereotype.Component;\nimport org.yaml.snakeyaml.DumperOptions;\nimport org.yaml.snakeyaml.Yaml;\nimport org.yaml.snakeyaml.introspector.BeanAccess;\nimport org.yaml.snakeyaml.introspector.Property;\nimport org.yaml.snakeyaml.introspector.PropertyUtils;\nimport org.yaml.snakeyaml.nodes.NodeTuple;\nimport org.yaml.snakeyaml.nodes.Tag;\nimport org.yaml.snakeyaml.representer.Representer;\nimport reactor.core.publisher.Mono;\n\n@Slf4j\n@RequiredArgsConstructor\n@Component\npublic class DynamicConfigOperations {\n\n  static final String DYNAMIC_CONFIG_ENABLED_ENV_PROPERTY = \"dynamic.config.enabled\";\n  static final String FILTERING_GROOVY_ENABLED_PROPERTY = \"filtering.groovy.enabled\";\n  static final String DYNAMIC_CONFIG_PATH_ENV_PROPERTY = \"dynamic.config.path\";\n  static final String DYNAMIC_CONFIG_PATH_ENV_PROPERTY_DEFAULT = \"/etc/kafkaui/dynamic_config.yaml\";\n\n  static final String CONFIG_RELATED_UPLOADS_DIR_PROPERTY = \"config.related.uploads.dir\";\n  static final String CONFIG_RELATED_UPLOADS_DIR_DEFAULT = \"/etc/kafkaui/uploads\";\n\n  public static ApplicationContextInitializer<ConfigurableApplicationContext> dynamicConfigPropertiesInitializer() {\n    return appCtx ->\n        new DynamicConfigOperations(appCtx)\n            .loadDynamicPropertySource()\n            .ifPresent(source -> appCtx.getEnvironment().getPropertySources().addFirst(source));\n  }\n\n  private final ConfigurableApplicationContext ctx;\n\n  public boolean dynamicConfigEnabled() {\n    return \"true\".equalsIgnoreCase(ctx.getEnvironment().getProperty(DYNAMIC_CONFIG_ENABLED_ENV_PROPERTY));\n  }\n\n  public boolean filteringGroovyEnabled() {\n    return \"true\".equalsIgnoreCase(ctx.getEnvironment().getProperty(FILTERING_GROOVY_ENABLED_PROPERTY));\n  }\n\n  private Path dynamicConfigFilePath() {\n    return Paths.get(\n        Optional.ofNullable(ctx.getEnvironment().getProperty(DYNAMIC_CONFIG_PATH_ENV_PROPERTY))\n            .orElse(DYNAMIC_CONFIG_PATH_ENV_PROPERTY_DEFAULT)\n    );\n  }\n\n  @SneakyThrows\n  public Optional<PropertySource<?>> loadDynamicPropertySource() {\n    if (dynamicConfigEnabled()) {\n      Path configPath = dynamicConfigFilePath();\n      if (!Files.exists(configPath) || !Files.isReadable(configPath)) {\n        log.warn(\"Dynamic config file {} doesnt exist or not readable\", configPath);\n        return Optional.empty();\n      }\n      var propertySource = new CompositePropertySource(\"dynamicProperties\");\n      new YamlPropertySourceLoader()\n          .load(\"dynamicProperties\", new FileSystemResource(configPath))\n          .forEach(propertySource::addPropertySource);\n      log.info(\"Dynamic config loaded from {}\", configPath);\n      return Optional.of(propertySource);\n    }\n    return Optional.empty();\n  }\n\n  public PropertiesStructure getCurrentProperties() {\n    checkIfDynamicConfigEnabled();\n    return PropertiesStructure.builder()\n        .kafka(getNullableBean(ClustersProperties.class))\n        .rbac(getNullableBean(RoleBasedAccessControlProperties.class))\n        .auth(\n            PropertiesStructure.Auth.builder()\n                .type(ctx.getEnvironment().getProperty(\"auth.type\"))\n                .oauth2(getNullableBean(OAuthProperties.class))\n                .build())\n        .webclient(getNullableBean(WebclientProperties.class))\n        .build();\n  }\n\n  @Nullable\n  private <T> T getNullableBean(Class<T> clazz) {\n    try {\n      return ctx.getBean(clazz);\n    } catch (NoSuchBeanDefinitionException nsbde) {\n      return null;\n    }\n  }\n\n  public void persist(PropertiesStructure properties) {\n    checkIfDynamicConfigEnabled();\n    properties.initAndValidate();\n\n    String yaml = serializeToYaml(properties);\n    writeYamlToFile(yaml, dynamicConfigFilePath());\n  }\n\n  public Mono<Path> uploadConfigRelatedFile(FilePart file) {\n    checkIfDynamicConfigEnabled();\n    String targetDirStr = ctx.getEnvironment()\n        .getProperty(CONFIG_RELATED_UPLOADS_DIR_PROPERTY, CONFIG_RELATED_UPLOADS_DIR_DEFAULT);\n\n    Path targetDir = Path.of(targetDirStr);\n    if (!Files.exists(targetDir)) {\n      try {\n        Files.createDirectories(targetDir);\n      } catch (IOException e) {\n        return Mono.error(\n            new FileUploadException(\"Error creating directory for uploads %s\".formatted(targetDir), e));\n      }\n    }\n\n    Path targetFilePath = targetDir.resolve(file.filename() + \"-\" + Instant.now().getEpochSecond());\n    log.info(\"Uploading config-related file {}\", targetFilePath);\n    if (Files.exists(targetFilePath)) {\n      log.info(\"File {} already exists, it will be overwritten\", targetFilePath);\n    }\n\n    return file.transferTo(targetFilePath)\n        .thenReturn(targetFilePath)\n        .doOnError(th -> log.error(\"Error uploading file {}\", targetFilePath, th))\n        .onErrorMap(th -> new FileUploadException(targetFilePath, th));\n  }\n\n  public void checkIfFilteringGroovyEnabled() {\n    if (!filteringGroovyEnabled()) {\n      throw new ValidationException(\n              \"Groovy filters is not allowed. \"\n                      + \"Set filtering.groovy.enabled property to 'true' to enabled it.\");\n    }\n  }\n\n  private void checkIfDynamicConfigEnabled() {\n    if (!dynamicConfigEnabled()) {\n      throw new ValidationException(\n          \"Dynamic config change is not allowed. \"\n              + \"Set dynamic.config.enabled property to 'true' to enabled it.\");\n    }\n  }\n\n  @SneakyThrows\n  private void writeYamlToFile(String yaml, Path path) {\n    if (Files.isDirectory(path)) {\n      throw new ValidationException(\"Dynamic file path is a directory, but should be a file path\");\n    }\n    if (!Files.exists(path.getParent())) {\n      Files.createDirectories(path.getParent());\n    }\n    if (Files.exists(path) && !Files.isWritable(path)) {\n      throw new ValidationException(\"File already exists and is not writable\");\n    }\n    try {\n      Files.writeString(\n          path,\n          yaml,\n          StandardOpenOption.CREATE,\n          StandardOpenOption.WRITE,\n          StandardOpenOption.TRUNCATE_EXISTING // to override existing file\n      );\n    } catch (IOException e) {\n      throw new ValidationException(\"Error writing to \" + path, e);\n    }\n  }\n\n  private String serializeToYaml(PropertiesStructure props) {\n    //representer, that skips fields with null values\n    Representer representer = new Representer(new DumperOptions()) {\n      @Override\n      protected NodeTuple representJavaBeanProperty(Object javaBean,\n                                                    Property property,\n                                                    Object propertyValue,\n                                                    Tag customTag) {\n        if (propertyValue == null) {\n          return null; // if value of property is null, ignore it.\n        } else {\n          return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag);\n        }\n      }\n    };\n    var propertyUtils = new PropertyUtils();\n    propertyUtils.setBeanAccess(BeanAccess.FIELD);\n    representer.setPropertyUtils(propertyUtils);\n    representer.addClassTag(PropertiesStructure.class, Tag.MAP); //to avoid adding class tag\n    representer.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); //use indent instead of {}\n    return new Yaml(representer).dump(props);\n  }\n\n  ///---------------------------------------------------------------------\n\n  @Data\n  @Builder\n  // field name should be in sync with @ConfigurationProperties annotation\n  public static class PropertiesStructure {\n\n    private ClustersProperties kafka;\n    private RoleBasedAccessControlProperties rbac;\n    private Auth auth;\n    private WebclientProperties webclient;\n\n    @Data\n    @Builder\n    public static class Auth {\n      String type;\n      OAuthProperties oauth2;\n    }\n\n    public void initAndValidate() {\n      Optional.ofNullable(kafka)\n          .ifPresent(ClustersProperties::validateAndSetDefaults);\n\n      Optional.ofNullable(rbac)\n          .ifPresent(RoleBasedAccessControlProperties::init);\n\n      Optional.ofNullable(auth)\n          .flatMap(a -> Optional.ofNullable(a.oauth2))\n          .ifPresent(OAuthProperties::init);\n\n      Optional.ofNullable(webclient)\n          .ifPresent(WebclientProperties::validate);\n    }\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/EmptyRedirectStrategy.java",
    "content": "package com.provectus.kafka.ui.util;\n\nimport java.net.URI;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.server.reactive.ServerHttpResponse;\nimport org.springframework.security.web.server.ServerRedirectStrategy;\nimport org.springframework.util.Assert;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\npublic class EmptyRedirectStrategy implements ServerRedirectStrategy {\n\n  private HttpStatus httpStatus = HttpStatus.FOUND;\n\n  private boolean contextRelative = true;\n\n  public Mono<Void> sendRedirect(ServerWebExchange exchange, URI location) {\n    Assert.notNull(exchange, \"exchange cannot be null\");\n    Assert.notNull(location, \"location cannot be null\");\n    return Mono.fromRunnable(() -> {\n      ServerHttpResponse response = exchange.getResponse();\n      response.setStatusCode(this.httpStatus);\n      response.getHeaders().setLocation(createLocation(exchange, location));\n    });\n  }\n\n  private URI createLocation(ServerWebExchange exchange, URI location) {\n    if (!this.contextRelative) {\n      return location;\n    }\n\n    String url = location.getPath().isEmpty() ? \"/\"\n        : location.toASCIIString();\n\n    if (url.startsWith(\"/\")) {\n      String context = exchange.getRequest().getPath().contextPath().value();\n      return URI.create(context + url);\n    }\n    return location;\n  }\n\n  public void setHttpStatus(HttpStatus httpStatus) {\n    Assert.notNull(httpStatus, \"httpStatus cannot be null\");\n    this.httpStatus = httpStatus;\n  }\n\n  public void setContextRelative(boolean contextRelative) {\n    this.contextRelative = contextRelative;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/GithubReleaseInfo.java",
    "content": "package com.provectus.kafka.ui.util;\n\nimport com.google.common.annotations.VisibleForTesting;\nimport java.time.Duration;\nimport lombok.extern.slf4j.Slf4j;\nimport reactor.core.publisher.Mono;\n\n@Slf4j\npublic class GithubReleaseInfo {\n\n  private static final String GITHUB_LATEST_RELEASE_RETRIEVAL_URL =\n      \"https://api.github.com/repos/provectus/kafka-ui/releases/latest\";\n\n  private static final Duration GITHUB_API_MAX_WAIT_TIME = Duration.ofSeconds(2);\n\n  public record GithubReleaseDto(String html_url, String tag_name, String published_at) {\n\n    static GithubReleaseDto empty() {\n      return new GithubReleaseDto(null, null, null);\n    }\n  }\n\n  private volatile GithubReleaseDto release = GithubReleaseDto.empty();\n\n  private final Mono<Void> refreshMono;\n\n  public GithubReleaseInfo() {\n    this(GITHUB_LATEST_RELEASE_RETRIEVAL_URL);\n  }\n\n  @VisibleForTesting\n  GithubReleaseInfo(String url) {\n    this.refreshMono = new WebClientConfigurator().build()\n        .get()\n        .uri(url)\n        .exchangeToMono(resp -> resp.bodyToMono(GithubReleaseDto.class))\n        .timeout(GITHUB_API_MAX_WAIT_TIME)\n        .doOnError(th -> log.trace(\"Error getting latest github release info\", th))\n        .onErrorResume(th -> true, th -> Mono.just(GithubReleaseDto.empty()))\n        .doOnNext(release -> this.release = release)\n        .then();\n  }\n\n  public GithubReleaseDto get() {\n    return release;\n  }\n\n  public Mono<Void> refresh() {\n    return refreshMono;\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/KafkaServicesValidation.java",
    "content": "package com.provectus.kafka.ui.util;\n\nimport static com.provectus.kafka.ui.config.ClustersProperties.TruststoreConfig;\n\nimport com.provectus.kafka.ui.connect.api.KafkaConnectClientApi;\nimport com.provectus.kafka.ui.model.ApplicationPropertyValidationDTO;\nimport com.provectus.kafka.ui.service.ReactiveAdminClient;\nimport com.provectus.kafka.ui.service.ksql.KsqlApiClient;\nimport com.provectus.kafka.ui.sr.api.KafkaSrClientApi;\nimport java.io.FileInputStream;\nimport java.security.KeyStore;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Properties;\nimport java.util.function.Supplier;\nimport javax.annotation.Nullable;\nimport javax.net.ssl.TrustManagerFactory;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.kafka.clients.admin.AdminClient;\nimport org.apache.kafka.clients.admin.AdminClientConfig;\nimport org.springframework.util.ResourceUtils;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\n@Slf4j\npublic final class KafkaServicesValidation {\n\n  private KafkaServicesValidation() {\n  }\n\n  private static Mono<ApplicationPropertyValidationDTO> valid() {\n    return Mono.just(new ApplicationPropertyValidationDTO().error(false));\n  }\n\n  private static Mono<ApplicationPropertyValidationDTO> invalid(String errorMsg) {\n    return Mono.just(new ApplicationPropertyValidationDTO().error(true).errorMessage(errorMsg));\n  }\n\n  private static Mono<ApplicationPropertyValidationDTO> invalid(Throwable th) {\n    return Mono.just(new ApplicationPropertyValidationDTO().error(true).errorMessage(th.getMessage()));\n  }\n\n  /**\n   * Returns error msg, if any.\n   */\n  public static Optional<String> validateTruststore(TruststoreConfig truststoreConfig) {\n    if (truststoreConfig.getTruststoreLocation() != null && truststoreConfig.getTruststorePassword() != null) {\n      try (FileInputStream fileInputStream = new FileInputStream(\n             (ResourceUtils.getFile(truststoreConfig.getTruststoreLocation())))) {\n        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());\n        trustStore.load(fileInputStream, truststoreConfig.getTruststorePassword().toCharArray());\n        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(\n            TrustManagerFactory.getDefaultAlgorithm()\n        );\n        trustManagerFactory.init(trustStore);\n      } catch (Exception e) {\n        return Optional.of(e.getMessage());\n      }\n    }\n    return Optional.empty();\n  }\n\n  public static Mono<ApplicationPropertyValidationDTO> validateClusterConnection(String bootstrapServers,\n                                                                                 Properties clusterProps,\n                                                                                 @Nullable\n                                                                                 TruststoreConfig ssl) {\n    Properties properties = new Properties();\n    SslPropertiesUtil.addKafkaSslProperties(ssl, properties);\n    properties.putAll(clusterProps);\n    properties.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);\n    // editing properties to make validation faster\n    properties.put(AdminClientConfig.RETRIES_CONFIG, 1);\n    properties.put(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, 5_000);\n    properties.put(AdminClientConfig.DEFAULT_API_TIMEOUT_MS_CONFIG, 5_000);\n    properties.put(AdminClientConfig.CLIENT_ID_CONFIG, \"kui-admin-client-validation-\" + System.currentTimeMillis());\n    AdminClient adminClient = null;\n    try {\n      adminClient = AdminClient.create(properties);\n    } catch (Exception e) {\n      log.error(\"Error creating admin client during validation\", e);\n      return invalid(\"Error while creating AdminClient. See logs for details.\");\n    }\n    return Mono.just(adminClient)\n        .then(ReactiveAdminClient.toMono(adminClient.listTopics().names()))\n        .then(valid())\n        .doOnTerminate(adminClient::close)\n        .onErrorResume(th -> {\n          log.error(\"Error connecting to cluster\", th);\n          return KafkaServicesValidation.invalid(\"Error connecting to cluster. See logs for details.\");\n        });\n  }\n\n  public static Mono<ApplicationPropertyValidationDTO> validateSchemaRegistry(\n      Supplier<ReactiveFailover<KafkaSrClientApi>> clientSupplier) {\n    ReactiveFailover<KafkaSrClientApi> client;\n    try {\n      client = clientSupplier.get();\n    } catch (Exception e) {\n      log.error(\"Error creating Schema Registry client\", e);\n      return invalid(\"Error creating Schema Registry client: \" + e.getMessage());\n    }\n    return client\n        .mono(KafkaSrClientApi::getGlobalCompatibilityLevel)\n        .then(valid())\n        .onErrorResume(KafkaServicesValidation::invalid);\n  }\n\n  public static Mono<ApplicationPropertyValidationDTO> validateConnect(\n      Supplier<ReactiveFailover<KafkaConnectClientApi>> clientSupplier) {\n    ReactiveFailover<KafkaConnectClientApi> client;\n    try {\n      client = clientSupplier.get();\n    } catch (Exception e) {\n      log.error(\"Error creating Connect client\", e);\n      return invalid(\"Error creating Connect client: \" + e.getMessage());\n    }\n    return client.flux(KafkaConnectClientApi::getConnectorPlugins)\n        .collectList()\n        .then(valid())\n        .onErrorResume(KafkaServicesValidation::invalid);\n  }\n\n  public static Mono<ApplicationPropertyValidationDTO> validateKsql(\n      Supplier<ReactiveFailover<KsqlApiClient>> clientSupplier) {\n    ReactiveFailover<KsqlApiClient> client;\n    try {\n      client = clientSupplier.get();\n    } catch (Exception e) {\n      log.error(\"Error creating Ksql client\", e);\n      return invalid(\"Error creating Ksql client: \" + e.getMessage());\n    }\n    return client.flux(c -> c.execute(\"SHOW VARIABLES;\", Map.of()))\n        .collectList()\n        .flatMap(ksqlResults ->\n            Flux.fromIterable(ksqlResults)\n                .filter(KsqlApiClient.KsqlResponseTable::isError)\n                .flatMap(err -> invalid(\"Error response from ksql: \" + err))\n                .next()\n                .switchIfEmpty(valid())\n        )\n        .onErrorResume(KafkaServicesValidation::invalid);\n  }\n\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/KafkaVersion.java",
    "content": "package com.provectus.kafka.ui.util;\n\nimport java.util.Optional;\n\npublic final class KafkaVersion {\n\n  private KafkaVersion() {\n  }\n\n  public static Optional<Float> parse(String version) throws NumberFormatException {\n    try {\n      final String[] parts = version.split(\"\\\\.\");\n      if (parts.length > 2) {\n        version = parts[0] + \".\" + parts[1];\n      }\n      return Optional.of(Float.parseFloat(version.split(\"-\")[0]));\n    } catch (Exception e) {\n      return Optional.empty();\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ReactiveFailover.java",
    "content": "package com.provectus.kafka.ui.util;\n\nimport com.google.common.base.Preconditions;\nimport java.io.IOException;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\npublic class ReactiveFailover<T> {\n\n  public static final Duration DEFAULT_RETRY_GRACE_PERIOD_MS = Duration.ofSeconds(5);\n  public static final Predicate<Throwable> CONNECTION_REFUSED_EXCEPTION_FILTER =\n      error -> error.getCause() instanceof IOException && error.getCause().getMessage().contains(\"Connection refused\");\n\n  private final List<PublisherHolder<T>> publishers;\n  private int currentIndex = 0;\n\n  private final Predicate<Throwable> failoverExceptionsPredicate;\n  private final String noAvailablePublishersMsg;\n\n  // creates single-publisher failover (basically for tests usage)\n  public static <T> ReactiveFailover<T> createNoop(T publisher) {\n    return create(\n        List.of(publisher),\n        th -> true,\n        \"publisher is not available\",\n        DEFAULT_RETRY_GRACE_PERIOD_MS\n    );\n  }\n\n  public static <T> ReactiveFailover<T> create(List<T> publishers,\n                                               Predicate<Throwable> failoverExeptionsPredicate,\n                                               String noAvailablePublishersMsg,\n                                               Duration retryGracePeriodMs) {\n    return new ReactiveFailover<>(\n        publishers.stream().map(p -> new PublisherHolder<>(() -> p, retryGracePeriodMs.toMillis())).toList(),\n        failoverExeptionsPredicate,\n        noAvailablePublishersMsg\n    );\n  }\n\n  public static <T, A> ReactiveFailover<T> create(List<A> args,\n                                                  Function<A, T> factory,\n                                                  Predicate<Throwable> failoverExeptionsPredicate,\n                                                  String noAvailablePublishersMsg,\n                                                  Duration retryGracePeriodMs) {\n    return new ReactiveFailover<>(\n        args.stream().map(arg ->\n            new PublisherHolder<>(() -> factory.apply(arg), retryGracePeriodMs.toMillis())).toList(),\n        failoverExeptionsPredicate,\n        noAvailablePublishersMsg\n    );\n  }\n\n  private ReactiveFailover(List<PublisherHolder<T>> publishers,\n                   Predicate<Throwable> failoverExceptionsPredicate,\n                   String noAvailablePublishersMsg) {\n    Preconditions.checkArgument(!publishers.isEmpty());\n    this.publishers = publishers;\n    this.failoverExceptionsPredicate = failoverExceptionsPredicate;\n    this.noAvailablePublishersMsg = noAvailablePublishersMsg;\n  }\n\n  public <V> Mono<V> mono(Function<T, Mono<V>> f) {\n    List<PublisherHolder<T>> candidates = getActivePublishers();\n    if (candidates.isEmpty()) {\n      return Mono.error(() -> new IllegalStateException(noAvailablePublishersMsg));\n    }\n    return mono(f, candidates);\n  }\n\n  private <V> Mono<V> mono(Function<T, Mono<V>> f, List<PublisherHolder<T>> candidates) {\n    var publisher = candidates.get(0);\n    return publisher.get()\n        .flatMap(f)\n        .onErrorResume(failoverExceptionsPredicate, th -> {\n          publisher.markFailed();\n          if (candidates.size() == 1) {\n            return Mono.error(th);\n          }\n          var newCandidates = candidates.stream().skip(1).filter(PublisherHolder::isActive).toList();\n          if (newCandidates.isEmpty()) {\n            return Mono.error(th);\n          }\n          return mono(f, newCandidates);\n        });\n  }\n\n  public <V> Flux<V> flux(Function<T, Flux<V>> f) {\n    List<PublisherHolder<T>> candidates = getActivePublishers();\n    if (candidates.isEmpty()) {\n      return Flux.error(() -> new IllegalStateException(noAvailablePublishersMsg));\n    }\n    return flux(f, candidates);\n  }\n\n  private <V> Flux<V> flux(Function<T, Flux<V>> f, List<PublisherHolder<T>> candidates) {\n    var publisher = candidates.get(0);\n    return publisher.get()\n        .flatMapMany(f)\n        .onErrorResume(failoverExceptionsPredicate, th -> {\n          publisher.markFailed();\n          if (candidates.size() == 1) {\n            return Flux.error(th);\n          }\n          var newCandidates = candidates.stream().skip(1).filter(PublisherHolder::isActive).toList();\n          if (newCandidates.isEmpty()) {\n            return Flux.error(th);\n          }\n          return flux(f, newCandidates);\n        });\n  }\n\n  /**\n   * Returns list of active publishers, starting with latest active.\n   */\n  private synchronized List<PublisherHolder<T>> getActivePublishers() {\n    var result = new ArrayList<PublisherHolder<T>>();\n    for (int i = 0, j = currentIndex; i < publishers.size(); i++) {\n      var publisher = publishers.get(j);\n      if (publisher.isActive()) {\n        result.add(publisher);\n      } else if (currentIndex == j) {\n        currentIndex = ++currentIndex == publishers.size() ? 0 : currentIndex;\n      }\n      j = ++j == publishers.size() ? 0 : j;\n    }\n    return result;\n  }\n\n  static class PublisherHolder<T> {\n\n    private final long retryGracePeriodMs;\n    private final Supplier<T> supplier;\n    private final AtomicLong lastErrorTs = new AtomicLong();\n    private T publisherInstance;\n\n    PublisherHolder(Supplier<T> supplier, long retryGracePeriodMs) {\n      this.supplier = supplier;\n      this.retryGracePeriodMs = retryGracePeriodMs;\n    }\n\n    synchronized Mono<T> get() {\n      if (publisherInstance == null) {\n        try {\n          publisherInstance = supplier.get();\n        } catch (Throwable th) {\n          return Mono.error(th);\n        }\n      }\n      return Mono.just(publisherInstance);\n    }\n\n    void markFailed() {\n      lastErrorTs.set(System.currentTimeMillis());\n    }\n\n    boolean isActive() {\n      return System.currentTimeMillis() - lastErrorTs.get() > retryGracePeriodMs;\n    }\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ResourceUtil.java",
    "content": "package com.provectus.kafka.ui.util;\n\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.io.Reader;\nimport java.nio.charset.StandardCharsets;\nimport org.springframework.core.io.Resource;\nimport org.springframework.util.FileCopyUtils;\n\npublic class ResourceUtil {\n\n  private ResourceUtil() {\n  }\n\n  public static String readAsString(Resource resource) throws IOException {\n    try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) {\n      return FileCopyUtils.copyToString(reader);\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/SslPropertiesUtil.java",
    "content": "package com.provectus.kafka.ui.util;\n\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport java.util.Properties;\nimport javax.annotation.Nullable;\nimport org.apache.kafka.common.config.SslConfigs;\n\npublic final class SslPropertiesUtil {\n\n  private SslPropertiesUtil() {\n  }\n\n  public static void addKafkaSslProperties(@Nullable ClustersProperties.TruststoreConfig truststoreConfig,\n                                           Properties sink) {\n    if (truststoreConfig != null && truststoreConfig.getTruststoreLocation() != null) {\n      sink.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, truststoreConfig.getTruststoreLocation());\n      if (truststoreConfig.getTruststorePassword() != null) {\n        sink.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, truststoreConfig.getTruststorePassword());\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/WebClientConfigurator.java",
    "content": "package com.provectus.kafka.ui.util;\n\nimport com.fasterxml.jackson.databind.DeserializationFeature;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport io.netty.handler.ssl.SslContext;\nimport io.netty.handler.ssl.SslContextBuilder;\nimport java.io.FileInputStream;\nimport java.security.KeyStore;\nimport java.util.function.Consumer;\nimport javax.annotation.Nullable;\nimport javax.net.ssl.KeyManagerFactory;\nimport javax.net.ssl.TrustManagerFactory;\nimport lombok.SneakyThrows;\nimport org.openapitools.jackson.nullable.JsonNullableModule;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.client.reactive.ReactorClientHttpConnector;\nimport org.springframework.http.codec.ClientCodecConfigurer;\nimport org.springframework.http.codec.json.Jackson2JsonDecoder;\nimport org.springframework.http.codec.json.Jackson2JsonEncoder;\nimport org.springframework.util.ResourceUtils;\nimport org.springframework.util.unit.DataSize;\nimport org.springframework.web.reactive.function.client.WebClient;\nimport reactor.netty.http.client.HttpClient;\n\npublic class WebClientConfigurator {\n\n  private final WebClient.Builder builder = WebClient.builder();\n  private HttpClient httpClient = HttpClient\n      .create()\n      .proxyWithSystemProperties();\n\n  public WebClientConfigurator() {\n    configureObjectMapper(defaultOM());\n  }\n\n  private static ObjectMapper defaultOM() {\n    return new ObjectMapper()\n        .registerModule(new JavaTimeModule())\n        .registerModule(new JsonNullableModule())\n        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);\n  }\n\n  public WebClientConfigurator configureSsl(@Nullable ClustersProperties.TruststoreConfig truststoreConfig,\n                                            @Nullable ClustersProperties.KeystoreConfig keystoreConfig) {\n    return configureSsl(\n        keystoreConfig != null ? keystoreConfig.getKeystoreLocation() : null,\n        keystoreConfig != null ? keystoreConfig.getKeystorePassword() : null,\n        truststoreConfig != null ? truststoreConfig.getTruststoreLocation() : null,\n        truststoreConfig != null ? truststoreConfig.getTruststorePassword() : null\n    );\n  }\n\n  @SneakyThrows\n  private WebClientConfigurator configureSsl(\n      @Nullable String keystoreLocation,\n      @Nullable String keystorePassword,\n      @Nullable String truststoreLocation,\n      @Nullable String truststorePassword) {\n    if (truststoreLocation == null && keystoreLocation == null) {\n      return this;\n    }\n\n    SslContextBuilder contextBuilder = SslContextBuilder.forClient();\n    if (truststoreLocation != null && truststorePassword != null) {\n      KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());\n      trustStore.load(\n          new FileInputStream((ResourceUtils.getFile(truststoreLocation))),\n          truststorePassword.toCharArray()\n      );\n      TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(\n          TrustManagerFactory.getDefaultAlgorithm()\n      );\n      trustManagerFactory.init(trustStore);\n      contextBuilder.trustManager(trustManagerFactory);\n    }\n\n    // Prepare keystore only if we got a keystore\n    if (keystoreLocation != null && keystorePassword != null) {\n      KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());\n      keyStore.load(\n          new FileInputStream(ResourceUtils.getFile(keystoreLocation)),\n          keystorePassword.toCharArray()\n      );\n\n      KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());\n      keyManagerFactory.init(keyStore, keystorePassword.toCharArray());\n      contextBuilder.keyManager(keyManagerFactory);\n    }\n\n    // Create webclient\n    SslContext context = contextBuilder.build();\n\n    httpClient = httpClient.secure(t -> t.sslContext(context));\n    return this;\n  }\n\n  public WebClientConfigurator configureBasicAuth(@Nullable String username, @Nullable String password) {\n    if (username != null && password != null) {\n      builder.defaultHeaders(httpHeaders -> httpHeaders.setBasicAuth(username, password));\n    } else if (username != null) {\n      throw new ValidationException(\"You specified username but did not specify password\");\n    } else if (password != null) {\n      throw new ValidationException(\"You specified password but did not specify username\");\n    }\n    return this;\n  }\n\n  public WebClientConfigurator configureBufferSize(DataSize maxBuffSize) {\n    builder.codecs(c -> c.defaultCodecs().maxInMemorySize((int) maxBuffSize.toBytes()));\n    return this;\n  }\n\n  public WebClientConfigurator configureObjectMapper(ObjectMapper mapper) {\n    builder.codecs(codecs -> {\n      codecs.defaultCodecs()\n          .jackson2JsonEncoder(new Jackson2JsonEncoder(mapper, MediaType.APPLICATION_JSON));\n      codecs.defaultCodecs()\n          .jackson2JsonDecoder(new Jackson2JsonDecoder(mapper, MediaType.APPLICATION_JSON));\n    });\n    return this;\n  }\n\n  public WebClientConfigurator configureCodecs(Consumer<ClientCodecConfigurer> configurer) {\n    builder.codecs(configurer);\n    return this;\n  }\n\n  public WebClient build() {\n    return builder.clientConnector(new ReactorClientHttpConnector(httpClient)).build();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/annotation/KafkaClientInternalsDependant.java",
    "content": "package com.provectus.kafka.ui.util.annotation;\n\n/**\n * All code places that depend on kafka-client's internals or implementation-specific logic\n * should be marked with this annotation to make further update process easier.\n */\npublic @interface KafkaClientInternalsDependant {\n  String value() default \"\";\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/AnyFieldSchema.java",
    "content": "package com.provectus.kafka.ui.util.jsonschema;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\n\n// Specifies field that can contain any kind of value - primitive, complex and nulls\nclass AnyFieldSchema implements FieldSchema {\n\n  static AnyFieldSchema get() {\n    return new AnyFieldSchema();\n  }\n\n  private AnyFieldSchema() {\n  }\n\n  @Override\n  public JsonNode toJsonNode(ObjectMapper mapper) {\n    var arr = mapper.createArrayNode();\n    arr.add(\"number\");\n    arr.add(\"string\");\n    arr.add(\"object\");\n    arr.add(\"array\");\n    arr.add(\"boolean\");\n    arr.add(\"null\");\n    return mapper.createObjectNode().set(\"type\", arr);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/ArrayFieldSchema.java",
    "content": "package com.provectus.kafka.ui.util.jsonschema;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\n\nclass ArrayFieldSchema implements FieldSchema {\n  private final FieldSchema itemsSchema;\n\n  ArrayFieldSchema(FieldSchema itemsSchema) {\n    this.itemsSchema = itemsSchema;\n  }\n\n  @Override\n  public JsonNode toJsonNode(ObjectMapper mapper) {\n    final ObjectNode objectNode = mapper.createObjectNode();\n    objectNode.setAll(new SimpleJsonType(JsonType.Type.ARRAY).toJsonNode(mapper));\n    objectNode.set(\"items\", itemsSchema.toJsonNode(mapper));\n    return objectNode;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/AvroJsonSchemaConverter.java",
    "content": "package com.provectus.kafka.ui.util.jsonschema;\n\nimport java.net.URI;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\nimport org.apache.avro.Schema;\nimport reactor.util.function.Tuple2;\nimport reactor.util.function.Tuples;\n\npublic class AvroJsonSchemaConverter implements JsonSchemaConverter<Schema> {\n\n  @Override\n  public JsonSchema convert(URI basePath, Schema schema) {\n    final JsonSchema.JsonSchemaBuilder builder = JsonSchema.builder();\n\n    builder.id(basePath.resolve(schema.getName()));\n    JsonType type = convertType(schema);\n    builder.type(type);\n\n    Map<String, FieldSchema> definitions = new HashMap<>();\n    final FieldSchema root = convertSchema(schema, definitions, true);\n    builder.definitions(definitions);\n\n    if (type.getType().equals(JsonType.Type.OBJECT)) {\n      final ObjectFieldSchema objectRoot = (ObjectFieldSchema) root;\n      builder.properties(objectRoot.getProperties());\n      builder.required(objectRoot.getRequired());\n    }\n\n    return builder.build();\n  }\n\n\n  private FieldSchema convertField(Schema.Field field, Map<String, FieldSchema> definitions) {\n    return convertSchema(field.schema(), definitions, false);\n  }\n\n  private FieldSchema convertSchema(Schema schema,\n                                    Map<String, FieldSchema> definitions, boolean isRoot) {\n    Optional<FieldSchema> logicalTypeSchema = JsonAvroConversion.LogicalTypeConversion.getJsonSchema(schema);\n    if (logicalTypeSchema.isPresent()) {\n      return logicalTypeSchema.get();\n    }\n    if (!schema.isUnion()) {\n      JsonType type = convertType(schema);\n      switch (type.getType()) {\n        case BOOLEAN:\n        case NULL:\n        case STRING:\n        case ENUM:\n        case NUMBER:\n        case INTEGER:\n          return new SimpleFieldSchema(type);\n        case OBJECT:\n          if (schema.getType().equals(Schema.Type.MAP)) {\n            return new MapFieldSchema(convertSchema(schema.getValueType(), definitions, isRoot));\n          } else {\n            return createObjectSchema(schema, definitions, isRoot);\n          }\n        case ARRAY:\n          return createArraySchema(schema, definitions);\n        default:\n          throw new RuntimeException(\"Unknown type\");\n      }\n    } else {\n      return createUnionSchema(schema, definitions);\n    }\n  }\n\n  // this method formats json-schema field in a way\n  // to fit avro-> json encoding rules (https://avro.apache.org/docs/1.11.1/specification/_print/#json-encoding)\n  private FieldSchema createUnionSchema(Schema schema, Map<String, FieldSchema> definitions) {\n    final boolean nullable = schema.getTypes().stream()\n        .anyMatch(t -> t.getType().equals(Schema.Type.NULL));\n\n    final Map<String, FieldSchema> fields = schema.getTypes().stream()\n        .filter(t -> !t.getType().equals(Schema.Type.NULL))\n        .map(f -> {\n          String oneOfFieldName;\n          if (f.getType().equals(Schema.Type.RECORD)) {\n            // for records using full record name\n            oneOfFieldName = f.getFullName();\n          } else {\n            // for primitive types - using type name\n            oneOfFieldName = f.getType().getName().toLowerCase();\n          }\n          return Tuples.of(oneOfFieldName, convertSchema(f, definitions, false));\n        }).collect(Collectors.toMap(\n            Tuple2::getT1,\n            Tuple2::getT2\n        ));\n\n    if (nullable) {\n      return new OneOfFieldSchema(\n          List.of(\n              new SimpleFieldSchema(new SimpleJsonType(JsonType.Type.NULL)),\n              new ObjectFieldSchema(fields, Collections.emptyList())\n          )\n      );\n    } else {\n      return new ObjectFieldSchema(fields, Collections.emptyList());\n    }\n  }\n\n  private FieldSchema createObjectSchema(Schema schema,\n                                         Map<String, FieldSchema> definitions,\n                                         boolean isRoot) {\n    var definitionName = schema.getFullName();\n    if (definitions.containsKey(definitionName)) {\n      return createRefField(definitionName);\n    }\n    // adding stub record, need to avoid infinite recursion\n    definitions.put(definitionName, ObjectFieldSchema.EMPTY);\n\n    final Map<String, FieldSchema> fields = schema.getFields().stream()\n        .map(f -> Tuples.of(f.name(), convertField(f, definitions)))\n        .collect(Collectors.toMap(\n            Tuple2::getT1,\n            Tuple2::getT2\n        ));\n\n    final List<String> required = schema.getFields().stream()\n        .filter(f -> !f.schema().isNullable())\n        .map(Schema.Field::name).collect(Collectors.toList());\n\n    var objectSchema = new ObjectFieldSchema(fields, required);\n    if (isRoot) {\n      // replacing stub with self-reference (need for usage in json-schema's oneOf)\n      definitions.put(definitionName, new RefFieldSchema(\"#\"));\n      return objectSchema;\n    } else {\n      // replacing stub record with actual object structure\n      definitions.put(definitionName, objectSchema);\n      return createRefField(definitionName);\n    }\n  }\n\n  private RefFieldSchema createRefField(String definitionName) {\n    return new RefFieldSchema(String.format(\"#/definitions/%s\", definitionName));\n  }\n\n  private ArrayFieldSchema createArraySchema(Schema schema,\n                                             Map<String, FieldSchema> definitions) {\n    return new ArrayFieldSchema(\n        convertSchema(schema.getElementType(), definitions, false)\n    );\n  }\n\n  private JsonType convertType(Schema schema) {\n    return switch (schema.getType()) {\n      case INT, LONG -> new SimpleJsonType(JsonType.Type.INTEGER);\n      case MAP, RECORD -> new SimpleJsonType(JsonType.Type.OBJECT);\n      case ENUM -> new EnumJsonType(schema.getEnumSymbols());\n      case BYTES, STRING -> new SimpleJsonType(JsonType.Type.STRING);\n      case NULL -> new SimpleJsonType(JsonType.Type.NULL);\n      case ARRAY -> new SimpleJsonType(JsonType.Type.ARRAY);\n      case FIXED, FLOAT, DOUBLE -> new SimpleJsonType(JsonType.Type.NUMBER);\n      case BOOLEAN -> new SimpleJsonType(JsonType.Type.BOOLEAN);\n      default -> new SimpleJsonType(JsonType.Type.STRING);\n    };\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/EnumJsonType.java",
    "content": "package com.provectus.kafka.ui.util.jsonschema;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.TextNode;\nimport java.util.List;\nimport java.util.Map;\n\n\nclass EnumJsonType extends JsonType {\n  private final List<String> values;\n\n  EnumJsonType(List<String> values) {\n    super(Type.ENUM);\n    this.values = values;\n  }\n\n  @Override\n  public Map<String, JsonNode> toJsonNode(ObjectMapper mapper) {\n    return Map.of(\n        \"type\",\n        new TextNode(Type.STRING.getName()),\n        Type.ENUM.getName(),\n        mapper.valueToTree(values)\n    );\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/FieldSchema.java",
    "content": "package com.provectus.kafka.ui.util.jsonschema;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\n\ninterface FieldSchema {\n  JsonNode toJsonNode(ObjectMapper mapper);\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/JsonAvroConversion.java",
    "content": "package com.provectus.kafka.ui.util.jsonschema;\n\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.fasterxml.jackson.databind.node.BooleanNode;\nimport com.fasterxml.jackson.databind.node.DecimalNode;\nimport com.fasterxml.jackson.databind.node.DoubleNode;\nimport com.fasterxml.jackson.databind.node.FloatNode;\nimport com.fasterxml.jackson.databind.node.IntNode;\nimport com.fasterxml.jackson.databind.node.JsonNodeType;\nimport com.fasterxml.jackson.databind.node.LongNode;\nimport com.fasterxml.jackson.databind.node.NullNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.fasterxml.jackson.databind.node.TextNode;\nimport com.google.common.collect.Lists;\nimport com.provectus.kafka.ui.exception.JsonAvroConversionException;\nimport io.confluent.kafka.serializers.AvroData;\nimport java.math.BigDecimal;\nimport java.nio.ByteBuffer;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Instant;\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.LocalTime;\nimport java.time.ZoneOffset;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.BiFunction;\nimport java.util.stream.Stream;\nimport org.apache.avro.Schema;\nimport org.apache.avro.generic.GenericData;\n\n// json <-> avro\npublic class JsonAvroConversion {\n\n  private static final JsonMapper MAPPER = new JsonMapper();\n  private static final Schema NULL_SCHEMA = Schema.create(Schema.Type.NULL);\n  private static final String FORMAT = \"format\";\n  private static final String DATE_TIME = \"date-time\";\n\n  // converts json into Object that is expected input for KafkaAvroSerializer\n  // (with AVRO_USE_LOGICAL_TYPE_CONVERTERS flat enabled!)\n  public static Object convertJsonToAvro(String jsonString, Schema avroSchema) {\n    JsonNode rootNode = null;\n    try {\n      rootNode = MAPPER.readTree(jsonString);\n    } catch (JsonProcessingException e) {\n      throw new JsonAvroConversionException(\"String is not a valid json\");\n    }\n    return convert(rootNode, avroSchema);\n  }\n\n  private static Object convert(JsonNode node, Schema avroSchema) {\n    return switch (avroSchema.getType()) {\n      case RECORD -> {\n        assertJsonType(node, JsonNodeType.OBJECT);\n        var rec = new GenericData.Record(avroSchema);\n        for (Schema.Field field : avroSchema.getFields()) {\n          if (node.has(field.name()) && !node.get(field.name()).isNull()) {\n            rec.put(field.name(), convert(node.get(field.name()), field.schema()));\n          }\n        }\n        yield rec;\n      }\n      case MAP -> {\n        assertJsonType(node, JsonNodeType.OBJECT);\n        var map = new LinkedHashMap<String, Object>();\n        var valueSchema = avroSchema.getValueType();\n        node.fields().forEachRemaining(f -> map.put(f.getKey(), convert(f.getValue(), valueSchema)));\n        yield map;\n      }\n      case ARRAY -> {\n        assertJsonType(node, JsonNodeType.ARRAY);\n        var lst = new ArrayList<>();\n        node.elements().forEachRemaining(e -> lst.add(convert(e, avroSchema.getElementType())));\n        yield lst;\n      }\n      case ENUM -> {\n        assertJsonType(node, JsonNodeType.STRING);\n        String symbol = node.textValue();\n        if (!avroSchema.getEnumSymbols().contains(symbol)) {\n          throw new JsonAvroConversionException(\"%s is not a part of enum symbols [%s]\"\n              .formatted(symbol, avroSchema.getEnumSymbols()));\n        }\n        yield new GenericData.EnumSymbol(avroSchema, symbol);\n      }\n      case UNION -> {\n        // for types from enum (other than null) payload should be an object with single key == name of type\n        // ex: schema = [ \"null\", \"int\", \"string\" ], possible payloads = null, { \"string\": \"str\" },  { \"int\": 123 }\n        if (node.isNull() && avroSchema.getTypes().contains(NULL_SCHEMA)) {\n          yield null;\n        }\n\n        assertJsonType(node, JsonNodeType.OBJECT);\n        var elements = Lists.newArrayList(node.fields());\n        if (elements.size() != 1) {\n          throw new JsonAvroConversionException(\n              \"UNION field value should be an object with single field == type name\");\n        }\n        Map.Entry<String, JsonNode> typeNameToValue = elements.get(0);\n        List<Schema> candidates = new ArrayList<>();\n        for (Schema unionType : avroSchema.getTypes()) {\n          if (typeNameToValue.getKey().equals(unionType.getFullName())) {\n            yield convert(typeNameToValue.getValue(), unionType);\n          }\n          if (typeNameToValue.getKey().equals(unionType.getName())) {\n            candidates.add(unionType);\n          }\n        }\n        if (candidates.size() == 1) {\n          yield convert(typeNameToValue.getValue(), candidates.get(0));\n        }\n        if (candidates.size() > 1) {\n          throw new JsonAvroConversionException(\n              \"Can't select type within union for value '%s'. Provide full type name.\".formatted(node)\n          );\n        }\n        throw new JsonAvroConversionException(\n            \"json value '%s' is cannot be converted to any of union types [%s]\"\n                .formatted(node, avroSchema.getTypes()));\n      }\n      case STRING -> {\n        if (isLogicalType(avroSchema)) {\n          yield processLogicalType(node, avroSchema);\n        }\n        assertJsonType(node, JsonNodeType.STRING);\n        yield node.textValue();\n      }\n      case LONG -> {\n        if (isLogicalType(avroSchema)) {\n          yield processLogicalType(node, avroSchema);\n        }\n        assertJsonType(node, JsonNodeType.NUMBER);\n        assertJsonNumberType(node, JsonParser.NumberType.LONG, JsonParser.NumberType.INT);\n        yield node.longValue();\n      }\n      case INT -> {\n        if (isLogicalType(avroSchema)) {\n          yield processLogicalType(node, avroSchema);\n        }\n        assertJsonType(node, JsonNodeType.NUMBER);\n        assertJsonNumberType(node, JsonParser.NumberType.INT);\n        yield node.intValue();\n      }\n      case FLOAT -> {\n        assertJsonType(node, JsonNodeType.NUMBER);\n        assertJsonNumberType(node, JsonParser.NumberType.DOUBLE, JsonParser.NumberType.FLOAT);\n        yield node.floatValue();\n      }\n      case DOUBLE -> {\n        assertJsonType(node, JsonNodeType.NUMBER);\n        assertJsonNumberType(node, JsonParser.NumberType.DOUBLE, JsonParser.NumberType.FLOAT);\n        yield node.doubleValue();\n      }\n      case BOOLEAN -> {\n        assertJsonType(node, JsonNodeType.BOOLEAN);\n        yield node.booleanValue();\n      }\n      case NULL -> {\n        assertJsonType(node, JsonNodeType.NULL);\n        yield null;\n      }\n      case BYTES -> {\n        if (isLogicalType(avroSchema)) {\n          yield processLogicalType(node, avroSchema);\n        }\n        assertJsonType(node, JsonNodeType.STRING);\n        // logic copied from JsonDecoder::readBytes\n        yield ByteBuffer.wrap(node.textValue().getBytes(StandardCharsets.ISO_8859_1));\n      }\n      case FIXED -> {\n        if (isLogicalType(avroSchema)) {\n          yield processLogicalType(node, avroSchema);\n        }\n        assertJsonType(node, JsonNodeType.STRING);\n        byte[] bytes = node.textValue().getBytes(StandardCharsets.ISO_8859_1);\n        if (bytes.length != avroSchema.getFixedSize()) {\n          throw new JsonAvroConversionException(\n              \"Fixed field has unexpected size %d (should be %d)\"\n                  .formatted(bytes.length, avroSchema.getFixedSize()));\n        }\n        yield new GenericData.Fixed(avroSchema, bytes);\n      }\n    };\n  }\n\n  // converts output of KafkaAvroDeserializer (with AVRO_USE_LOGICAL_TYPE_CONVERTERS flat enabled!) into json.\n  // Note: conversion should be compatible with AvroJsonSchemaConverter logic!\n  public static JsonNode convertAvroToJson(Object obj, Schema avroSchema) {\n    if (obj == null) {\n      return NullNode.getInstance();\n    }\n    return switch (avroSchema.getType()) {\n      case RECORD -> {\n        var rec = (GenericData.Record) obj;\n        ObjectNode node = MAPPER.createObjectNode();\n        for (Schema.Field field : avroSchema.getFields()) {\n          var fieldVal = rec.get(field.name());\n          if (fieldVal != null) {\n            node.set(field.name(), convertAvroToJson(fieldVal, field.schema()));\n          }\n        }\n        yield node;\n      }\n      case MAP -> {\n        ObjectNode node = MAPPER.createObjectNode();\n        ((Map) obj).forEach((k, v) -> node.set(k.toString(), convertAvroToJson(v, avroSchema.getValueType())));\n        yield node;\n      }\n      case ARRAY -> {\n        var list = (List<Object>) obj;\n        ArrayNode node = MAPPER.createArrayNode();\n        list.forEach(e -> node.add(convertAvroToJson(e, avroSchema.getElementType())));\n        yield node;\n      }\n      case ENUM -> {\n        yield new TextNode(obj.toString());\n      }\n      case UNION -> {\n        ObjectNode node = MAPPER.createObjectNode();\n        int unionIdx = AvroData.getGenericData().resolveUnion(avroSchema, obj);\n        Schema selectedType = avroSchema.getTypes().get(unionIdx);\n        node.set(\n            selectUnionTypeFieldName(avroSchema, selectedType, unionIdx),\n            convertAvroToJson(obj, selectedType)\n        );\n        yield node;\n      }\n      case STRING -> {\n        if (isLogicalType(avroSchema)) {\n          yield processLogicalType(obj, avroSchema);\n        }\n        yield new TextNode(obj.toString());\n      }\n      case LONG -> {\n        if (isLogicalType(avroSchema)) {\n          yield processLogicalType(obj, avroSchema);\n        }\n        yield new LongNode((Long) obj);\n      }\n      case INT -> {\n        if (isLogicalType(avroSchema)) {\n          yield processLogicalType(obj, avroSchema);\n        }\n        yield new IntNode((Integer) obj);\n      }\n      case FLOAT -> new FloatNode((Float) obj);\n      case DOUBLE -> new DoubleNode((Double) obj);\n      case BOOLEAN -> BooleanNode.valueOf((Boolean) obj);\n      case NULL -> NullNode.getInstance();\n      case BYTES -> {\n        if (isLogicalType(avroSchema)) {\n          yield processLogicalType(obj, avroSchema);\n        }\n        ByteBuffer bytes = (ByteBuffer) obj;\n        //see JsonEncoder::writeByteArray\n        yield new TextNode(new String(bytes.array(), StandardCharsets.ISO_8859_1));\n      }\n      case FIXED -> {\n        if (isLogicalType(avroSchema)) {\n          yield processLogicalType(obj, avroSchema);\n        }\n        var fixed = (GenericData.Fixed) obj;\n        yield new TextNode(new String(fixed.bytes(), StandardCharsets.ISO_8859_1));\n      }\n    };\n  }\n\n  // select name for a key field that represents type name of union.\n  // For records selects short name, if it is possible.\n  private static String selectUnionTypeFieldName(Schema unionSchema,\n                                                 Schema chosenType,\n                                                 int chosenTypeIdx) {\n    var types = unionSchema.getTypes();\n    if (types.size() == 2 && types.contains(NULL_SCHEMA)) {\n      return chosenType.getName();\n    }\n    for (int i = 0; i < types.size(); i++) {\n      if (i != chosenTypeIdx && chosenType.getName().equals(types.get(i).getName())) {\n        // there is another type inside union with the same name\n        // so, we have to use fullname\n        return chosenType.getFullName();\n      }\n    }\n    return chosenType.getName();\n  }\n\n  private static Object processLogicalType(JsonNode node, Schema schema) {\n    return findConversion(schema)\n        .map(c -> c.jsonToAvroConversion.apply(node, schema))\n        .orElseThrow(() ->\n            new JsonAvroConversionException(\"'%s' logical type is not supported\"\n                .formatted(schema.getLogicalType().getName())));\n  }\n\n  private static JsonNode processLogicalType(Object obj, Schema schema) {\n    return findConversion(schema)\n        .map(c -> c.avroToJsonConversion.apply(obj, schema))\n        .orElseThrow(() ->\n            new JsonAvroConversionException(\"'%s' logical type is not supported\"\n                .formatted(schema.getLogicalType().getName())));\n  }\n\n  private static Optional<LogicalTypeConversion> findConversion(Schema schema) {\n    String logicalTypeName = schema.getLogicalType().getName();\n    return Stream.of(LogicalTypeConversion.values())\n        .filter(t -> t.name.equalsIgnoreCase(logicalTypeName))\n        .findFirst();\n  }\n\n  private static boolean isLogicalType(Schema schema) {\n    return schema.getLogicalType() != null;\n  }\n\n  private static void assertJsonType(JsonNode node, JsonNodeType... allowedTypes) {\n    if (Stream.of(allowedTypes).noneMatch(t -> node.getNodeType() == t)) {\n      throw new JsonAvroConversionException(\n          \"%s node has unexpected type, allowed types %s, actual type %s\"\n              .formatted(node, Arrays.toString(allowedTypes), node.getNodeType()));\n    }\n  }\n\n  private static void assertJsonNumberType(JsonNode node, JsonParser.NumberType... allowedTypes) {\n    if (Stream.of(allowedTypes).noneMatch(t -> node.numberType() == t)) {\n      throw new JsonAvroConversionException(\n          \"%s node has unexpected numeric type, allowed types %s, actual type %s\"\n              .formatted(node, Arrays.toString(allowedTypes), node.numberType()));\n    }\n  }\n\n  enum LogicalTypeConversion {\n\n    UUID(\"uuid\",\n        (node, schema) -> {\n          assertJsonType(node, JsonNodeType.STRING);\n          return java.util.UUID.fromString(node.asText());\n        },\n        (obj, schema) -> {\n          return new TextNode(obj.toString());\n        },\n        new SimpleFieldSchema(\n            new SimpleJsonType(\n                JsonType.Type.STRING,\n                Map.of(FORMAT, new TextNode(\"uuid\"))))\n    ),\n\n    DECIMAL(\"decimal\",\n        (node, schema) -> {\n          if (node.isTextual()) {\n            return new BigDecimal(node.asText());\n          } else if (node.isNumber()) {\n            return new BigDecimal(node.numberValue().toString());\n          }\n          throw new JsonAvroConversionException(\n              \"node '%s' can't be converted to decimal logical type\"\n                  .formatted(node));\n        },\n        (obj, schema) -> {\n          return new DecimalNode((BigDecimal) obj);\n        },\n        new SimpleFieldSchema(new SimpleJsonType(JsonType.Type.NUMBER))\n    ),\n\n    DATE(\"date\",\n        (node, schema) -> {\n          if (node.isInt()) {\n            return LocalDate.ofEpochDay(node.intValue());\n          } else if (node.isTextual()) {\n            return LocalDate.parse(node.asText());\n          } else {\n            throw new JsonAvroConversionException(\n                \"node '%s' can't be converted to date logical type\"\n                    .formatted(node));\n          }\n        },\n        (obj, schema) -> {\n          return new TextNode(obj.toString());\n        },\n        new SimpleFieldSchema(\n            new SimpleJsonType(\n                JsonType.Type.STRING,\n                Map.of(FORMAT, new TextNode(\"date\"))))\n    ),\n\n    TIME_MILLIS(\"time-millis\",\n        (node, schema) -> {\n          if (node.isIntegralNumber()) {\n            return LocalTime.ofNanoOfDay(TimeUnit.MILLISECONDS.toNanos(node.longValue()));\n          } else if (node.isTextual()) {\n            return LocalTime.parse(node.asText());\n          } else {\n            throw new JsonAvroConversionException(\n                \"node '%s' can't be converted to time-millis logical type\"\n                    .formatted(node));\n          }\n        },\n        (obj, schema) -> {\n          return new TextNode(obj.toString());\n        },\n        new SimpleFieldSchema(\n            new SimpleJsonType(\n                JsonType.Type.STRING,\n                Map.of(FORMAT, new TextNode(\"time\"))))\n    ),\n\n    TIME_MICROS(\"time-micros\",\n        (node, schema) -> {\n          if (node.isIntegralNumber()) {\n            return LocalTime.ofNanoOfDay(TimeUnit.MICROSECONDS.toNanos(node.longValue()));\n          } else if (node.isTextual()) {\n            return LocalTime.parse(node.asText());\n          } else {\n            throw new JsonAvroConversionException(\n                \"node '%s' can't be converted to time-micros logical type\"\n                    .formatted(node));\n          }\n        },\n        (obj, schema) -> {\n          return new TextNode(obj.toString());\n        },\n        new SimpleFieldSchema(\n            new SimpleJsonType(\n                JsonType.Type.STRING,\n                Map.of(FORMAT, new TextNode(\"time\"))))\n    ),\n\n    TIMESTAMP_MILLIS(\"timestamp-millis\",\n        (node, schema) -> {\n          if (node.isIntegralNumber()) {\n            return Instant.ofEpochMilli(node.longValue());\n          } else if (node.isTextual()) {\n            return Instant.parse(node.asText());\n          } else {\n            throw new JsonAvroConversionException(\n                \"node '%s' can't be converted to timestamp-millis logical type\"\n                    .formatted(node));\n          }\n        },\n        (obj, schema) -> {\n          return new TextNode(obj.toString());\n        },\n        new SimpleFieldSchema(\n            new SimpleJsonType(\n                JsonType.Type.STRING,\n                Map.of(FORMAT, new TextNode(DATE_TIME))))\n    ),\n\n    TIMESTAMP_MICROS(\"timestamp-micros\",\n        (node, schema) -> {\n          if (node.isIntegralNumber()) {\n            // TimeConversions.TimestampMicrosConversion for impl\n            long microsFromEpoch = node.longValue();\n            long epochSeconds = microsFromEpoch / (1_000_000L);\n            long nanoAdjustment = (microsFromEpoch % (1_000_000L)) * 1_000L;\n            return Instant.ofEpochSecond(epochSeconds, nanoAdjustment);\n          } else if (node.isTextual()) {\n            return Instant.parse(node.asText());\n          } else {\n            throw new JsonAvroConversionException(\n                \"node '%s' can't be converted to timestamp-millis logical type\"\n                    .formatted(node));\n          }\n        },\n        (obj, schema) -> {\n          return new TextNode(obj.toString());\n        },\n        new SimpleFieldSchema(\n            new SimpleJsonType(\n                JsonType.Type.STRING,\n                Map.of(FORMAT, new TextNode(DATE_TIME))))\n    ),\n\n    LOCAL_TIMESTAMP_MILLIS(\"local-timestamp-millis\",\n        (node, schema) -> {\n          if (node.isTextual()) {\n            return LocalDateTime.parse(node.asText());\n          }\n          // TimeConversions.TimestampMicrosConversion for impl\n          Instant instant = (Instant) TIMESTAMP_MILLIS.jsonToAvroConversion.apply(node, schema);\n          return LocalDateTime.ofInstant(instant, ZoneOffset.UTC);\n        },\n        (obj, schema) -> {\n          return new TextNode(obj.toString());\n        },\n        new SimpleFieldSchema(\n            new SimpleJsonType(\n                JsonType.Type.STRING,\n                Map.of(FORMAT, new TextNode(DATE_TIME))))\n    ),\n\n    LOCAL_TIMESTAMP_MICROS(\"local-timestamp-micros\",\n        (node, schema) -> {\n          if (node.isTextual()) {\n            return LocalDateTime.parse(node.asText());\n          }\n          Instant instant = (Instant) TIMESTAMP_MICROS.jsonToAvroConversion.apply(node, schema);\n          return LocalDateTime.ofInstant(instant, ZoneOffset.UTC);\n        },\n        (obj, schema) -> {\n          return new TextNode(obj.toString());\n        },\n        new SimpleFieldSchema(\n            new SimpleJsonType(\n                JsonType.Type.STRING,\n                Map.of(FORMAT, new TextNode(DATE_TIME))))\n    );\n\n    private final String name;\n    private final BiFunction<JsonNode, Schema, Object> jsonToAvroConversion;\n    private final BiFunction<Object, Schema, JsonNode> avroToJsonConversion;\n    private final FieldSchema jsonSchema;\n\n    LogicalTypeConversion(String name,\n                          BiFunction<JsonNode, Schema, Object> jsonToAvroConversion,\n                          BiFunction<Object, Schema, JsonNode> avroToJsonConversion,\n                          FieldSchema jsonSchema) {\n      this.name = name;\n      this.jsonToAvroConversion = jsonToAvroConversion;\n      this.avroToJsonConversion = avroToJsonConversion;\n      this.jsonSchema = jsonSchema;\n    }\n\n    static Optional<FieldSchema> getJsonSchema(Schema schema) {\n      if (schema.getLogicalType() == null) {\n        return Optional.empty();\n      }\n      String logicalTypeName = schema.getLogicalType().getName();\n      return Stream.of(JsonAvroConversion.LogicalTypeConversion.values())\n          .filter(t -> t.name.equalsIgnoreCase(logicalTypeName))\n          .map(c -> c.jsonSchema)\n          .findFirst();\n    }\n  }\n\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/JsonSchema.java",
    "content": "package com.provectus.kafka.ui.util.jsonschema;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.fasterxml.jackson.databind.node.TextNode;\nimport java.net.URI;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.SneakyThrows;\nimport reactor.util.function.Tuple2;\nimport reactor.util.function.Tuples;\n\n@Data\n@Builder\npublic class JsonSchema {\n  private final URI id;\n  private final URI schema = URI.create(\"https://json-schema.org/draft/2020-12/schema\");\n  private final String title;\n  private final JsonType type;\n  private final Map<String, FieldSchema> properties;\n  private final Map<String, FieldSchema> definitions;\n  private final List<String> required;\n  private final String rootRef;\n\n  public String toJson() {\n    final ObjectMapper mapper = new ObjectMapper();\n    final ObjectNode objectNode = mapper.createObjectNode();\n    objectNode.set(\"$id\", new TextNode(id.toString()));\n    objectNode.set(\"$schema\", new TextNode(schema.toString()));\n    objectNode.setAll(type.toJsonNode(mapper));\n    if (properties != null && !properties.isEmpty()) {\n      objectNode.set(\"properties\", mapper.valueToTree(\n          properties.entrySet().stream()\n              .map(e -> Tuples.of(e.getKey(), e.getValue().toJsonNode(mapper)))\n              .collect(Collectors.toMap(\n                  Tuple2::getT1,\n                  Tuple2::getT2\n              ))\n      ));\n      if (!required.isEmpty()) {\n        objectNode.set(\"required\", mapper.valueToTree(required));\n      }\n    }\n    if (definitions != null && !definitions.isEmpty()) {\n      objectNode.set(\"definitions\", mapper.valueToTree(\n          definitions.entrySet().stream()\n              .map(e -> Tuples.of(e.getKey(), e.getValue().toJsonNode(mapper)))\n              .collect(Collectors.toMap(\n                  Tuple2::getT1,\n                  Tuple2::getT2\n              ))\n      ));\n    }\n    if (rootRef != null) {\n      objectNode.set(\"$ref\", new TextNode(rootRef));\n    }\n    return objectNode.toString();\n  }\n\n  @SneakyThrows\n  public static JsonSchema stringSchema() {\n    return JsonSchema.builder()\n        .id(new URI(\"http://unknown.unknown\"))\n        .type(new SimpleJsonType(JsonType.Type.STRING))\n        .build();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/JsonSchemaConverter.java",
    "content": "package com.provectus.kafka.ui.util.jsonschema;\n\nimport java.net.URI;\n\npublic interface JsonSchemaConverter<T> {\n  JsonSchema convert(URI basePath, T schema);\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/JsonType.java",
    "content": "package com.provectus.kafka.ui.util.jsonschema;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.util.Map;\n\nabstract class JsonType {\n\n  protected final Type type;\n\n  protected JsonType(Type type) {\n    this.type = type;\n  }\n\n  Type getType() {\n    return type;\n  }\n\n  abstract Map<String, JsonNode> toJsonNode(ObjectMapper mapper);\n\n  enum Type {\n    NULL,\n    BOOLEAN,\n    OBJECT,\n    ARRAY,\n    NUMBER,\n    INTEGER,\n    ENUM,\n    STRING;\n\n    private final String name;\n\n    Type() {\n      this.name = this.name().toLowerCase();\n    }\n\n    public String getName() {\n      return name;\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/MapFieldSchema.java",
    "content": "package com.provectus.kafka.ui.util.jsonschema;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.BooleanNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.fasterxml.jackson.databind.node.TextNode;\nimport javax.annotation.Nullable;\n\nclass MapFieldSchema implements FieldSchema {\n  private final @Nullable FieldSchema itemSchema;\n\n  MapFieldSchema(@Nullable FieldSchema itemSchema) {\n    this.itemSchema = itemSchema;\n  }\n\n  MapFieldSchema() {\n    this(null);\n  }\n\n  @Override\n  public JsonNode toJsonNode(ObjectMapper mapper) {\n    final ObjectNode objectNode = mapper.createObjectNode();\n    objectNode.set(\"type\", new TextNode(JsonType.Type.OBJECT.getName()));\n    objectNode.set(\"additionalProperties\", itemSchema != null ? itemSchema.toJsonNode(mapper) : BooleanNode.TRUE);\n    return objectNode;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/ObjectFieldSchema.java",
    "content": "package com.provectus.kafka.ui.util.jsonschema;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport reactor.util.function.Tuple2;\nimport reactor.util.function.Tuples;\n\nclass ObjectFieldSchema implements FieldSchema {\n\n  static final ObjectFieldSchema EMPTY = new ObjectFieldSchema(Map.of(), List.of());\n\n  private final Map<String, FieldSchema> properties;\n  private final List<String> required;\n\n  ObjectFieldSchema(Map<String, FieldSchema> properties,\n                           List<String> required) {\n    this.properties = properties;\n    this.required = required;\n  }\n\n  Map<String, FieldSchema> getProperties() {\n    return properties;\n  }\n\n  List<String> getRequired() {\n    return required;\n  }\n\n  @Override\n  public JsonNode toJsonNode(ObjectMapper mapper) {\n    final Map<String, JsonNode> nodes = properties.entrySet().stream()\n        .map(e -> Tuples.of(e.getKey(), e.getValue().toJsonNode(mapper)))\n        .collect(Collectors.toMap(\n            Tuple2::getT1,\n            Tuple2::getT2\n        ));\n    final ObjectNode objectNode = mapper.createObjectNode();\n    objectNode.setAll(\n        new SimpleJsonType(JsonType.Type.OBJECT).toJsonNode(mapper)\n    );\n    objectNode.set(\"properties\", mapper.valueToTree(nodes));\n    if (!required.isEmpty()) {\n      objectNode.set(\"required\", mapper.valueToTree(required));\n    }\n    return objectNode;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/OneOfFieldSchema.java",
    "content": "package com.provectus.kafka.ui.util.jsonschema;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nclass OneOfFieldSchema implements FieldSchema {\n  private final List<FieldSchema> schemaList;\n\n  OneOfFieldSchema(List<FieldSchema> schemaList) {\n    this.schemaList = schemaList;\n  }\n\n  @Override\n  public JsonNode toJsonNode(ObjectMapper mapper) {\n    return mapper.createObjectNode()\n        .set(\"oneOf\",\n            mapper.createArrayNode().addAll(\n                schemaList.stream()\n                    .map(s -> s.toJsonNode(mapper))\n                    .collect(Collectors.toList())\n            )\n        );\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/ProtobufSchemaConverter.java",
    "content": "package com.provectus.kafka.ui.util.jsonschema;\n\nimport static java.util.Objects.requireNonNull;\n\nimport com.fasterxml.jackson.databind.node.BigIntegerNode;\nimport com.fasterxml.jackson.databind.node.IntNode;\nimport com.fasterxml.jackson.databind.node.LongNode;\nimport com.fasterxml.jackson.databind.node.TextNode;\nimport com.google.common.primitives.UnsignedInteger;\nimport com.google.common.primitives.UnsignedLong;\nimport com.google.protobuf.Any;\nimport com.google.protobuf.BoolValue;\nimport com.google.protobuf.BytesValue;\nimport com.google.protobuf.Descriptors;\nimport com.google.protobuf.DoubleValue;\nimport com.google.protobuf.Duration;\nimport com.google.protobuf.FieldMask;\nimport com.google.protobuf.FloatValue;\nimport com.google.protobuf.Int32Value;\nimport com.google.protobuf.Int64Value;\nimport com.google.protobuf.ListValue;\nimport com.google.protobuf.StringValue;\nimport com.google.protobuf.Struct;\nimport com.google.protobuf.Timestamp;\nimport com.google.protobuf.UInt32Value;\nimport com.google.protobuf.UInt64Value;\nimport com.google.protobuf.Value;\nimport java.net.URI;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport reactor.util.function.Tuple2;\nimport reactor.util.function.Tuples;\n\npublic class ProtobufSchemaConverter implements JsonSchemaConverter<Descriptors.Descriptor> {\n\n  private static final String MAXIMUM = \"maximum\";\n  private static final String MINIMUM = \"minimum\";\n\n  private final Set<String> simpleTypesWrapperNames = Set.of(\n      BoolValue.getDescriptor().getFullName(),\n      Int32Value.getDescriptor().getFullName(),\n      UInt32Value.getDescriptor().getFullName(),\n      Int64Value.getDescriptor().getFullName(),\n      UInt64Value.getDescriptor().getFullName(),\n      StringValue.getDescriptor().getFullName(),\n      BytesValue.getDescriptor().getFullName(),\n      FloatValue.getDescriptor().getFullName(),\n      DoubleValue.getDescriptor().getFullName()\n  );\n\n  @Override\n  public JsonSchema convert(URI basePath, Descriptors.Descriptor schema) {\n    Map<String, FieldSchema> definitions = new HashMap<>();\n    RefFieldSchema rootRef = registerObjectAndReturnRef(schema, definitions);\n    return JsonSchema.builder()\n        .id(basePath.resolve(schema.getFullName()))\n        .type(new SimpleJsonType(JsonType.Type.OBJECT))\n        .rootRef(rootRef.getRef())\n        .definitions(definitions)\n        .build();\n  }\n\n  private RefFieldSchema registerObjectAndReturnRef(Descriptors.Descriptor schema,\n                                                    Map<String, FieldSchema> definitions) {\n    var definition = schema.getFullName();\n    if (definitions.containsKey(definition)) {\n      return createRefField(definition);\n    }\n    // adding stub record, need to avoid infinite recursion\n    definitions.put(definition, ObjectFieldSchema.EMPTY);\n\n    Map<String, FieldSchema> fields = schema.getFields().stream()\n        .map(f -> Tuples.of(f.getName(), convertField(f, definitions)))\n        .collect(Collectors.toMap(Tuple2::getT1, Tuple2::getT2));\n\n    List<String> required = schema.getFields().stream()\n        .filter(Descriptors.FieldDescriptor::isRequired)\n        .map(Descriptors.FieldDescriptor::getName)\n        .collect(Collectors.toList());\n\n    // replacing stub record with actual object structure\n    definitions.put(definition, new ObjectFieldSchema(fields, required));\n    return createRefField(definition);\n  }\n\n  private RefFieldSchema createRefField(String definition) {\n    return new RefFieldSchema(\"#/definitions/%s\".formatted(definition));\n  }\n\n  private FieldSchema convertField(Descriptors.FieldDescriptor field,\n                                   Map<String, FieldSchema> definitions) {\n    Optional<FieldSchema> wellKnownTypeSchema = convertProtoWellKnownTypes(field);\n    if (wellKnownTypeSchema.isPresent()) {\n      return wellKnownTypeSchema.get();\n    }\n    if (field.isMapField()) {\n      return new MapFieldSchema();\n    }\n    final JsonType jsonType = convertType(field);\n    FieldSchema fieldSchema;\n    if (jsonType.getType().equals(JsonType.Type.OBJECT)) {\n      fieldSchema = registerObjectAndReturnRef(field.getMessageType(), definitions);\n    } else {\n      fieldSchema = new SimpleFieldSchema(jsonType);\n    }\n\n    if (field.isRepeated()) {\n      return new ArrayFieldSchema(fieldSchema);\n    } else {\n      return fieldSchema;\n    }\n  }\n\n  // converts Protobuf Well-known type (from google.protobuf.* packages) to Json-schema types\n  // see JsonFormat::buildWellKnownTypePrinters for impl details\n  private Optional<FieldSchema> convertProtoWellKnownTypes(Descriptors.FieldDescriptor field) {\n    // all well-known types are messages\n    if (field.getType() != Descriptors.FieldDescriptor.Type.MESSAGE) {\n      return Optional.empty();\n    }\n    String typeName = field.getMessageType().getFullName();\n    if (typeName.equals(Timestamp.getDescriptor().getFullName())) {\n      return Optional.of(\n          new SimpleFieldSchema(\n              new SimpleJsonType(JsonType.Type.STRING, Map.of(\"format\", new TextNode(\"date-time\")))));\n    }\n    if (typeName.equals(Duration.getDescriptor().getFullName())) {\n      return Optional.of(\n          new SimpleFieldSchema(\n              //TODO: current UI is failing when format=duration is set - need to fix this first\n              new SimpleJsonType(JsonType.Type.STRING // , Map.of(\"format\", new TextNode(\"duration\"))\n              )));\n    }\n    if (typeName.equals(FieldMask.getDescriptor().getFullName())) {\n      return Optional.of(new SimpleFieldSchema(new SimpleJsonType(JsonType.Type.STRING)));\n    }\n    if (typeName.equals(Any.getDescriptor().getFullName()) || typeName.equals(Struct.getDescriptor().getFullName())) {\n      return Optional.of(ObjectFieldSchema.EMPTY);\n    }\n    if (typeName.equals(Value.getDescriptor().getFullName())) {\n      return Optional.of(AnyFieldSchema.get());\n    }\n    if (typeName.equals(ListValue.getDescriptor().getFullName())) {\n      return Optional.of(new ArrayFieldSchema(AnyFieldSchema.get()));\n    }\n    if (simpleTypesWrapperNames.contains(typeName)) {\n      return Optional.of(new SimpleFieldSchema(\n          convertType(requireNonNull(field.getMessageType().findFieldByName(\"value\")))));\n    }\n    return Optional.empty();\n  }\n\n  private JsonType convertType(Descriptors.FieldDescriptor field) {\n    return switch (field.getType()) {\n      case INT32, FIXED32, SFIXED32, SINT32 -> new SimpleJsonType(\n          JsonType.Type.INTEGER,\n          Map.of(\n              MAXIMUM, IntNode.valueOf(Integer.MAX_VALUE),\n              MINIMUM, IntNode.valueOf(Integer.MIN_VALUE)\n          )\n      );\n      case UINT32 -> new SimpleJsonType(\n          JsonType.Type.INTEGER,\n          Map.of(\n              MAXIMUM, LongNode.valueOf(UnsignedInteger.MAX_VALUE.longValue()),\n              MINIMUM, IntNode.valueOf(0)\n          )\n      );\n      //TODO: actually all *64 types will be printed with quotes (as strings),\n      // see JsonFormat::printSingleFieldValue for impl. This can cause problems when you copy-paste from messages\n      // table to `Produce` area - need to think if it is critical or not.\n      case INT64, FIXED64, SFIXED64, SINT64 -> new SimpleJsonType(\n          JsonType.Type.INTEGER,\n          Map.of(\n              MAXIMUM, LongNode.valueOf(Long.MAX_VALUE),\n              MINIMUM, LongNode.valueOf(Long.MIN_VALUE)\n          )\n      );\n      case UINT64 -> new SimpleJsonType(\n          JsonType.Type.INTEGER,\n          Map.of(\n              MAXIMUM, new BigIntegerNode(UnsignedLong.MAX_VALUE.bigIntegerValue()),\n              MINIMUM, LongNode.valueOf(0)\n          )\n      );\n      case MESSAGE, GROUP -> new SimpleJsonType(JsonType.Type.OBJECT);\n      case ENUM -> new EnumJsonType(\n          field.getEnumType().getValues().stream()\n              .map(Descriptors.EnumValueDescriptor::getName)\n              .collect(Collectors.toList())\n      );\n      case BYTES, STRING -> new SimpleJsonType(JsonType.Type.STRING);\n      case FLOAT, DOUBLE -> new SimpleJsonType(JsonType.Type.NUMBER);\n      case BOOL -> new SimpleJsonType(JsonType.Type.BOOLEAN);\n    };\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/RefFieldSchema.java",
    "content": "package com.provectus.kafka.ui.util.jsonschema;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.TextNode;\n\nclass RefFieldSchema implements FieldSchema {\n  private final String ref;\n\n  RefFieldSchema(String ref) {\n    this.ref = ref;\n  }\n\n  @Override\n  public JsonNode toJsonNode(ObjectMapper mapper) {\n    return mapper.createObjectNode().set(\"$ref\", new TextNode(ref));\n  }\n\n  String getRef() {\n    return ref;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/SimpleFieldSchema.java",
    "content": "package com.provectus.kafka.ui.util.jsonschema;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\n\nclass SimpleFieldSchema implements FieldSchema {\n  private final JsonType type;\n\n  SimpleFieldSchema(JsonType type) {\n    this.type = type;\n  }\n\n  @Override\n  public JsonNode toJsonNode(ObjectMapper mapper) {\n    return mapper.createObjectNode().setAll(type.toJsonNode(mapper));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/SimpleJsonType.java",
    "content": "package com.provectus.kafka.ui.util.jsonschema;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.TextNode;\nimport com.google.common.collect.ImmutableMap;\nimport java.util.Map;\n\nclass SimpleJsonType extends JsonType {\n\n  private final Map<String, JsonNode> additionalTypeProperties;\n\n  SimpleJsonType(Type type) {\n    this(type, Map.of());\n  }\n\n  SimpleJsonType(Type type, Map<String, JsonNode> additionalTypeProperties) {\n    super(type);\n    this.additionalTypeProperties = additionalTypeProperties;\n  }\n\n  @Override\n  public Map<String, JsonNode> toJsonNode(ObjectMapper mapper) {\n    return ImmutableMap.<String, JsonNode>builder()\n        .put(\"type\", new TextNode(type.getName()))\n        .putAll(additionalTypeProperties)\n        .build();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/main/resources/application-local.yml",
    "content": "logging:\n  level:\n    root: INFO\n    com.provectus: DEBUG\n    #org.springframework.http.codec.json.Jackson2JsonEncoder: DEBUG\n    #org.springframework.http.codec.json.Jackson2JsonDecoder: DEBUG\n    reactor.netty.http.server.AccessLog: INFO\n    org.springframework.security: DEBUG\n\n#server:\n#  port: 8080 #- Port in which kafka-ui will run.\n\nspring:\n  jmx:\n    enabled: true\n  ldap:\n    urls: ldap://localhost:10389\n    base: \"cn={0},ou=people,dc=planetexpress,dc=com\"\n    admin-user: \"cn=admin,dc=planetexpress,dc=com\"\n    admin-password: \"GoodNewsEveryone\"\n    user-filter-search-base: \"dc=planetexpress,dc=com\"\n    user-filter-search-filter: \"(&(uid={0})(objectClass=inetOrgPerson))\"\n    group-filter-search-base: \"ou=people,dc=planetexpress,dc=com\"\n\nkafka:\n  clusters:\n    - name: local\n      bootstrapServers: localhost:9092\n      schemaRegistry: http://localhost:8085\n      ksqldbServer: http://localhost:8088\n      kafkaConnect:\n        - name: first\n          address: http://localhost:8083\n      metrics:\n        port: 9997\n        type: JMX\n\ndynamic.config.enabled: true\n\noauth2:\n  ldap:\n    activeDirectory: false\n    aсtiveDirectory.domain: domain.com\n\nauth:\n  type: DISABLED\n  #  type: OAUTH2\n  #  type: LDAP\n  oauth2:\n    client:\n      cognito:\n        clientId: # CLIENT ID\n        clientSecret: # CLIENT SECRET\n        scope: openid\n        client-name: cognito\n        provider: cognito\n        redirect-uri: http://localhost:8080/login/oauth2/code/cognito\n        authorization-grant-type: authorization_code\n        issuer-uri: https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_M7cIUn1nj\n        jwk-set-uri: https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_M7cIUn1nj/.well-known/jwks.json\n        user-name-attribute: cognito:username\n        custom-params:\n          type: cognito\n          logoutUrl: https://kafka-ui.auth.eu-central-1.amazoncognito.com/logout\n      google:\n        provider: google\n        clientId: # CLIENT ID\n        clientSecret: # CLIENT SECRET\n        user-name-attribute: email\n        custom-params:\n          type: google\n          allowedDomain: provectus.com\n      github:\n        provider: github\n        clientId: # CLIENT ID\n        clientSecret: # CLIENT SECRET\n        scope:\n          - read:org\n        user-name-attribute: login\n        custom-params:\n          type: github\n\nrbac:\n  roles:\n    - name: \"memelords\"\n      clusters:\n        - local\n      subjects:\n        - provider: oauth_google\n          type: domain\n          value: \"provectus.com\"\n        - provider: oauth_google\n          type: user\n          value: \"name@provectus.com\"\n\n        - provider: oauth_github\n          type: organization\n          value: \"provectus\"\n        - provider: oauth_github\n          type: user\n          value: \"memelord\"\n\n        - provider: oauth_cognito\n          type: user\n          value: \"username\"\n        - provider: oauth_cognito\n          type: group\n          value: \"memelords\"\n\n        - provider: ldap\n          type: group\n          value: \"admin_staff\"\n\n        # NOT IMPLEMENTED YET\n      #        - provider: ldap_ad\n      #          type: group\n      #          value: \"admin_staff\"\n\n      permissions:\n        - resource: applicationconfig\n          actions: all\n\n        - resource: clusterconfig\n          actions: all\n\n        - resource: topic\n          value: \".*\"\n          actions: all\n\n        - resource: consumer\n          value: \".*\"\n          actions: all\n\n        - resource: schema\n          value: \".*\"\n          actions: all\n\n        - resource: connect\n          value: \"*\"\n          actions: all\n\n        - resource: ksql\n          actions: all\n\n        - resource: acl\n          actions: all\n\n        - resource: audit\n          actions: all\n"
  },
  {
    "path": "kafka-ui-api/src/main/resources/application.yml",
    "content": "auth:\n  type: DISABLED\n\nmanagement:\n  endpoint:\n    info:\n      enabled: true\n    health:\n      enabled: true\n  endpoints:\n    web:\n      exposure:\n        include: \"info,health,prometheus\"\n\nlogging:\n  level:\n    root: INFO\n    com.provectus: DEBUG\n    reactor.netty.http.server.AccessLog: INFO\n    org.hibernate.validator: WARN\n\n"
  },
  {
    "path": "kafka-ui-api/src/main/resources/banner.txt",
    "content": " _   _ ___    __             _                _          _  __      __ _\n| | | |_ _|  / _|___ _ _    /_\\  _ __ __ _ __| |_  ___  | |/ /__ _ / _| |_____\n| |_| || |  |  _/ _ | '_|  / _ \\| '_ / _` / _| ' \\/ -_) | ' </ _` |  _| / / _`|\n \\___/|___| |_| \\___|_|   /_/ \\_| .__\\__,_\\__|_||_\\___| |_|\\_\\__,_|_| |_\\_\\__,|\n                                 |_|                                             \n"
  },
  {
    "path": "kafka-ui-api/src/main/resources/logback-spring.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%black(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%c{1}): %msg%n%throwable</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"info\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n\n</configuration>\n"
  },
  {
    "path": "kafka-ui-api/src/main/resources/static/static/css/bootstrap.min.css",
    "content": "/*!\n * Bootstrap v4.0.0-beta (https://getbootstrap.com)\n * Copyright 2011-2017 The Bootstrap Authors\n * Copyright 2011-2017 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}abbr[title]::after{content:\" (\" attr(title) \")\"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}html{box-sizing:border-box;font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}*,::after,::before{box-sizing:inherit}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff}[tabindex=\"-1\"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}[role=button],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#868e96;text-align:left;caption-side:bottom}th{text-align:left}label{display:inline-block;margin-bottom:.5rem}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.1}.display-2{font-size:5.5rem;font-weight:300;line-height:1.1}.display-3{font-size:4.5rem;font-weight:300;line-height:1.1}.display-4{font-size:3.5rem;font-weight:300;line-height:1.1}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:5px}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#868e96}.blockquote-footer::before{content:\"\\2014 \\00A0\"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #ddd;border-radius:.25rem;transition:all .2s ease-in-out;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#868e96}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace}code{padding:.2rem .4rem;font-size:90%;color:#bd4147;background-color:#f8f9fa;border-radius:.25rem}a>code{padding:0;color:inherit;background-color:inherit}kbd{padding:.2rem .4rem;font-size:90%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;margin-top:0;margin-bottom:1rem;font-size:90%;color:#212529}pre code{padding:0;font-size:inherit;color:inherit;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-right:15px;padding-left:15px;width:100%}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;margin-right:auto;margin-left:auto;padding-right:15px;padding-left:15px;width:100%}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}}.table{width:100%;max-width:100%;margin-bottom:1rem;background-color:transparent}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #e9ecef}.table thead th{vertical-align:bottom;border-bottom:2px solid #e9ecef}.table tbody+tbody{border-top:2px solid #e9ecef}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #e9ecef}.table-bordered td,.table-bordered th{border:1px solid #e9ecef}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#dddfe2}.table-hover .table-secondary:hover{background-color:#cfd2d6}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#cfd2d6}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.thead-inverse th{color:#fff;background-color:#212529}.thead-default th{color:#495057;background-color:#e9ecef}.table-inverse{color:#fff;background-color:#212529}.table-inverse td,.table-inverse th,.table-inverse thead th{border-color:#32383e}.table-inverse.table-bordered{border:0}.table-inverse.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-inverse.table-hover tbody tr:hover{background-color:rgba(255,255,255,.075)}@media (max-width:991px){.table-responsive{display:block;width:100%;overflow-x:auto;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive.table-bordered{border:0}}.form-control{display:block;width:100%;padding:.5rem .75rem;font-size:1rem;line-height:1.25;color:#495057;background-color:#fff;background-image:none;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0}.form-control::-webkit-input-placeholder{color:#868e96;opacity:1}.form-control:-ms-input-placeholder{color:#868e96;opacity:1}.form-control::placeholder{color:#868e96;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:not([size]):not([multiple]){height:calc(2.25rem + 2px)}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block}.col-form-label{padding-top:calc(.5rem - 1px * 2);padding-bottom:calc(.5rem - 1px * 2);margin-bottom:0}.col-form-label-lg{padding-top:calc(.5rem - 1px * 2);padding-bottom:calc(.5rem - 1px * 2);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem - 1px * 2);padding-bottom:calc(.25rem - 1px * 2);font-size:.875rem}.col-form-legend{padding-top:.5rem;padding-bottom:.5rem;margin-bottom:0;font-size:1rem}.form-control-plaintext{padding-top:.5rem;padding-bottom:.5rem;margin-bottom:0;line-height:1.25;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm,.input-group-lg>.form-control-plaintext.form-control,.input-group-lg>.form-control-plaintext.input-group-addon,.input-group-lg>.input-group-btn>.form-control-plaintext.btn,.input-group-sm>.form-control-plaintext.form-control,.input-group-sm>.form-control-plaintext.input-group-addon,.input-group-sm>.input-group-btn>.form-control-plaintext.btn{padding-right:0;padding-left:0}.form-control-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-sm>.input-group-btn>select.btn:not([size]):not([multiple]),.input-group-sm>select.form-control:not([size]):not([multiple]),.input-group-sm>select.input-group-addon:not([size]):not([multiple]),select.form-control-sm:not([size]):not([multiple]){height:calc(1.8125rem + 2px)}.form-control-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-lg>.input-group-btn>select.btn:not([size]):not([multiple]),.input-group-lg>select.form-control:not([size]):not([multiple]),.input-group-lg>select.input-group-addon:not([size]):not([multiple]),select.form-control-lg:not([size]):not([multiple]){height:calc(2.3125rem + 2px)}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;margin-bottom:.5rem}.form-check.disabled .form-check-label{color:#868e96}.form-check-label{padding-left:1.25rem;margin-bottom:0}.form-check-input{position:absolute;margin-top:.25rem;margin-left:-1.25rem}.form-check-input:only-child{position:static}.form-check-inline{display:inline-block}.form-check-inline .form-check-label{vertical-align:middle}.form-check-inline+.form-check-inline{margin-left:.75rem}.invalid-feedback{display:none;margin-top:.25rem;font-size:.875rem;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;width:250px;padding:.5rem;margin-top:.1rem;font-size:.875rem;line-height:1;color:#fff;background-color:rgba(220,53,69,.8);border-radius:.2rem}.custom-select.is-valid,.form-control.is-valid,.was-validated .custom-select:valid,.was-validated .form-control:valid{border-color:#28a745}.custom-select.is-valid:focus,.form-control.is-valid:focus,.was-validated .custom-select:valid:focus,.was-validated .form-control:valid:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-select.is-valid~.invalid-feedback,.custom-select.is-valid~.invalid-tooltip,.form-control.is-valid~.invalid-feedback,.form-control.is-valid~.invalid-tooltip,.was-validated .custom-select:valid~.invalid-feedback,.was-validated .custom-select:valid~.invalid-tooltip,.was-validated .form-control:valid~.invalid-feedback,.was-validated .form-control:valid~.invalid-tooltip{display:block}.form-check-input.is-valid+.form-check-label,.was-validated .form-check-input:valid+.form-check-label{color:#28a745}.custom-control-input.is-valid~.custom-control-indicator,.was-validated .custom-control-input:valid~.custom-control-indicator{background-color:rgba(40,167,69,.25)}.custom-control-input.is-valid~.custom-control-description,.was-validated .custom-control-input:valid~.custom-control-description{color:#28a745}.custom-file-input.is-valid~.custom-file-control,.was-validated .custom-file-input:valid~.custom-file-control{border-color:#28a745}.custom-file-input.is-valid~.custom-file-control::before,.was-validated .custom-file-input:valid~.custom-file-control::before{border-color:inherit}.custom-file-input.is-valid:focus,.was-validated .custom-file-input:valid:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-select.is-invalid,.form-control.is-invalid,.was-validated .custom-select:invalid,.was-validated .form-control:invalid{border-color:#dc3545}.custom-select.is-invalid:focus,.form-control.is-invalid:focus,.was-validated .custom-select:invalid:focus,.was-validated .form-control:invalid:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid+.form-check-label,.was-validated .form-check-input:invalid+.form-check-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-indicator,.was-validated .custom-control-input:invalid~.custom-control-indicator{background-color:rgba(220,53,69,.25)}.custom-control-input.is-invalid~.custom-control-description,.was-validated .custom-control-input:invalid~.custom-control-description{color:#dc3545}.custom-file-input.is-invalid~.custom-file-control,.was-validated .custom-file-input:invalid~.custom-file-control{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-control::before,.was-validated .custom-file-input:invalid~.custom-file-control::before{border-color:inherit}.custom-file-input.is-invalid:focus,.was-validated .custom-file-input:invalid:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .input-group{width:auto}.form-inline .form-control-label{margin-bottom:0;vertical-align:middle}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;margin-top:0;margin-bottom:0}.form-inline .form-check-label{padding-left:0}.form-inline .form-check-input{position:relative;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;padding-left:0}.form-inline .custom-control-indicator{position:static;display:inline-block;margin-right:.25rem;vertical-align:text-bottom}.form-inline .has-feedback .form-control-feedback{top:0}}.btn{display:inline-block;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid transparent;padding:.5rem .75rem;font-size:1rem;line-height:1.25;border-radius:.25rem;transition:all .15s ease-in-out}.btn:focus,.btn:hover{text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 3px rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn.active,.btn:active{background-image:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 3px rgba(0,123,255,.5)}.btn-primary.disabled,.btn-primary:disabled{background-color:#007bff;border-color:#007bff}.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{background-color:#0069d9;background-image:none;border-color:#0062cc}.btn-secondary{color:#fff;background-color:#868e96;border-color:#868e96}.btn-secondary:hover{color:#fff;background-color:#727b84;border-color:#6c757d}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 3px rgba(134,142,150,.5)}.btn-secondary.disabled,.btn-secondary:disabled{background-color:#868e96;border-color:#868e96}.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{background-color:#727b84;background-image:none;border-color:#6c757d}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 3px rgba(40,167,69,.5)}.btn-success.disabled,.btn-success:disabled{background-color:#28a745;border-color:#28a745}.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{background-color:#218838;background-image:none;border-color:#1e7e34}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 3px rgba(23,162,184,.5)}.btn-info.disabled,.btn-info:disabled{background-color:#17a2b8;border-color:#17a2b8}.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{background-color:#138496;background-image:none;border-color:#117a8b}.btn-warning{color:#111;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#111;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 3px rgba(255,193,7,.5)}.btn-warning.disabled,.btn-warning:disabled{background-color:#ffc107;border-color:#ffc107}.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{background-color:#e0a800;background-image:none;border-color:#d39e00}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 3px rgba(220,53,69,.5)}.btn-danger.disabled,.btn-danger:disabled{background-color:#dc3545;border-color:#dc3545}.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{background-color:#c82333;background-image:none;border-color:#bd2130}.btn-light{color:#111;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#111;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 3px rgba(248,249,250,.5)}.btn-light.disabled,.btn-light:disabled{background-color:#f8f9fa;border-color:#f8f9fa}.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{background-color:#e2e6ea;background-image:none;border-color:#dae0e5}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 3px rgba(52,58,64,.5)}.btn-dark.disabled,.btn-dark:disabled{background-color:#343a40;border-color:#343a40}.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{background-color:#23272b;background-image:none;border-color:#1d2124}.btn-outline-primary{color:#007bff;background-color:transparent;background-image:none;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 3px rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary.active,.btn-outline-primary:active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-secondary{color:#868e96;background-color:transparent;background-image:none;border-color:#868e96}.btn-outline-secondary:hover{color:#fff;background-color:#868e96;border-color:#868e96}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 3px rgba(134,142,150,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#868e96;background-color:transparent}.btn-outline-secondary.active,.btn-outline-secondary:active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#868e96;border-color:#868e96}.btn-outline-success{color:#28a745;background-color:transparent;background-image:none;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 3px rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success.active,.btn-outline-success:active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-info{color:#17a2b8;background-color:transparent;background-image:none;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 3px rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info.active,.btn-outline-info:active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-warning{color:#ffc107;background-color:transparent;background-image:none;border-color:#ffc107}.btn-outline-warning:hover{color:#fff;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 3px rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning.active,.btn-outline-warning:active,.show>.btn-outline-warning.dropdown-toggle{color:#fff;background-color:#ffc107;border-color:#ffc107}.btn-outline-danger{color:#dc3545;background-color:transparent;background-image:none;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 3px rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger.active,.btn-outline-danger:active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-light{color:#f8f9fa;background-color:transparent;background-image:none;border-color:#f8f9fa}.btn-outline-light:hover{color:#fff;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 3px rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light.active,.btn-outline-light:active,.show>.btn-outline-light.dropdown-toggle{color:#fff;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-dark{color:#343a40;background-color:transparent;background-image:none;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 3px rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark.active,.btn-outline-dark:active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-link{font-weight:400;color:#007bff;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link:disabled{background-color:transparent}.btn-link,.btn-link:active,.btn-link:focus{border-color:transparent;box-shadow:none}.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#0056b3;text-decoration:underline;background-color:transparent}.btn-link:disabled{color:#868e96}.btn-link:disabled:focus,.btn-link:disabled:hover{text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;transition:opacity .15s linear}.fade.show{opacity:1}.collapse{display:none}.collapse.show{display:block}tr.collapse.show{display:table-row}tbody.collapse.show{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}.dropdown,.dropup{position:relative}.dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:\"\";border-top:.3em solid;border-right:.3em solid transparent;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropup .dropdown-menu{margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{border-top:0;border-bottom:.3em solid}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background:0 0;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#868e96;background-color:transparent}.show>a{outline:0}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#868e96;white-space:nowrap}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:0 1 auto;flex:0 1 auto;margin-bottom:0}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:2}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group,.btn-group-vertical .btn+.btn,.btn-group-vertical .btn+.btn-group,.btn-group-vertical .btn-group+.btn,.btn-group-vertical .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn+.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.btn+.dropdown-toggle-split::after{margin-left:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical .btn,.btn-group-vertical .btn-group{width:100%}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;width:100%}.input-group .form-control{position:relative;z-index:2;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group .form-control:active,.input-group .form-control:focus,.input-group .form-control:hover{z-index:3}.input-group .form-control,.input-group-addon,.input-group-btn{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{white-space:nowrap;vertical-align:middle}.input-group-addon{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.25;color:#495057;text-align:center;background-color:#e9ecef;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.input-group-addon.form-control-sm,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-addon.form-control-lg,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:not(:last-child),.input-group-addon:not(:last-child),.input-group-btn:not(:first-child)>.btn-group:not(:last-child)>.btn,.input-group-btn:not(:first-child)>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:not(:last-child)>.btn,.input-group-btn:not(:last-child)>.btn-group>.btn,.input-group-btn:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:not(:last-child){border-right:0}.input-group .form-control:not(:first-child),.input-group-addon:not(:first-child),.input-group-btn:not(:first-child)>.btn,.input-group-btn:not(:first-child)>.btn-group>.btn,.input-group-btn:not(:first-child)>.dropdown-toggle,.input-group-btn:not(:last-child)>.btn-group:not(:first-child)>.btn,.input-group-btn:not(:last-child)>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.form-control+.input-group-addon:not(:first-child){border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:3}.input-group-btn:not(:last-child)>.btn,.input-group-btn:not(:last-child)>.btn-group{margin-right:-1px}.input-group-btn:not(:first-child)>.btn,.input-group-btn:not(:first-child)>.btn-group{z-index:2;margin-left:-1px}.input-group-btn:not(:first-child)>.btn-group:active,.input-group-btn:not(:first-child)>.btn-group:focus,.input-group-btn:not(:first-child)>.btn-group:hover,.input-group-btn:not(:first-child)>.btn:active,.input-group-btn:not(:first-child)>.btn:focus,.input-group-btn:not(:first-child)>.btn:hover{z-index:3}.custom-control{position:relative;display:-ms-inline-flexbox;display:inline-flex;min-height:1.5rem;padding-left:1.5rem;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-indicator{color:#fff;background-color:#007bff}.custom-control-input:focus~.custom-control-indicator{box-shadow:0 0 0 1px #fff,0 0 0 3px #007bff}.custom-control-input:active~.custom-control-indicator{color:#fff;background-color:#b3d7ff}.custom-control-input:disabled~.custom-control-indicator{background-color:#e9ecef}.custom-control-input:disabled~.custom-control-description{color:#868e96}.custom-control-indicator{position:absolute;top:.25rem;left:0;display:block;width:1rem;height:1rem;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#ddd;background-repeat:no-repeat;background-position:center center;background-size:50% 50%}.custom-checkbox .custom-control-indicator{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-indicator{background-image:url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E\")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-indicator{background-color:#007bff;background-image:url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E\")}.custom-radio .custom-control-indicator{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-indicator{background-image:url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E\")}.custom-controls-stacked{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.custom-controls-stacked .custom-control{margin-bottom:.25rem}.custom-controls-stacked .custom-control+.custom-control{margin-left:0}.custom-select{display:inline-block;max-width:100%;height:calc(2.25rem + 2px);padding:.375rem 1.75rem .375rem .75rem;line-height:1.25;color:#495057;vertical-align:middle;background:#fff url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23333' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E\") no-repeat right .75rem center;background-size:8px 10px;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select:disabled{color:#868e96;background-color:#e9ecef}.custom-select::-ms-expand{opacity:0}.custom-select-sm{height:calc(1.8125rem + 2px);padding-top:.375rem;padding-bottom:.375rem;font-size:75%}.custom-file{position:relative;display:inline-block;max-width:100%;height:2.5rem;margin-bottom:0}.custom-file-input{min-width:14rem;max-width:100%;height:2.5rem;margin:0;opacity:0}.custom-file-control{position:absolute;top:0;right:0;left:0;z-index:5;height:2.5rem;padding:.5rem 1rem;line-height:1.5;color:#495057;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#fff;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.custom-file-control:lang(en):empty::after{content:\"Choose file...\"}.custom-file-control::before{position:absolute;top:-1px;right:-1px;bottom:-1px;z-index:6;display:block;height:2.5rem;padding:.5rem 1rem;line-height:1.5;color:#495057;background-color:#e9ecef;border:1px solid rgba(0,0,0,.15);border-radius:0 .25rem .25rem 0}.custom-file-control:lang(en)::before{content:\"Browse\"}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#868e96}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #ddd}.nav-tabs .nav-link.disabled{color:#868e96;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#ddd #ddd #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.show>.nav-pills .nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background:0 0;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:\"\";background:no-repeat center center;background-size:100% 100%}@media (max-width:575px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url(\"data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E\")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url(\"data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E\")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}@media (min-width:576px){.card-deck{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-ms-flexbox;display:flex;-ms-flex:1 0 0%;flex:1 0 0%;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-left:15px}}@media (min-width:576px){.card-group{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group .card{-ms-flex:1 0 0%;flex:1 0 0%}.card-group .card+.card{margin-left:0;border-left:0}.card-group .card:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.card-group .card:first-child .card-img-top{border-top-right-radius:0}.card-group .card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group .card:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.card-group .card:last-child .card-img-top{border-top-left-radius:0}.card-group .card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group .card:not(:first-child):not(:last-child){border-radius:0}.card-group .card:not(:first-child):not(:last-child) .card-img-bottom,.card-group .card:not(:first-child):not(:last-child) .card-img-top{border-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;column-count:3;-webkit-column-gap:1.25rem;column-gap:1.25rem}.card-columns .card{display:inline-block;width:100%}}.breadcrumb{padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb::after{display:block;clear:both;content:\"\"}.breadcrumb-item{float:left}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;padding-left:.5rem;color:#868e96;content:\"/\"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#868e96}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#868e96;pointer-events:none;background-color:#fff;border-color:#ddd}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #ddd}.page-link:focus,.page-link:hover{color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#ddd}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}.badge-primary[href]:focus,.badge-primary[href]:hover{color:#fff;text-decoration:none;background-color:#0062cc}.badge-secondary{color:#fff;background-color:#868e96}.badge-secondary[href]:focus,.badge-secondary[href]:hover{color:#fff;text-decoration:none;background-color:#6c757d}.badge-success{color:#fff;background-color:#28a745}.badge-success[href]:focus,.badge-success[href]:hover{color:#fff;text-decoration:none;background-color:#1e7e34}.badge-info{color:#fff;background-color:#17a2b8}.badge-info[href]:focus,.badge-info[href]:hover{color:#fff;text-decoration:none;background-color:#117a8b}.badge-warning{color:#111;background-color:#ffc107}.badge-warning[href]:focus,.badge-warning[href]:hover{color:#111;text-decoration:none;background-color:#d39e00}.badge-danger{color:#fff;background-color:#dc3545}.badge-danger[href]:focus,.badge-danger[href]:hover{color:#fff;text-decoration:none;background-color:#bd2130}.badge-light{color:#111;background-color:#f8f9fa}.badge-light[href]:focus,.badge-light[href]:hover{color:#111;text-decoration:none;background-color:#dae0e5}.badge-dark{color:#fff;background-color:#343a40}.badge-dark[href]:focus,.badge-dark[href]:hover{color:#fff;text-decoration:none;background-color:#1d2124}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible .close{position:relative;top:-.75rem;right:-1.25rem;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#464a4e;background-color:#e7e8ea;border-color:#dddfe2}.alert-secondary hr{border-top-color:#cfd2d6}.alert-secondary .alert-link{color:#2e3133}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;overflow:hidden;font-size:.75rem;line-height:1rem;text-align:center;background-color:#e9ecef;border-radius:.25rem}.progress-bar{height:1rem;line-height:1rem;color:#fff;background-color:#007bff;transition:width .6s ease}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item:focus,.list-group-item:hover{text-decoration:none}.list-group-item.disabled,.list-group-item:disabled{color:#868e96;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom:0}.list-group-item-primary{color:#004085;background-color:#b8daff}a.list-group-item-primary,button.list-group-item-primary{color:#004085}a.list-group-item-primary:focus,a.list-group-item-primary:hover,button.list-group-item-primary:focus,button.list-group-item-primary:hover{color:#004085;background-color:#9fcdff}a.list-group-item-primary.active,button.list-group-item-primary.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#464a4e;background-color:#dddfe2}a.list-group-item-secondary,button.list-group-item-secondary{color:#464a4e}a.list-group-item-secondary:focus,a.list-group-item-secondary:hover,button.list-group-item-secondary:focus,button.list-group-item-secondary:hover{color:#464a4e;background-color:#cfd2d6}a.list-group-item-secondary.active,button.list-group-item-secondary.active{color:#fff;background-color:#464a4e;border-color:#464a4e}.list-group-item-success{color:#155724;background-color:#c3e6cb}a.list-group-item-success,button.list-group-item-success{color:#155724}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#155724;background-color:#b1dfbb}a.list-group-item-success.active,button.list-group-item-success.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}a.list-group-item-info,button.list-group-item-info{color:#0c5460}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#0c5460;background-color:#abdde5}a.list-group-item-info.active,button.list-group-item-info.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}a.list-group-item-warning,button.list-group-item-warning{color:#856404}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#856404;background-color:#ffe8a1}a.list-group-item-warning.active,button.list-group-item-warning.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}a.list-group-item-danger,button.list-group-item-danger{color:#721c24}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#721c24;background-color:#f1b0b7}a.list-group-item-danger.active,button.list-group-item-danger.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}a.list-group-item-light,button.list-group-item-light{color:#818182}a.list-group-item-light:focus,a.list-group-item-light:hover,button.list-group-item-light:focus,button.list-group-item-light:hover{color:#818182;background-color:#ececf6}a.list-group-item-light.active,button.list-group-item-light.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}a.list-group-item-dark,button.list-group-item-dark{color:#1b1e21}a.list-group-item-dark:focus,a.list-group-item-dark:hover,button.list-group-item-dark:focus,button.list-group-item-dark:hover{color:#1b1e21;background-color:#b9bbbe}a.list-group-item-dark.active,button.list-group-item-dark.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:focus,.close:hover{color:#000;text-decoration:none;opacity:.75}button.close{padding:0;background:0 0;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;outline:0}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.show .modal-dialog{-webkit-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:15px;border-bottom:1px solid #e9ecef}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:15px}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:15px;border-top:1px solid #e9ecef}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:30px auto}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg{max-width:800px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:5px;height:5px}.tooltip.bs-tooltip-auto[x-placement^=top],.tooltip.bs-tooltip-top{padding:5px 0}.tooltip.bs-tooltip-auto[x-placement^=top] .arrow,.tooltip.bs-tooltip-top .arrow{bottom:0}.tooltip.bs-tooltip-auto[x-placement^=top] .arrow::before,.tooltip.bs-tooltip-top .arrow::before{margin-left:-3px;content:\"\";border-width:5px 5px 0;border-top-color:#000}.tooltip.bs-tooltip-auto[x-placement^=right],.tooltip.bs-tooltip-right{padding:0 5px}.tooltip.bs-tooltip-auto[x-placement^=right] .arrow,.tooltip.bs-tooltip-right .arrow{left:0}.tooltip.bs-tooltip-auto[x-placement^=right] .arrow::before,.tooltip.bs-tooltip-right .arrow::before{margin-top:-3px;content:\"\";border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.bs-tooltip-auto[x-placement^=bottom],.tooltip.bs-tooltip-bottom{padding:5px 0}.tooltip.bs-tooltip-auto[x-placement^=bottom] .arrow,.tooltip.bs-tooltip-bottom .arrow{top:0}.tooltip.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.tooltip.bs-tooltip-bottom .arrow::before{margin-left:-3px;content:\"\";border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bs-tooltip-auto[x-placement^=left],.tooltip.bs-tooltip-left{padding:0 5px}.tooltip.bs-tooltip-auto[x-placement^=left] .arrow,.tooltip.bs-tooltip-left .arrow{right:0}.tooltip.bs-tooltip-auto[x-placement^=left] .arrow::before,.tooltip.bs-tooltip-left .arrow::before{right:0;margin-top:-3px;content:\"\";border-width:5px 0 5px 5px;border-left-color:#000}.tooltip .arrow::before{position:absolute;border-color:transparent;border-style:solid}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;padding:1px;font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:10px;height:5px}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;border-color:transparent;border-style:solid}.popover .arrow::before{content:\"\";border-width:11px}.popover .arrow::after{content:\"\";border-width:11px}.popover.bs-popover-auto[x-placement^=top],.popover.bs-popover-top{margin-bottom:10px}.popover.bs-popover-auto[x-placement^=top] .arrow,.popover.bs-popover-top .arrow{bottom:0}.popover.bs-popover-auto[x-placement^=top] .arrow::after,.popover.bs-popover-auto[x-placement^=top] .arrow::before,.popover.bs-popover-top .arrow::after,.popover.bs-popover-top .arrow::before{border-bottom-width:0}.popover.bs-popover-auto[x-placement^=top] .arrow::before,.popover.bs-popover-top .arrow::before{bottom:-11px;margin-left:-6px;border-top-color:rgba(0,0,0,.25)}.popover.bs-popover-auto[x-placement^=top] .arrow::after,.popover.bs-popover-top .arrow::after{bottom:-10px;margin-left:-6px;border-top-color:#fff}.popover.bs-popover-auto[x-placement^=right],.popover.bs-popover-right{margin-left:10px}.popover.bs-popover-auto[x-placement^=right] .arrow,.popover.bs-popover-right .arrow{left:0}.popover.bs-popover-auto[x-placement^=right] .arrow::after,.popover.bs-popover-auto[x-placement^=right] .arrow::before,.popover.bs-popover-right .arrow::after,.popover.bs-popover-right .arrow::before{margin-top:-8px;border-left-width:0}.popover.bs-popover-auto[x-placement^=right] .arrow::before,.popover.bs-popover-right .arrow::before{left:-11px;border-right-color:rgba(0,0,0,.25)}.popover.bs-popover-auto[x-placement^=right] .arrow::after,.popover.bs-popover-right .arrow::after{left:-10px;border-right-color:#fff}.popover.bs-popover-auto[x-placement^=bottom],.popover.bs-popover-bottom{margin-top:10px}.popover.bs-popover-auto[x-placement^=bottom] .arrow,.popover.bs-popover-bottom .arrow{top:0}.popover.bs-popover-auto[x-placement^=bottom] .arrow::after,.popover.bs-popover-auto[x-placement^=bottom] .arrow::before,.popover.bs-popover-bottom .arrow::after,.popover.bs-popover-bottom .arrow::before{margin-left:-7px;border-top-width:0}.popover.bs-popover-auto[x-placement^=bottom] .arrow::before,.popover.bs-popover-bottom .arrow::before{top:-11px;border-bottom-color:rgba(0,0,0,.25)}.popover.bs-popover-auto[x-placement^=bottom] .arrow::after,.popover.bs-popover-bottom .arrow::after{top:-10px;border-bottom-color:#fff}.popover.bs-popover-auto[x-placement^=bottom] .popover-header::before,.popover.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:20px;margin-left:-10px;content:\"\";border-bottom:1px solid #f7f7f7}.popover.bs-popover-auto[x-placement^=left],.popover.bs-popover-left{margin-right:10px}.popover.bs-popover-auto[x-placement^=left] .arrow,.popover.bs-popover-left .arrow{right:0}.popover.bs-popover-auto[x-placement^=left] .arrow::after,.popover.bs-popover-auto[x-placement^=left] .arrow::before,.popover.bs-popover-left .arrow::after,.popover.bs-popover-left .arrow::before{margin-top:-8px;border-right-width:0}.popover.bs-popover-auto[x-placement^=left] .arrow::before,.popover.bs-popover-left .arrow::before{right:-11px;border-left-color:rgba(0,0,0,.25)}.popover.bs-popover-auto[x-placement^=left] .arrow::after,.popover.bs-popover-left .arrow::after{right:-10px;border-left-color:#fff}.popover-header{padding:8px 14px;margin-bottom:0;font-size:1rem;color:inherit;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:9px 14px;color:#212529}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-item{position:relative;display:none;-ms-flex-align:center;align-items:center;width:100%;transition:-webkit-transform .6s ease;transition:transform .6s ease;transition:transform .6s ease,-webkit-transform .6s ease;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.carousel-item-next,.carousel-item-prev{position:absolute;top:0}.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translateX(0);transform:translateX(0)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translateX(100%);transform:translateX(100%)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translateX(-100%);transform:translateX(-100%)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:transparent no-repeat center center;background-size:100% 100%}.carousel-control-prev-icon{background-image:url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M4 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E\")}.carousel-control-next-icon{background-image:url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M1.5 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E\")}.carousel-indicators{position:absolute;right:0;bottom:10px;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{position:relative;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;background-color:rgba(255,255,255,.5)}.carousel-indicators li::before{position:absolute;top:-10px;left:0;display:inline-block;width:100%;height:10px;content:\"\"}.carousel-indicators li::after{position:absolute;bottom:-10px;left:0;display:inline-block;width:100%;height:10px;content:\"\"}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#868e96!important}a.bg-secondary:focus,a.bg-secondary:hover{background-color:#6c757d!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #e9ecef!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#868e96!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-circle{border-radius:50%}.rounded-0{border-radius:0}.clearfix::after{display:block;clear:both;content:\"\"}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.d-print-block{display:none!important}@media print{.d-print-block{display:block!important}}.d-print-inline{display:none!important}@media print{.d-print-inline{display:inline!important}}.d-print-inline-block{display:none!important}@media print{.d-print-inline-block{display:inline-block!important}}@media print{.d-print-none{display:none!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:\"\"}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;-webkit-clip-path:inset(50%);clip-path:inset(50%);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal;-webkit-clip-path:none;clip-path:none}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.m-0{margin:0!important}.mt-0{margin-top:0!important}.mr-0{margin-right:0!important}.mb-0{margin-bottom:0!important}.ml-0{margin-left:0!important}.mx-0{margin-right:0!important;margin-left:0!important}.my-0{margin-top:0!important;margin-bottom:0!important}.m-1{margin:.25rem!important}.mt-1{margin-top:.25rem!important}.mr-1{margin-right:.25rem!important}.mb-1{margin-bottom:.25rem!important}.ml-1{margin-left:.25rem!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-2{margin:.5rem!important}.mt-2{margin-top:.5rem!important}.mr-2{margin-right:.5rem!important}.mb-2{margin-bottom:.5rem!important}.ml-2{margin-left:.5rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-3{margin:1rem!important}.mt-3{margin-top:1rem!important}.mr-3{margin-right:1rem!important}.mb-3{margin-bottom:1rem!important}.ml-3{margin-left:1rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-4{margin:1.5rem!important}.mt-4{margin-top:1.5rem!important}.mr-4{margin-right:1.5rem!important}.mb-4{margin-bottom:1.5rem!important}.ml-4{margin-left:1.5rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-5{margin:3rem!important}.mt-5{margin-top:3rem!important}.mr-5{margin-right:3rem!important}.mb-5{margin-bottom:3rem!important}.ml-5{margin-left:3rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-0{padding:0!important}.pt-0{padding-top:0!important}.pr-0{padding-right:0!important}.pb-0{padding-bottom:0!important}.pl-0{padding-left:0!important}.px-0{padding-right:0!important;padding-left:0!important}.py-0{padding-top:0!important;padding-bottom:0!important}.p-1{padding:.25rem!important}.pt-1{padding-top:.25rem!important}.pr-1{padding-right:.25rem!important}.pb-1{padding-bottom:.25rem!important}.pl-1{padding-left:.25rem!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-2{padding:.5rem!important}.pt-2{padding-top:.5rem!important}.pr-2{padding-right:.5rem!important}.pb-2{padding-bottom:.5rem!important}.pl-2{padding-left:.5rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-3{padding:1rem!important}.pt-3{padding-top:1rem!important}.pr-3{padding-right:1rem!important}.pb-3{padding-bottom:1rem!important}.pl-3{padding-left:1rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-4{padding:1.5rem!important}.pt-4{padding-top:1.5rem!important}.pr-4{padding-right:1.5rem!important}.pb-4{padding-bottom:1.5rem!important}.pl-4{padding-left:1.5rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-5{padding:3rem!important}.pt-5{padding-top:3rem!important}.pr-5{padding-right:3rem!important}.pb-5{padding-bottom:3rem!important}.pl-5{padding-left:3rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-auto{margin:auto!important}.mt-auto{margin-top:auto!important}.mr-auto{margin-right:auto!important}.mb-auto{margin-bottom:auto!important}.ml-auto{margin-left:auto!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0{margin-top:0!important}.mr-sm-0{margin-right:0!important}.mb-sm-0{margin-bottom:0!important}.ml-sm-0{margin-left:0!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1{margin-top:.25rem!important}.mr-sm-1{margin-right:.25rem!important}.mb-sm-1{margin-bottom:.25rem!important}.ml-sm-1{margin-left:.25rem!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2{margin-top:.5rem!important}.mr-sm-2{margin-right:.5rem!important}.mb-sm-2{margin-bottom:.5rem!important}.ml-sm-2{margin-left:.5rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3{margin-top:1rem!important}.mr-sm-3{margin-right:1rem!important}.mb-sm-3{margin-bottom:1rem!important}.ml-sm-3{margin-left:1rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4{margin-top:1.5rem!important}.mr-sm-4{margin-right:1.5rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.ml-sm-4{margin-left:1.5rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5{margin-top:3rem!important}.mr-sm-5{margin-right:3rem!important}.mb-sm-5{margin-bottom:3rem!important}.ml-sm-5{margin-left:3rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0{padding-top:0!important}.pr-sm-0{padding-right:0!important}.pb-sm-0{padding-bottom:0!important}.pl-sm-0{padding-left:0!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1{padding-top:.25rem!important}.pr-sm-1{padding-right:.25rem!important}.pb-sm-1{padding-bottom:.25rem!important}.pl-sm-1{padding-left:.25rem!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2{padding-top:.5rem!important}.pr-sm-2{padding-right:.5rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pl-sm-2{padding-left:.5rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3{padding-top:1rem!important}.pr-sm-3{padding-right:1rem!important}.pb-sm-3{padding-bottom:1rem!important}.pl-sm-3{padding-left:1rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4{padding-top:1.5rem!important}.pr-sm-4{padding-right:1.5rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pl-sm-4{padding-left:1.5rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5{padding-top:3rem!important}.pr-sm-5{padding-right:3rem!important}.pb-sm-5{padding-bottom:3rem!important}.pl-sm-5{padding-left:3rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto{margin-top:auto!important}.mr-sm-auto{margin-right:auto!important}.mb-sm-auto{margin-bottom:auto!important}.ml-sm-auto{margin-left:auto!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0{margin-top:0!important}.mr-md-0{margin-right:0!important}.mb-md-0{margin-bottom:0!important}.ml-md-0{margin-left:0!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.m-md-1{margin:.25rem!important}.mt-md-1{margin-top:.25rem!important}.mr-md-1{margin-right:.25rem!important}.mb-md-1{margin-bottom:.25rem!important}.ml-md-1{margin-left:.25rem!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2{margin-top:.5rem!important}.mr-md-2{margin-right:.5rem!important}.mb-md-2{margin-bottom:.5rem!important}.ml-md-2{margin-left:.5rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3{margin-top:1rem!important}.mr-md-3{margin-right:1rem!important}.mb-md-3{margin-bottom:1rem!important}.ml-md-3{margin-left:1rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4{margin-top:1.5rem!important}.mr-md-4{margin-right:1.5rem!important}.mb-md-4{margin-bottom:1.5rem!important}.ml-md-4{margin-left:1.5rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5{margin-top:3rem!important}.mr-md-5{margin-right:3rem!important}.mb-md-5{margin-bottom:3rem!important}.ml-md-5{margin-left:3rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-md-0{padding:0!important}.pt-md-0{padding-top:0!important}.pr-md-0{padding-right:0!important}.pb-md-0{padding-bottom:0!important}.pl-md-0{padding-left:0!important}.px-md-0{padding-right:0!important;padding-left:0!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.p-md-1{padding:.25rem!important}.pt-md-1{padding-top:.25rem!important}.pr-md-1{padding-right:.25rem!important}.pb-md-1{padding-bottom:.25rem!important}.pl-md-1{padding-left:.25rem!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2{padding-top:.5rem!important}.pr-md-2{padding-right:.5rem!important}.pb-md-2{padding-bottom:.5rem!important}.pl-md-2{padding-left:.5rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3{padding-top:1rem!important}.pr-md-3{padding-right:1rem!important}.pb-md-3{padding-bottom:1rem!important}.pl-md-3{padding-left:1rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4{padding-top:1.5rem!important}.pr-md-4{padding-right:1.5rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pl-md-4{padding-left:1.5rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5{padding-top:3rem!important}.pr-md-5{padding-right:3rem!important}.pb-md-5{padding-bottom:3rem!important}.pl-md-5{padding-left:3rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto{margin-top:auto!important}.mr-md-auto{margin-right:auto!important}.mb-md-auto{margin-bottom:auto!important}.ml-md-auto{margin-left:auto!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0{margin-top:0!important}.mr-lg-0{margin-right:0!important}.mb-lg-0{margin-bottom:0!important}.ml-lg-0{margin-left:0!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1{margin-top:.25rem!important}.mr-lg-1{margin-right:.25rem!important}.mb-lg-1{margin-bottom:.25rem!important}.ml-lg-1{margin-left:.25rem!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2{margin-top:.5rem!important}.mr-lg-2{margin-right:.5rem!important}.mb-lg-2{margin-bottom:.5rem!important}.ml-lg-2{margin-left:.5rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3{margin-top:1rem!important}.mr-lg-3{margin-right:1rem!important}.mb-lg-3{margin-bottom:1rem!important}.ml-lg-3{margin-left:1rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4{margin-top:1.5rem!important}.mr-lg-4{margin-right:1.5rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.ml-lg-4{margin-left:1.5rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5{margin-top:3rem!important}.mr-lg-5{margin-right:3rem!important}.mb-lg-5{margin-bottom:3rem!important}.ml-lg-5{margin-left:3rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0{padding-top:0!important}.pr-lg-0{padding-right:0!important}.pb-lg-0{padding-bottom:0!important}.pl-lg-0{padding-left:0!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1{padding-top:.25rem!important}.pr-lg-1{padding-right:.25rem!important}.pb-lg-1{padding-bottom:.25rem!important}.pl-lg-1{padding-left:.25rem!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2{padding-top:.5rem!important}.pr-lg-2{padding-right:.5rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pl-lg-2{padding-left:.5rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3{padding-top:1rem!important}.pr-lg-3{padding-right:1rem!important}.pb-lg-3{padding-bottom:1rem!important}.pl-lg-3{padding-left:1rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4{padding-top:1.5rem!important}.pr-lg-4{padding-right:1.5rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pl-lg-4{padding-left:1.5rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5{padding-top:3rem!important}.pr-lg-5{padding-right:3rem!important}.pb-lg-5{padding-bottom:3rem!important}.pl-lg-5{padding-left:3rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto{margin-top:auto!important}.mr-lg-auto{margin-right:auto!important}.mb-lg-auto{margin-bottom:auto!important}.ml-lg-auto{margin-left:auto!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0{margin-top:0!important}.mr-xl-0{margin-right:0!important}.mb-xl-0{margin-bottom:0!important}.ml-xl-0{margin-left:0!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1{margin-top:.25rem!important}.mr-xl-1{margin-right:.25rem!important}.mb-xl-1{margin-bottom:.25rem!important}.ml-xl-1{margin-left:.25rem!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2{margin-top:.5rem!important}.mr-xl-2{margin-right:.5rem!important}.mb-xl-2{margin-bottom:.5rem!important}.ml-xl-2{margin-left:.5rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3{margin-top:1rem!important}.mr-xl-3{margin-right:1rem!important}.mb-xl-3{margin-bottom:1rem!important}.ml-xl-3{margin-left:1rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4{margin-top:1.5rem!important}.mr-xl-4{margin-right:1.5rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.ml-xl-4{margin-left:1.5rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5{margin-top:3rem!important}.mr-xl-5{margin-right:3rem!important}.mb-xl-5{margin-bottom:3rem!important}.ml-xl-5{margin-left:3rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0{padding-top:0!important}.pr-xl-0{padding-right:0!important}.pb-xl-0{padding-bottom:0!important}.pl-xl-0{padding-left:0!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1{padding-top:.25rem!important}.pr-xl-1{padding-right:.25rem!important}.pb-xl-1{padding-bottom:.25rem!important}.pl-xl-1{padding-left:.25rem!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2{padding-top:.5rem!important}.pr-xl-2{padding-right:.5rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pl-xl-2{padding-left:.5rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3{padding-top:1rem!important}.pr-xl-3{padding-right:1rem!important}.pb-xl-3{padding-bottom:1rem!important}.pl-xl-3{padding-left:1rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4{padding-top:1.5rem!important}.pr-xl-4{padding-right:1.5rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pl-xl-4{padding-left:1.5rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5{padding-top:3rem!important}.pr-xl-5{padding-right:3rem!important}.pb-xl-5{padding-bottom:3rem!important}.pl-xl-5{padding-left:3rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto{margin-top:auto!important}.mr-xl-auto{margin-right:auto!important}.mb-xl-auto{margin-bottom:auto!important}.ml-xl-auto{margin-left:auto!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}}.text-justify{text-align:justify!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-normal{font-weight:400}.font-weight-bold{font-weight:700}.font-italic{font-style:italic}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0062cc!important}.text-secondary{color:#868e96!important}a.text-secondary:focus,a.text-secondary:hover{color:#6c757d!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#1e7e34!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#117a8b!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#d39e00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#bd2130!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#dae0e5!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#1d2124!important}.text-muted{color:#868e96!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.visible{visibility:visible!important}.invisible{visibility:hidden!important}\n/*# sourceMappingURL=bootstrap.min.css.map */"
  },
  {
    "path": "kafka-ui-api/src/main/resources/static/static/css/signin.css",
    "content": "body {\n  padding-top: 40px;\n  padding-bottom: 40px;\n  background-color: #eee;\n}\n\n.form-signin {\n  max-width: 330px;\n  padding: 15px;\n  margin: 0 auto;\n}\n.form-signin .form-signin-heading,\n.form-signin .checkbox {\n  margin-bottom: 10px;\n}\n.form-signin .checkbox {\n  font-weight: 400;\n}\n.form-signin .form-control {\n  position: relative;\n  box-sizing: border-box;\n  height: auto;\n  padding: 10px;\n  font-size: 16px;\n}\n.form-signin .form-control:focus {\n  z-index: 2;\n}\n.form-signin input[type=\"email\"] {\n  margin-bottom: -1px;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.form-signin input[type=\"password\"] {\n  margin-bottom: 10px;\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/AbstractIntegrationTest.java",
    "content": "package com.provectus.kafka.ui;\n\nimport com.provectus.kafka.ui.container.KafkaConnectContainer;\nimport com.provectus.kafka.ui.container.KsqlDbContainer;\nimport com.provectus.kafka.ui.container.SchemaRegistryContainer;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.Properties;\nimport org.apache.kafka.clients.admin.AdminClient;\nimport org.apache.kafka.clients.admin.AdminClientConfig;\nimport org.apache.kafka.clients.admin.NewTopic;\nimport org.jetbrains.annotations.NotNull;\nimport org.junit.jupiter.api.function.ThrowingConsumer;\nimport org.junit.jupiter.api.io.TempDir;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.context.ApplicationContextInitializer;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.springframework.test.context.ActiveProfiles;\nimport org.springframework.test.context.ContextConfiguration;\nimport org.springframework.test.util.TestSocketUtils;\nimport org.testcontainers.containers.KafkaContainer;\nimport org.testcontainers.containers.Network;\nimport org.testcontainers.utility.DockerImageName;\n\n\n@SpringBootTest\n@ActiveProfiles(\"test\")\n@AutoConfigureWebTestClient(timeout = \"60000\")\n@ContextConfiguration(initializers = {AbstractIntegrationTest.Initializer.class})\npublic abstract class AbstractIntegrationTest {\n  public static final String LOCAL = \"local\";\n  public static final String SECOND_LOCAL = \"secondLocal\";\n\n  private static final String CONFLUENT_PLATFORM_VERSION = \"7.2.1\"; // Append \".arm64\" for a local run\n\n  public static final KafkaContainer kafka = new KafkaContainer(\n      DockerImageName.parse(\"confluentinc/cp-kafka\").withTag(CONFLUENT_PLATFORM_VERSION))\n      .withNetwork(Network.SHARED);\n\n  public static final SchemaRegistryContainer schemaRegistry =\n      new SchemaRegistryContainer(CONFLUENT_PLATFORM_VERSION)\n          .withKafka(kafka)\n          .dependsOn(kafka);\n\n  public static final KafkaConnectContainer kafkaConnect =\n      new KafkaConnectContainer(CONFLUENT_PLATFORM_VERSION)\n          .withKafka(kafka)\n          .dependsOn(kafka)\n          .dependsOn(schemaRegistry);\n\n  protected static final KsqlDbContainer KSQL_DB = new KsqlDbContainer(\n      DockerImageName.parse(\"confluentinc/cp-ksqldb-server\")\n          .withTag(CONFLUENT_PLATFORM_VERSION))\n      .withKafka(kafka);\n\n  @TempDir\n  public static Path tmpDir;\n\n  static {\n    kafka.start();\n    schemaRegistry.start();\n    kafkaConnect.start();\n  }\n\n  public static class Initializer\n      implements ApplicationContextInitializer<ConfigurableApplicationContext> {\n    @Override\n    public void initialize(@NotNull ConfigurableApplicationContext context) {\n      System.setProperty(\"kafka.clusters.0.name\", LOCAL);\n      System.setProperty(\"kafka.clusters.0.bootstrapServers\", kafka.getBootstrapServers());\n      // List unavailable hosts to verify failover\n      System.setProperty(\"kafka.clusters.0.schemaRegistry\", String.format(\"http://localhost:%1$s,http://localhost:%1$s,%2$s\",\n              TestSocketUtils.findAvailableTcpPort(), schemaRegistry.getUrl()));\n      System.setProperty(\"kafka.clusters.0.kafkaConnect.0.name\", \"kafka-connect\");\n      System.setProperty(\"kafka.clusters.0.kafkaConnect.0.userName\", \"kafka-connect\");\n      System.setProperty(\"kafka.clusters.0.kafkaConnect.0.password\", \"kafka-connect\");\n      System.setProperty(\"kafka.clusters.0.kafkaConnect.0.address\", kafkaConnect.getTarget());\n      System.setProperty(\"kafka.clusters.0.kafkaConnect.1.name\", \"notavailable\");\n      System.setProperty(\"kafka.clusters.0.kafkaConnect.1.address\", \"http://notavailable:6666\");\n      System.setProperty(\"kafka.clusters.0.masking.0.type\", \"REPLACE\");\n      System.setProperty(\"kafka.clusters.0.masking.0.replacement\", \"***\");\n      System.setProperty(\"kafka.clusters.0.masking.0.topicValuesPattern\", \"masking-test-.*\");\n      System.setProperty(\"kafka.clusters.0.audit.topicAuditEnabled\", \"true\");\n      System.setProperty(\"kafka.clusters.0.audit.consoleAuditEnabled\", \"true\");\n\n      System.setProperty(\"kafka.clusters.1.name\", SECOND_LOCAL);\n      System.setProperty(\"kafka.clusters.1.readOnly\", \"true\");\n      System.setProperty(\"kafka.clusters.1.bootstrapServers\", kafka.getBootstrapServers());\n      System.setProperty(\"kafka.clusters.1.schemaRegistry\", schemaRegistry.getUrl());\n      System.setProperty(\"kafka.clusters.1.kafkaConnect.0.name\", \"kafka-connect\");\n      System.setProperty(\"kafka.clusters.1.kafkaConnect.0.address\", kafkaConnect.getTarget());\n\n      System.setProperty(\"dynamic.config.enabled\", \"true\");\n      System.setProperty(\"config.related.uploads.dir\", tmpDir.toString());\n    }\n  }\n\n  public static void createTopic(NewTopic topic) {\n    withAdminClient(client -> client.createTopics(List.of(topic)).all().get());\n  }\n\n  public static void deleteTopic(String topic) {\n    withAdminClient(client -> client.deleteTopics(List.of(topic)).all().get());\n  }\n\n  private static void withAdminClient(ThrowingConsumer<AdminClient> consumer) {\n    Properties properties = new Properties();\n    properties.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());\n    try (var client = AdminClient.create(properties)) {\n      try {\n        consumer.accept(client);\n      } catch (Throwable throwable) {\n        throw new RuntimeException(throwable);\n      }\n    }\n  }\n\n  @Autowired\n  protected ConfigurableApplicationContext applicationContext;\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaConnectServiceTests.java",
    "content": "package com.provectus.kafka.ui;\n\nimport static java.util.function.Predicate.not;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport com.provectus.kafka.ui.model.ConnectorDTO;\nimport com.provectus.kafka.ui.model.ConnectorPluginConfigDTO;\nimport com.provectus.kafka.ui.model.ConnectorPluginConfigValidationResponseDTO;\nimport com.provectus.kafka.ui.model.ConnectorPluginConfigValueDTO;\nimport com.provectus.kafka.ui.model.ConnectorPluginDTO;\nimport com.provectus.kafka.ui.model.ConnectorStateDTO;\nimport com.provectus.kafka.ui.model.ConnectorStatusDTO;\nimport com.provectus.kafka.ui.model.ConnectorTypeDTO;\nimport com.provectus.kafka.ui.model.NewConnectorDTO;\nimport com.provectus.kafka.ui.model.TaskIdDTO;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.core.ParameterizedTypeReference;\nimport org.springframework.test.web.reactive.server.WebTestClient;\n\n@Slf4j\npublic class KafkaConnectServiceTests extends AbstractIntegrationTest {\n  private final String connectName = \"kafka-connect\";\n  private final String connectorName = UUID.randomUUID().toString();\n  private final Map<String, Object> config = Map.of(\n      \"name\", connectorName,\n      \"connector.class\", \"org.apache.kafka.connect.file.FileStreamSinkConnector\",\n      \"tasks.max\", \"1\",\n      \"topics\", \"output-topic\",\n      \"file\", \"/tmp/test\",\n      \"test.password\", \"******\"\n  );\n\n  @Autowired\n  private WebTestClient webTestClient;\n\n\n  @BeforeEach\n  public void setUp() {\n    webTestClient.post()\n        .uri(\"/api/clusters/{clusterName}/connects/{connectName}/connectors\", LOCAL, connectName)\n        .bodyValue(new NewConnectorDTO()\n            .name(connectorName)\n            .config(Map.of(\n                \"connector.class\", \"org.apache.kafka.connect.file.FileStreamSinkConnector\",\n                \"tasks.max\", \"1\",\n                \"topics\", \"output-topic\",\n                \"file\", \"/tmp/test\",\n                \"test.password\", \"test-credentials\"\n            ))\n        )\n        .exchange()\n        .expectStatus().isOk();\n  }\n\n  @AfterEach\n  public void tearDown() {\n    webTestClient.delete()\n        .uri(\"/api/clusters/{clusterName}/connects/{connectName}/connectors/{connectorName}\", LOCAL,\n            connectName, connectorName)\n        .exchange()\n        .expectStatus().isOk();\n  }\n\n  @Test\n  public void shouldListAllConnectors() {\n    webTestClient.get()\n            .uri(\"/api/clusters/{clusterName}/connectors\", LOCAL)\n            .exchange()\n            .expectStatus().isOk()\n            .expectBody()\n            .jsonPath(String.format(\"$[?(@.name == '%s')]\", connectorName))\n            .exists();\n  }\n\n  @Test\n  public void shouldFilterByNameConnectors() {\n    webTestClient.get()\n            .uri(\n                    \"/api/clusters/{clusterName}/connectors?search={search}\",\n                    LOCAL,\n                    connectorName.split(\"-\")[1])\n            .exchange()\n            .expectStatus().isOk()\n            .expectBody()\n            .jsonPath(String.format(\"$[?(@.name == '%s')]\", connectorName))\n            .exists();\n  }\n\n  @Test\n  public void shouldFilterByStatusConnectors() {\n    webTestClient.get()\n            .uri(\n                    \"/api/clusters/{clusterName}/connectors?search={search}\",\n                    LOCAL,\n                    \"running\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectBody()\n            .jsonPath(String.format(\"$[?(@.name == '%s')]\", connectorName))\n            .exists();\n  }\n\n  @Test\n  public void shouldFilterByTypeConnectors() {\n    webTestClient.get()\n            .uri(\n                    \"/api/clusters/{clusterName}/connectors?search={search}\",\n                    LOCAL,\n                    \"sink\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectBody()\n            .jsonPath(String.format(\"$[?(@.name == '%s')]\", connectorName))\n            .exists();\n  }\n\n  @Test\n  public void shouldNotFilterConnectors() {\n    webTestClient.get()\n            .uri(\n                    \"/api/clusters/{clusterName}/connectors?search={search}\",\n                    LOCAL,\n                    \"something-else\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectBody()\n            .jsonPath(String.format(\"$[?(@.name == '%s')]\", connectorName))\n            .doesNotExist();\n  }\n\n  @Test\n  public void shouldListConnectors() {\n    webTestClient.get()\n        .uri(\"/api/clusters/{clusterName}/connects/{connectName}/connectors\", LOCAL, connectName)\n        .exchange()\n        .expectStatus().isOk()\n        .expectBodyList(String.class)\n        .contains(connectorName);\n  }\n\n  @Test\n  public void shouldReturnNotFoundForNonExistingCluster() {\n    webTestClient.get()\n        .uri(\"/api/clusters/{clusterName}/connects/{connectName}/connectors\", \"nonExistingCluster\",\n            connectName)\n        .exchange()\n        .expectStatus().isNotFound();\n  }\n\n  @Test\n  public void shouldReturnNotFoundForNonExistingConnectName() {\n    webTestClient.get()\n        .uri(\"/api/clusters/{clusterName}/connects/{connectName}/connectors\", LOCAL,\n            \"nonExistingConnect\")\n        .exchange()\n        .expectStatus().isNotFound();\n  }\n\n  @Test\n  public void shouldRetrieveConnector() {\n    ConnectorDTO expected = (ConnectorDTO) new ConnectorDTO()\n        .connect(connectName)\n        .status(new ConnectorStatusDTO()\n            .state(ConnectorStateDTO.RUNNING)\n            .workerId(\"kafka-connect:8083\"))\n        .tasks(List.of(new TaskIdDTO()\n            .connector(connectorName)\n            .task(0)))\n        .type(ConnectorTypeDTO.SINK)\n        .name(connectorName)\n        .config(config);\n    webTestClient.get()\n        .uri(\"/api/clusters/{clusterName}/connects/{connectName}/connectors/{connectorName}\", LOCAL,\n            connectName, connectorName)\n        .exchange()\n        .expectStatus().isOk()\n        .expectBody(ConnectorDTO.class)\n        .value(connector -> assertEquals(expected, connector));\n  }\n\n  @Test\n  public void shouldUpdateConfig() {\n    webTestClient.put()\n        .uri(\"/api/clusters/{clusterName}/connects/{connectName}/connectors/{connectorName}/config\",\n            LOCAL, connectName, connectorName)\n        .bodyValue(Map.of(\n            \"connector.class\", \"org.apache.kafka.connect.file.FileStreamSinkConnector\",\n            \"tasks.max\", \"1\",\n            \"topics\", \"another-topic\",\n            \"file\", \"/tmp/new\"\n            )\n        )\n        .exchange()\n        .expectStatus().isOk();\n\n    webTestClient.get()\n        .uri(\"/api/clusters/{clusterName}/connects/{connectName}/connectors/{connectorName}/config\",\n            LOCAL, connectName, connectorName)\n        .exchange()\n        .expectStatus().isOk()\n        .expectBody(new ParameterizedTypeReference<Map<String, Object>>() {\n        })\n        .isEqualTo(Map.of(\n            \"connector.class\", \"org.apache.kafka.connect.file.FileStreamSinkConnector\",\n            \"tasks.max\", \"1\",\n            \"topics\", \"another-topic\",\n            \"file\", \"/tmp/new\",\n            \"name\", connectorName\n        ));\n  }\n\n  @Test\n  public void shouldReturn400WhenConnectReturns400ForInvalidConfigCreate() {\n    var connectorName = UUID.randomUUID().toString();\n    webTestClient.post()\n        .uri(\"/api/clusters/{clusterName}/connects/{connectName}/connectors\", LOCAL, connectName)\n        .bodyValue(Map.of(\n            \"name\", connectorName,\n            \"config\", Map.of(\n                \"connector.class\", \"org.apache.kafka.connect.file.FileStreamSinkConnector\",\n                \"tasks.max\", \"invalid number\",\n                \"topics\", \"another-topic\",\n                \"file\", \"/tmp/test\"\n            ))\n        )\n        .exchange()\n        .expectStatus().isBadRequest();\n\n    webTestClient.get()\n        .uri(\"/api/clusters/{clusterName}/connects/{connectName}/connectors\", LOCAL, connectName)\n        .exchange()\n        .expectStatus().isOk()\n        .expectBody()\n        .jsonPath(String.format(\"$[?(@ == '%s')]\", connectorName))\n        .doesNotExist();\n  }\n\n  @Test\n  public void shouldReturn400WhenConnectReturns500ForInvalidConfigCreate() {\n    var connectorName = UUID.randomUUID().toString();\n    webTestClient.post()\n        .uri(\"/api/clusters/{clusterName}/connects/{connectName}/connectors\", LOCAL, connectName)\n        .bodyValue(Map.of(\n            \"name\", connectorName,\n            \"config\", Map.of(\n                \"connector.class\", \"org.apache.kafka.connect.file.FileStreamSinkConnector\"\n            ))\n        )\n        .exchange()\n        .expectStatus().isBadRequest();\n\n    webTestClient.get()\n        .uri(\"/api/clusters/{clusterName}/connects/{connectName}/connectors\", LOCAL, connectName)\n        .exchange()\n        .expectStatus().isOk()\n        .expectBody()\n        .jsonPath(String.format(\"$[?(@ == '%s')]\", connectorName))\n        .doesNotExist();\n  }\n\n\n  @Test\n  public void shouldReturn400WhenConnectReturns400ForInvalidConfigUpdate() {\n    webTestClient.put()\n        .uri(\"/api/clusters/{clusterName}/connects/{connectName}/connectors/{connectorName}/config\",\n            LOCAL, connectName, connectorName)\n        .bodyValue(Map.of(\n            \"connector.class\", \"org.apache.kafka.connect.file.FileStreamSinkConnector\",\n            \"tasks.max\", \"invalid number\",\n            \"topics\", \"another-topic\",\n            \"file\", \"/tmp/test\"\n            )\n        )\n        .exchange()\n        .expectStatus().isBadRequest();\n\n    webTestClient.get()\n        .uri(\"/api/clusters/{clusterName}/connects/{connectName}/connectors/{connectorName}/config\",\n            LOCAL, connectName, connectorName)\n        .exchange()\n        .expectStatus().isOk()\n        .expectBody(new ParameterizedTypeReference<Map<String, Object>>() {\n        })\n        .isEqualTo(Map.of(\n            \"connector.class\", \"org.apache.kafka.connect.file.FileStreamSinkConnector\",\n            \"tasks.max\", \"1\",\n            \"topics\", \"output-topic\",\n            \"file\", \"/tmp/test\",\n            \"name\", connectorName,\n            \"test.password\", \"******\"\n        ));\n  }\n\n  @Test\n  public void shouldReturn400WhenConnectReturns500ForInvalidConfigUpdate() {\n    webTestClient.put()\n        .uri(\"/api/clusters/{clusterName}/connects/{connectName}/connectors/{connectorName}/config\",\n            LOCAL, connectName, connectorName)\n        .bodyValue(Map.of(\n            \"connector.class\", \"org.apache.kafka.connect.file.FileStreamSinkConnector\"\n            )\n        )\n        .exchange()\n        .expectStatus().isBadRequest();\n\n    webTestClient.get()\n        .uri(\"/api/clusters/{clusterName}/connects/{connectName}/connectors/{connectorName}/config\",\n            LOCAL, connectName, connectorName)\n        .exchange()\n        .expectStatus().isOk()\n        .expectBody(new ParameterizedTypeReference<Map<String, Object>>() {\n        })\n        .isEqualTo(Map.of(\n            \"connector.class\", \"org.apache.kafka.connect.file.FileStreamSinkConnector\",\n            \"tasks.max\", \"1\",\n            \"topics\", \"output-topic\",\n            \"file\", \"/tmp/test\",\n            \"test.password\", \"******\",\n            \"name\", connectorName\n        ));\n  }\n\n  @Test\n  public void shouldRetrieveConnectorPlugins() {\n    webTestClient.get()\n        .uri(\"/api/clusters/{clusterName}/connects/{connectName}/plugins\", LOCAL, connectName)\n        .exchange()\n        .expectStatus().isOk()\n        .expectBodyList(ConnectorPluginDTO.class)\n        .value(plugins -> assertThat(plugins.size()).isGreaterThan(0));\n  }\n\n  @Test\n  public void shouldSuccessfullyValidateConnectorPluginConfiguration() {\n    var pluginName = \"FileStreamSinkConnector\";\n    var path =\n        \"/api/clusters/{clusterName}/connects/{connectName}/plugins/{pluginName}/config/validate\";\n    webTestClient.put()\n        .uri(path, LOCAL, connectName, pluginName)\n        .bodyValue(Map.of(\n            \"connector.class\", \"org.apache.kafka.connect.file.FileStreamSinkConnector\",\n            \"tasks.max\", \"1\",\n            \"topics\", \"output-topic\",\n            \"file\", \"/tmp/test\",\n            \"name\", connectorName\n            )\n        )\n        .exchange()\n        .expectStatus().isOk()\n        .expectBody(ConnectorPluginConfigValidationResponseDTO.class)\n        .value(response -> assertEquals(0, response.getErrorCount()));\n  }\n\n  @Test\n  public void shouldValidateAndReturnErrorsOfConnectorPluginConfiguration() {\n    var pluginName = \"FileStreamSinkConnector\";\n    var path =\n        \"/api/clusters/{clusterName}/connects/{connectName}/plugins/{pluginName}/config/validate\";\n    webTestClient.put()\n        .uri(path, LOCAL, connectName, pluginName)\n        .bodyValue(Map.of(\n            \"connector.class\", \"org.apache.kafka.connect.file.FileStreamSinkConnector\",\n            \"tasks.max\", \"0\",\n            \"topics\", \"output-topic\",\n            \"file\", \"/tmp/test\",\n            \"name\", connectorName\n            )\n        )\n        .exchange()\n        .expectStatus().isOk()\n        .expectBody(ConnectorPluginConfigValidationResponseDTO.class)\n        .value(response -> {\n          assertEquals(1, response.getErrorCount());\n          var error = response.getConfigs().stream()\n              .map(ConnectorPluginConfigDTO::getValue)\n              .map(ConnectorPluginConfigValueDTO::getErrors)\n              .filter(not(List::isEmpty))\n              .findFirst().get();\n          assertEquals(\n              \"Invalid value 0 for configuration tasks.max: Value must be at least 1\",\n              error.get(0)\n          );\n        });\n  }\n\n  @Test\n  public void shouldReturn400WhenTryingToCreateConnectorWithExistingName() {\n    webTestClient.post()\n        .uri(\"/api/clusters/{clusterName}/connects/{connectName}/connectors\", LOCAL, connectName)\n        .bodyValue(new NewConnectorDTO()\n            .name(connectorName)\n            .config(Map.of(\n                \"connector.class\", \"org.apache.kafka.connect.file.FileStreamSinkConnector\",\n                \"tasks.max\", \"1\",\n                \"topics\", \"output-topic\",\n                \"file\", \"/tmp/test\"\n            ))\n        )\n        .exchange()\n        .expectStatus()\n        .isBadRequest();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaConsumerGroupTests.java",
    "content": "package com.provectus.kafka.ui;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.provectus.kafka.ui.model.ConsumerGroupDTO;\nimport com.provectus.kafka.ui.model.ConsumerGroupsPageResponseDTO;\nimport java.io.Closeable;\nimport java.time.Duration;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Properties;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport lombok.extern.slf4j.Slf4j;\nimport lombok.val;\nimport org.apache.commons.lang3.RandomStringUtils;\nimport org.apache.kafka.clients.admin.NewTopic;\nimport org.apache.kafka.clients.consumer.ConsumerConfig;\nimport org.apache.kafka.clients.consumer.KafkaConsumer;\nimport org.apache.kafka.common.serialization.BytesDeserializer;\nimport org.apache.kafka.common.utils.Bytes;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.test.web.reactive.server.WebTestClient;\n\n@Slf4j\npublic class KafkaConsumerGroupTests extends AbstractIntegrationTest {\n  @Autowired\n  WebTestClient webTestClient;\n\n  @Test\n  void shouldNotFoundWhenNoSuchConsumerGroupId() {\n    String groupId = \"groupA\";\n    String expError = \"The group id does not exist\";\n    webTestClient\n        .delete()\n        .uri(\"/api/clusters/{clusterName}/consumer-groups/{groupId}\", LOCAL, groupId)\n        .exchange()\n        .expectStatus()\n        .isNotFound();\n  }\n\n  @Test\n  void shouldOkWhenConsumerGroupIsNotActive() {\n    String topicName = createTopicWithRandomName();\n\n    //Create a consumer and subscribe to the topic\n    String groupId = UUID.randomUUID().toString();\n    val consumer = createTestConsumerWithGroupId(groupId);\n    consumer.subscribe(List.of(topicName));\n    consumer.poll(Duration.ofMillis(100));\n\n    //Unsubscribe from all topics to be able to delete this consumer\n    consumer.unsubscribe();\n\n    //Delete the consumer when it's INACTIVE and check\n    webTestClient\n        .delete()\n        .uri(\"/api/clusters/{clusterName}/consumer-groups/{groupId}\", LOCAL, groupId)\n        .exchange()\n        .expectStatus()\n        .isOk();\n  }\n\n  @Test\n  void shouldBeBadRequestWhenConsumerGroupIsActive() {\n    String topicName = createTopicWithRandomName();\n\n    //Create a consumer and subscribe to the topic\n    String groupId = UUID.randomUUID().toString();\n    val consumer = createTestConsumerWithGroupId(groupId);\n    consumer.subscribe(List.of(topicName));\n    consumer.poll(Duration.ofMillis(100));\n\n    //Try to delete the consumer when it's ACTIVE\n    String expError = \"The group is not empty\";\n    webTestClient\n        .delete()\n        .uri(\"/api/clusters/{clusterName}/consumer-groups/{groupId}\", LOCAL, groupId)\n        .exchange()\n        .expectStatus()\n        .isBadRequest();\n  }\n\n  @Test\n  void shouldReturnConsumerGroupsWithPagination() throws Exception {\n    try (var groups1 = startConsumerGroups(3, \"cgPageTest1\");\n        var groups2 = startConsumerGroups(2, \"cgPageTest2\")) {\n      webTestClient\n          .get()\n          .uri(\"/api/clusters/{clusterName}/consumer-groups/paged?perPage=3&search=cgPageTest\", LOCAL)\n          .exchange()\n          .expectStatus()\n          .isOk()\n          .expectBody(ConsumerGroupsPageResponseDTO.class)\n          .value(page -> {\n            assertThat(page.getPageCount()).isEqualTo(2);\n            assertThat(page.getConsumerGroups().size()).isEqualTo(3);\n          });\n\n      webTestClient\n          .get()\n          .uri(\"/api/clusters/{clusterName}/consumer-groups/paged?perPage=10&search=cgPageTest\", LOCAL)\n          .exchange()\n          .expectStatus()\n          .isOk()\n          .expectBody(ConsumerGroupsPageResponseDTO.class)\n          .value(page -> {\n            assertThat(page.getPageCount()).isEqualTo(1);\n            assertThat(page.getConsumerGroups().size()).isEqualTo(5);\n            assertThat(page.getConsumerGroups())\n                .isSortedAccordingTo(Comparator.comparing(ConsumerGroupDTO::getGroupId));\n          });\n\n      webTestClient\n            .get()\n            .uri(\"/api/clusters/{clusterName}/consumer-groups/paged?perPage=10&&search\"\n                + \"=cgPageTest&orderBy=NAME&sortOrder=DESC\", LOCAL)\n            .exchange()\n            .expectStatus()\n            .isOk()\n            .expectBody(ConsumerGroupsPageResponseDTO.class)\n            .value(page -> {\n              assertThat(page.getPageCount()).isEqualTo(1);\n              assertThat(page.getConsumerGroups().size()).isEqualTo(5);\n              assertThat(page.getConsumerGroups())\n                  .isSortedAccordingTo(Comparator.comparing(ConsumerGroupDTO::getGroupId).reversed());\n            });\n\n      webTestClient\n          .get()\n          .uri(\"/api/clusters/{clusterName}/consumer-groups/paged?perPage=10&&search\"\n              + \"=cgPageTest&orderBy=MEMBERS&sortOrder=DESC\", LOCAL)\n          .exchange()\n          .expectStatus()\n          .isOk()\n          .expectBody(ConsumerGroupsPageResponseDTO.class)\n          .value(page -> {\n            assertThat(page.getPageCount()).isEqualTo(1);\n            assertThat(page.getConsumerGroups().size()).isEqualTo(5);\n            assertThat(page.getConsumerGroups())\n                .isSortedAccordingTo(Comparator.comparing(ConsumerGroupDTO::getMembers).reversed());\n          });\n    }\n  }\n\n  private Closeable startConsumerGroups(int count, String consumerGroupPrefix) {\n    String topicName = createTopicWithRandomName();\n    var consumers =\n        Stream.generate(() -> {\n          String groupId = consumerGroupPrefix + RandomStringUtils.randomAlphabetic(5);\n          val consumer = createTestConsumerWithGroupId(groupId);\n          consumer.subscribe(List.of(topicName));\n          consumer.poll(Duration.ofMillis(100));\n          return consumer;\n        })\n        .limit(count)\n        .collect(Collectors.toList());\n    return () -> {\n      consumers.forEach(KafkaConsumer::close);\n      deleteTopic(topicName);\n    };\n  }\n\n  private String createTopicWithRandomName() {\n    String topicName = getClass().getSimpleName() + \"-\" + UUID.randomUUID();\n    short replicationFactor = 1;\n    int partitions = 1;\n    createTopic(new NewTopic(topicName, partitions, replicationFactor));\n    return topicName;\n  }\n\n  private KafkaConsumer<Bytes, Bytes> createTestConsumerWithGroupId(String groupId) {\n    Properties props = new Properties();\n    props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);\n    props.put(ConsumerConfig.CLIENT_ID_CONFIG, groupId);\n    props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());\n    props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, BytesDeserializer.class);\n    props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, BytesDeserializer.class);\n    props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, \"earliest\");\n    return new KafkaConsumer<>(props);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaConsumerTests.java",
    "content": "package com.provectus.kafka.ui;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.springframework.http.MediaType.TEXT_EVENT_STREAM;\n\nimport com.provectus.kafka.ui.model.BrokerConfigDTO;\nimport com.provectus.kafka.ui.model.PartitionsIncreaseDTO;\nimport com.provectus.kafka.ui.model.PartitionsIncreaseResponseDTO;\nimport com.provectus.kafka.ui.model.TopicConfigDTO;\nimport com.provectus.kafka.ui.model.TopicCreationDTO;\nimport com.provectus.kafka.ui.model.TopicDetailsDTO;\nimport com.provectus.kafka.ui.model.TopicMessageEventDTO;\nimport com.provectus.kafka.ui.producer.KafkaTestProducer;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.stream.Stream;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\n@Slf4j\npublic class KafkaConsumerTests extends AbstractIntegrationTest {\n\n  @Autowired\n  private WebTestClient webTestClient;\n\n\n  @Test\n  public void shouldDeleteRecords() {\n    var topicName = UUID.randomUUID().toString();\n    webTestClient.post()\n        .uri(\"/api/clusters/{clusterName}/topics\", LOCAL)\n        .bodyValue(new TopicCreationDTO()\n            .name(topicName)\n            .partitions(1)\n            .replicationFactor(1)\n            .configs(Map.of())\n        )\n        .exchange()\n        .expectStatus()\n        .isOk();\n\n    try (KafkaTestProducer<String, String> producer = KafkaTestProducer.forKafka(kafka)) {\n      Flux.fromStream(\n          Stream.of(\"one\", \"two\", \"three\", \"four\")\n              .map(value -> Mono.fromFuture(producer.send(topicName, value)))\n      ).blockLast();\n    } catch (Throwable e) {\n      log.error(\"Error on sending\", e);\n      throw new RuntimeException(e);\n    }\n\n    long count = webTestClient.get()\n        .uri(\"/api/clusters/{clusterName}/topics/{topicName}/messages\", LOCAL, topicName)\n        .accept(TEXT_EVENT_STREAM)\n        .exchange()\n        .expectStatus()\n        .isOk()\n        .expectBodyList(TopicMessageEventDTO.class)\n        .returnResult()\n        .getResponseBody()\n        .stream()\n        .filter(e -> e.getType().equals(TopicMessageEventDTO.TypeEnum.MESSAGE))\n        .count();\n\n    assertThat(count).isEqualTo(4);\n\n    webTestClient.delete()\n        .uri(\"/api/clusters/{clusterName}/topics/{topicName}/messages\", LOCAL, topicName)\n        .exchange()\n        .expectStatus()\n        .isOk();\n\n    count = webTestClient.get()\n        .uri(\"/api/clusters/{clusterName}/topics/{topicName}/messages\", LOCAL, topicName)\n        .exchange()\n        .expectStatus()\n        .isOk()\n        .expectBodyList(TopicMessageEventDTO.class)\n        .returnResult()\n        .getResponseBody()\n        .stream()\n        .filter(e -> e.getType().equals(TopicMessageEventDTO.TypeEnum.MESSAGE))\n        .count();\n\n    assertThat(count).isZero();\n  }\n\n  @Test\n  public void shouldIncreasePartitionsUpTo10() {\n    var topicName = UUID.randomUUID().toString();\n    webTestClient.post()\n        .uri(\"/api/clusters/{clusterName}/topics\", LOCAL)\n        .bodyValue(new TopicCreationDTO()\n            .name(topicName)\n            .partitions(1)\n            .replicationFactor(1)\n            .configs(Map.of())\n        )\n        .exchange()\n        .expectStatus()\n        .isOk();\n\n    PartitionsIncreaseResponseDTO response = webTestClient.patch()\n        .uri(\"/api/clusters/{clusterName}/topics/{topicName}/partitions\",\n            LOCAL,\n            topicName)\n        .bodyValue(new PartitionsIncreaseDTO()\n            .totalPartitionsCount(10)\n        )\n        .exchange()\n        .expectStatus()\n        .isOk()\n        .expectBody(PartitionsIncreaseResponseDTO.class)\n        .returnResult()\n        .getResponseBody();\n\n    assert response != null;\n    Assertions.assertEquals(10, response.getTotalPartitionsCount());\n\n    TopicDetailsDTO topicDetails = webTestClient.get()\n        .uri(\"/api/clusters/{clusterName}/topics/{topicName}\",\n            LOCAL,\n            topicName)\n        .exchange()\n        .expectStatus()\n        .isOk()\n        .expectBody(TopicDetailsDTO.class)\n        .returnResult()\n        .getResponseBody();\n\n    assert topicDetails != null;\n    Assertions.assertEquals(10, topicDetails.getPartitionCount());\n  }\n\n  @Test\n  public void shouldReturn404ForNonExistingTopic() {\n    var topicName = UUID.randomUUID().toString();\n\n    webTestClient.delete()\n        .uri(\"/api/clusters/{clusterName}/topics/{topicName}/messages\", LOCAL, topicName)\n        .exchange()\n        .expectStatus()\n        .isNotFound();\n\n    webTestClient.get()\n        .uri(\"/api/clusters/{clusterName}/topics/{topicName}/config\", LOCAL, topicName)\n        .exchange()\n        .expectStatus()\n        .isNotFound();\n  }\n\n  @Test\n  public void shouldReturnConfigsForBroker() {\n    var topicName = UUID.randomUUID().toString();\n\n    List<BrokerConfigDTO> configs = webTestClient.get()\n        .uri(\"/api/clusters/{clusterName}/brokers/{id}/configs\",\n            LOCAL,\n            1)\n        .exchange()\n        .expectStatus()\n        .isOk()\n        .expectBodyList(BrokerConfigDTO.class)\n        .returnResult()\n        .getResponseBody();\n\n    Assertions.assertNotNull(configs);\n    assert !configs.isEmpty();\n    Assertions.assertNotNull(configs.get(0).getName());\n    Assertions.assertNotNull(configs.get(0).getIsReadOnly());\n    Assertions.assertNotNull(configs.get(0).getIsSensitive());\n    Assertions.assertNotNull(configs.get(0).getSource());\n    Assertions.assertNotNull(configs.get(0).getSynonyms());\n  }\n\n  @Test\n  public void shouldReturn404ForNonExistingBroker() {\n    webTestClient.get()\n        .uri(\"/api/clusters/{clusterName}/brokers/{id}/configs\",\n            LOCAL,\n            0)\n        .exchange()\n        .expectStatus()\n        .isNotFound();\n  }\n\n  @Test\n  public void shouldRetrieveTopicConfig() {\n    var topicName = UUID.randomUUID().toString();\n\n    webTestClient.post()\n            .uri(\"/api/clusters/{clusterName}/topics\", LOCAL)\n            .bodyValue(new TopicCreationDTO()\n                    .name(topicName)\n                    .partitions(1)\n                    .replicationFactor(1)\n                    .configs(Map.of())\n            )\n            .exchange()\n            .expectStatus()\n            .isOk();\n\n    List<TopicConfigDTO> configs = webTestClient.get()\n            .uri(\"/api/clusters/{clusterName}/topics/{topicName}/config\", LOCAL, topicName)\n            .exchange()\n            .expectStatus()\n            .isOk()\n            .expectBodyList(TopicConfigDTO.class)\n            .returnResult()\n            .getResponseBody();\n\n    Assertions.assertNotNull(configs);\n    assert !configs.isEmpty();\n    Assertions.assertNotNull(configs.get(0).getName());\n    Assertions.assertNotNull(configs.get(0).getIsReadOnly());\n    Assertions.assertNotNull(configs.get(0).getIsSensitive());\n    Assertions.assertNotNull(configs.get(0).getSource());\n    Assertions.assertNotNull(configs.get(0).getSynonyms());\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaTopicCreateTests.java",
    "content": "package com.provectus.kafka.ui;\n\nimport com.provectus.kafka.ui.model.TopicCreationDTO;\nimport java.util.UUID;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.test.web.reactive.server.WebTestClient;\n\npublic class KafkaTopicCreateTests extends AbstractIntegrationTest {\n  @Autowired\n  private WebTestClient webTestClient;\n  private TopicCreationDTO topicCreation;\n\n  @BeforeEach\n  public void setUpBefore() {\n    this.topicCreation = new TopicCreationDTO()\n        .replicationFactor(1)\n        .partitions(3)\n        .name(UUID.randomUUID().toString());\n  }\n\n  @Test\n  void shouldCreateNewTopicSuccessfully() {\n    webTestClient.post()\n        .uri(\"/api/clusters/{clusterName}/topics\", LOCAL)\n        .bodyValue(topicCreation)\n        .exchange()\n        .expectStatus()\n        .isOk();\n  }\n\n  @Test\n  void shouldReturn400IfTopicAlreadyExists() {\n    TopicCreationDTO topicCreation = new TopicCreationDTO()\n        .replicationFactor(1)\n        .partitions(3)\n        .name(UUID.randomUUID().toString());\n\n    webTestClient.post()\n        .uri(\"/api/clusters/{clusterName}/topics\", LOCAL)\n        .bodyValue(topicCreation)\n        .exchange()\n        .expectStatus()\n        .isOk();\n\n    webTestClient.post()\n        .uri(\"/api/clusters/{clusterName}/topics\", LOCAL)\n        .bodyValue(topicCreation)\n        .exchange()\n        .expectStatus()\n        .isBadRequest();\n  }\n\n  @Test\n  void shouldRecreateExistingTopicSuccessfully() {\n    TopicCreationDTO topicCreation = new TopicCreationDTO()\n            .replicationFactor(1)\n            .partitions(3)\n            .name(UUID.randomUUID().toString());\n\n    webTestClient.post()\n            .uri(\"/api/clusters/{clusterName}/topics\", LOCAL)\n            .bodyValue(topicCreation)\n            .exchange()\n            .expectStatus()\n            .isOk();\n\n    webTestClient.post()\n            .uri(\"/api/clusters/{clusterName}/topics/\" + topicCreation.getName(), LOCAL)\n            .exchange()\n            .expectStatus()\n            .isCreated()\n            .expectBody()\n            .jsonPath(\"partitionCount\").isEqualTo(topicCreation.getPartitions().toString())\n            .jsonPath(\"replicationFactor\").isEqualTo(topicCreation.getReplicationFactor().toString())\n            .jsonPath(\"name\").isEqualTo(topicCreation.getName());\n  }\n\n  @Test\n  void shouldCloneExistingTopicSuccessfully() {\n    TopicCreationDTO topicCreation = new TopicCreationDTO()\n        .replicationFactor(1)\n        .partitions(3)\n        .name(UUID.randomUUID().toString());\n    String clonedTopicName = UUID.randomUUID().toString();\n\n    webTestClient.post()\n        .uri(\"/api/clusters/{clusterName}/topics\", LOCAL)\n        .bodyValue(topicCreation)\n        .exchange()\n        .expectStatus()\n        .isOk();\n\n    webTestClient.post()\n        .uri(\"/api/clusters/{clusterName}/topics/{topicName}/clone?newTopicName=\" + clonedTopicName,\n            LOCAL, topicCreation.getName())\n        .exchange()\n        .expectStatus()\n        .isCreated()\n        .expectBody()\n        .jsonPath(\"partitionCount\").isEqualTo(topicCreation.getPartitions().toString())\n        .jsonPath(\"replicationFactor\").isEqualTo(topicCreation.getReplicationFactor().toString())\n        .jsonPath(\"name\").isEqualTo(clonedTopicName);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/ReadOnlyModeTests.java",
    "content": "package com.provectus.kafka.ui;\n\nimport com.provectus.kafka.ui.model.TopicCreationDTO;\nimport com.provectus.kafka.ui.model.TopicUpdateDTO;\nimport java.util.Map;\nimport java.util.UUID;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.test.web.reactive.server.WebTestClient;\n\npublic class ReadOnlyModeTests extends AbstractIntegrationTest {\n\n  @Autowired\n  private WebTestClient webTestClient;\n\n  @Test\n  public void shouldCreateTopicForNonReadonlyCluster() {\n    var topicName = UUID.randomUUID().toString();\n    webTestClient.post()\n        .uri(\"/api/clusters/{clusterName}/topics\", LOCAL)\n        .bodyValue(new TopicCreationDTO()\n            .name(topicName)\n            .partitions(1)\n            .replicationFactor(1)\n            .configs(Map.of())\n        )\n        .exchange()\n        .expectStatus()\n        .isOk();\n  }\n\n  @Test\n  public void shouldNotCreateTopicForReadonlyCluster() {\n    var topicName = UUID.randomUUID().toString();\n    webTestClient.post()\n        .uri(\"/api/clusters/{clusterName}/topics\", SECOND_LOCAL)\n        .bodyValue(new TopicCreationDTO()\n            .name(topicName)\n            .partitions(1)\n            .replicationFactor(1)\n            .configs(Map.of())\n        )\n        .exchange()\n        .expectStatus()\n        .isEqualTo(HttpStatus.METHOD_NOT_ALLOWED);\n  }\n\n  @Test\n  public void shouldUpdateTopicForNonReadonlyCluster() {\n    var topicName = UUID.randomUUID().toString();\n    webTestClient.post()\n        .uri(\"/api/clusters/{clusterName}/topics\", LOCAL)\n        .bodyValue(new TopicCreationDTO()\n            .name(topicName)\n            .partitions(1)\n            .replicationFactor(1)\n        )\n        .exchange()\n        .expectStatus()\n        .isOk();\n\n    webTestClient.patch()\n        .uri(\"/api/clusters/{clusterName}/topics/{topicName}\", LOCAL, topicName)\n        .bodyValue(new TopicUpdateDTO()\n            .configs(Map.of(\"cleanup.policy\", \"compact\"))\n        )\n        .exchange()\n        .expectStatus()\n        .isOk()\n        .expectBody()\n        .jsonPath(\"$.cleanUpPolicy\").isEqualTo(\"COMPACT\");\n  }\n\n  @Test\n  public void shouldNotUpdateTopicForReadonlyCluster() {\n    var topicName = UUID.randomUUID().toString();\n    webTestClient.patch()\n        .uri(\"/api/clusters/{clusterName}/topics/{topicName}\", SECOND_LOCAL, topicName)\n        .bodyValue(new TopicUpdateDTO()\n            .configs(Map.of())\n        )\n        .exchange()\n        .expectStatus()\n        .isEqualTo(HttpStatus.METHOD_NOT_ALLOWED);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/SchemaRegistryServiceTests.java",
    "content": "package com.provectus.kafka.ui;\n\nimport com.provectus.kafka.ui.model.CompatibilityLevelDTO;\nimport com.provectus.kafka.ui.model.NewSchemaSubjectDTO;\nimport com.provectus.kafka.ui.model.SchemaReferenceDTO;\nimport com.provectus.kafka.ui.model.SchemaSubjectDTO;\nimport com.provectus.kafka.ui.model.SchemaSubjectsResponseDTO;\nimport com.provectus.kafka.ui.model.SchemaTypeDTO;\nimport java.nio.charset.StandardCharsets;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.UUID;\nimport lombok.extern.slf4j.Slf4j;\nimport lombok.val;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.MediaType;\nimport org.springframework.test.web.reactive.server.EntityExchangeResult;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.web.reactive.function.BodyInserters;\nimport org.testcontainers.shaded.org.hamcrest.MatcherAssert;\nimport org.testcontainers.shaded.org.hamcrest.Matchers;\nimport reactor.core.publisher.Mono;\n\n@Slf4j\nclass SchemaRegistryServiceTests extends AbstractIntegrationTest {\n  @Autowired\n  WebTestClient webTestClient;\n  String subject;\n\n  @BeforeEach\n  public void setUpBefore() {\n    this.subject = UUID.randomUUID().toString();\n  }\n\n  @Test\n  public void should404WhenGetAllSchemasForUnknownCluster() {\n    webTestClient\n        .get()\n        .uri(\"/api/clusters/unknown-cluster/schemas\")\n        .exchange()\n        .expectStatus().isNotFound();\n  }\n\n  @Test\n  public void shouldReturn404WhenGetLatestSchemaByNonExistingSubject() {\n    String unknownSchema = \"unknown-schema\";\n    webTestClient\n        .get()\n        .uri(\"/api/clusters/{clusterName}/schemas/{subject}/latest\", LOCAL, unknownSchema)\n        .exchange()\n        .expectStatus().isNotFound();\n  }\n\n  /**\n   * It should create a new schema w/o submitting a schemaType field to Schema Registry.\n   */\n  @Test\n  void shouldBeBadRequestIfNoSchemaType() {\n    String schema = \"{\\\"subject\\\":\\\"%s\\\",\\\"schema\\\":\\\"{\\\\\\\"type\\\\\\\": \\\\\\\"string\\\\\\\"}\\\"}\";\n\n    webTestClient\n        .post()\n        .uri(\"/api/clusters/{clusterName}/schemas\", LOCAL)\n        .contentType(MediaType.APPLICATION_JSON)\n        .body(BodyInserters.fromValue(String.format(schema, subject)))\n        .exchange()\n        .expectStatus().isBadRequest();\n  }\n\n  @Test\n  void shouldNotDoAnythingIfSchemaNotChanged() {\n    String schema =\n        \"{\\\"subject\\\":\\\"%s\\\",\\\"schemaType\\\":\\\"AVRO\\\",\\\"schema\\\":\"\n            + \"\\\"{\\\\\\\"type\\\\\\\": \\\\\\\"string\\\\\\\"}\\\"}\";\n\n    SchemaSubjectDTO dto = webTestClient\n        .post()\n        .uri(\"/api/clusters/{clusterName}/schemas\", LOCAL)\n        .contentType(MediaType.APPLICATION_JSON)\n        .body(BodyInserters.fromValue(String.format(schema, subject)))\n        .exchange()\n        .expectStatus()\n        .isOk()\n        .expectBody(SchemaSubjectDTO.class)\n        .returnResult()\n        .getResponseBody();\n\n    Assertions.assertNotNull(dto);\n    Assertions.assertEquals(\"1\", dto.getVersion());\n\n    dto = webTestClient\n        .post()\n        .uri(\"/api/clusters/{clusterName}/schemas\", LOCAL)\n        .contentType(MediaType.APPLICATION_JSON)\n        .body(BodyInserters.fromValue(String.format(schema, subject)))\n        .exchange()\n        .expectStatus()\n        .isOk()\n        .expectBody(SchemaSubjectDTO.class)\n        .returnResult()\n        .getResponseBody();\n\n    Assertions.assertNotNull(dto);\n    Assertions.assertEquals(\"1\", dto.getVersion());\n  }\n\n  @Test\n  void shouldReturnCorrectMessageWhenIncompatibleSchema() {\n    String schema = \"{\\\"subject\\\":\\\"%s\\\",\\\"schemaType\\\":\\\"JSON\\\",\\\"schema\\\":\"\n        + \"\\\"{\\\\\\\"type\\\\\\\": \\\\\\\"string\\\\\\\",\" + \"\\\\\\\"properties\\\\\\\": \"\n        + \"{\\\\\\\"f1\\\\\\\": {\\\\\\\"type\\\\\\\": \\\\\\\"integer\\\\\\\"}}}\"\n        + \"\\\"}\";\n    String schema2 = \"{\\\"subject\\\":\\\"%s\\\",\" + \"\\\"schemaType\\\":\\\"JSON\\\",\\\"schema\\\":\"\n        + \"\\\"{\\\\\\\"type\\\\\\\": \\\\\\\"string\\\\\\\",\" + \"\\\\\\\"properties\\\\\\\": \"\n        + \"{\\\\\\\"f1\\\\\\\": {\\\\\\\"type\\\\\\\": \\\\\\\"string\\\\\\\"},\"\n        + \"\\\\\\\"f2\\\\\\\": {\" + \"\\\\\\\"type\\\\\\\": \\\\\\\"string\\\\\\\"}}}\"\n        + \"\\\"}\";\n\n    SchemaSubjectDTO dto =\n        webTestClient\n            .post()\n            .uri(\"/api/clusters/{clusterName}/schemas\", LOCAL)\n            .contentType(MediaType.APPLICATION_JSON)\n            .body(BodyInserters.fromValue(String.format(schema, subject)))\n            .exchange()\n            .expectStatus()\n            .isOk()\n            .expectBody(SchemaSubjectDTO.class)\n            .returnResult()\n            .getResponseBody();\n\n    Assertions.assertNotNull(dto);\n    Assertions.assertEquals(\"1\", dto.getVersion());\n\n    webTestClient\n        .post()\n        .uri(\"/api/clusters/{clusterName}/schemas\", LOCAL)\n        .contentType(MediaType.APPLICATION_JSON)\n        .body(BodyInserters.fromValue(String.format(schema2, subject)))\n        .exchange()\n        .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY)\n        .expectBody().consumeWith(body -> {\n          String responseBody = new String(Objects.requireNonNull(body.getResponseBody()), StandardCharsets.UTF_8);\n          MatcherAssert.assertThat(\"Must return correct message incompatible schema\",\n              responseBody,\n              Matchers.containsString(\"Schema being registered is incompatible with an earlier schema\"));\n        });\n\n    dto = webTestClient\n        .get()\n        .uri(\"/api/clusters/{clusterName}/schemas/{subject}/latest\", LOCAL, subject)\n        .exchange()\n        .expectStatus().isOk()\n        .expectBody(SchemaSubjectDTO.class)\n        .returnResult()\n        .getResponseBody();\n\n    Assertions.assertNotNull(dto);\n    Assertions.assertEquals(\"1\", dto.getVersion());\n  }\n\n  @Test\n  void shouldCreateNewProtobufSchema() {\n    String schema =\n        \"syntax = \\\"proto3\\\";\\n\\nmessage MyRecord {\\n  int32 id = 1;\\n  string name = 2;\\n}\\n\";\n    NewSchemaSubjectDTO requestBody = new NewSchemaSubjectDTO()\n        .schemaType(SchemaTypeDTO.PROTOBUF)\n        .subject(subject)\n        .schema(schema);\n    SchemaSubjectDTO actual = webTestClient\n        .post()\n        .uri(\"/api/clusters/{clusterName}/schemas\", LOCAL)\n        .contentType(MediaType.APPLICATION_JSON)\n        .body(BodyInserters.fromPublisher(Mono.just(requestBody), NewSchemaSubjectDTO.class))\n        .exchange()\n        .expectStatus()\n        .isOk()\n        .expectBody(SchemaSubjectDTO.class)\n        .returnResult()\n        .getResponseBody();\n\n    Assertions.assertNotNull(actual);\n    Assertions.assertEquals(CompatibilityLevelDTO.CompatibilityEnum.BACKWARD.name(),\n        actual.getCompatibilityLevel());\n    Assertions.assertEquals(\"1\", actual.getVersion());\n    Assertions.assertEquals(SchemaTypeDTO.PROTOBUF, actual.getSchemaType());\n    Assertions.assertEquals(schema, actual.getSchema());\n  }\n\n\n  @Test\n  void shouldCreateNewProtobufSchemaWithRefs() {\n    NewSchemaSubjectDTO requestBody = new NewSchemaSubjectDTO()\n        .schemaType(SchemaTypeDTO.PROTOBUF)\n        .subject(subject + \"-ref\")\n        .schema(\"\"\"\n            syntax = \"proto3\";\n            message MyRecord {\n              int32 id = 1;\n              string name = 2;\n            }\n            \"\"\");\n\n    webTestClient\n        .post()\n        .uri(\"/api/clusters/{clusterName}/schemas\", LOCAL)\n        .contentType(MediaType.APPLICATION_JSON)\n        .body(BodyInserters.fromPublisher(Mono.just(requestBody), NewSchemaSubjectDTO.class))\n        .exchange()\n        .expectStatus()\n        .isOk();\n\n    requestBody = new NewSchemaSubjectDTO()\n        .schemaType(SchemaTypeDTO.PROTOBUF)\n        .subject(subject)\n        .schema(\"\"\"\n            syntax = \"proto3\";\n            import \"MyRecord.proto\";\n            message MyRecordWithRef {\n              int32 id = 1;\n              MyRecord my_ref = 2;\n            }\n            \"\"\")\n        .references(List.of(new SchemaReferenceDTO().name(\"MyRecord.proto\").subject(subject + \"-ref\").version(1)));\n\n    SchemaSubjectDTO actual = webTestClient\n        .post()\n        .uri(\"/api/clusters/{clusterName}/schemas\", LOCAL)\n        .contentType(MediaType.APPLICATION_JSON)\n        .body(BodyInserters.fromPublisher(Mono.just(requestBody), NewSchemaSubjectDTO.class))\n        .exchange()\n        .expectStatus()\n        .isOk()\n        .expectBody(SchemaSubjectDTO.class)\n        .returnResult()\n        .getResponseBody();\n\n    Assertions.assertNotNull(actual);\n    Assertions.assertEquals(requestBody.getReferences(), actual.getReferences());\n  }\n\n  @Test\n  public void shouldReturnBackwardAsGlobalCompatibilityLevelByDefault() {\n    webTestClient\n        .get()\n        .uri(\"/api/clusters/{clusterName}/schemas/compatibility\", LOCAL)\n        .exchange()\n        .expectStatus().isOk()\n        .expectBody(CompatibilityLevelDTO.class)\n        .consumeWith(result -> {\n          CompatibilityLevelDTO responseBody = result.getResponseBody();\n          Assertions.assertNotNull(responseBody);\n          Assertions.assertEquals(CompatibilityLevelDTO.CompatibilityEnum.BACKWARD,\n              responseBody.getCompatibility());\n        });\n  }\n\n  @Test\n  public void shouldReturnNotEmptyResponseWhenGetAllSchemas() {\n    createNewSubjectAndAssert(subject);\n\n    webTestClient\n        .get()\n        .uri(\"/api/clusters/{clusterName}/schemas\", LOCAL)\n        .exchange()\n        .expectStatus().isOk()\n        .expectBody(SchemaSubjectsResponseDTO.class)\n        .consumeWith(result -> {\n          SchemaSubjectsResponseDTO responseBody = result.getResponseBody();\n          log.info(\"Response of test schemas: {}\", responseBody);\n          Assertions.assertNotNull(responseBody);\n          Assertions.assertFalse(responseBody.getSchemas().isEmpty());\n\n          SchemaSubjectDTO actualSchemaSubject = responseBody.getSchemas().stream()\n              .filter(schemaSubject -> subject.equals(schemaSubject.getSubject()))\n              .findFirst()\n              .orElseThrow();\n          Assertions.assertNotNull(actualSchemaSubject.getId());\n          Assertions.assertNotNull(actualSchemaSubject.getVersion());\n          Assertions.assertNotNull(actualSchemaSubject.getCompatibilityLevel());\n          Assertions.assertEquals(\"\\\"string\\\"\", actualSchemaSubject.getSchema());\n        });\n  }\n\n  @Test\n  public void shouldOkWhenCreateNewSchemaThenGetAndUpdateItsCompatibilityLevel() {\n    createNewSubjectAndAssert(subject);\n\n    //Get the created schema and check its items\n    webTestClient\n        .get()\n        .uri(\"/api/clusters/{clusterName}/schemas/{subject}/latest\", LOCAL, subject)\n        .exchange()\n        .expectStatus().isOk()\n        .expectBodyList(SchemaSubjectDTO.class)\n        .consumeWith(listEntityExchangeResult -> {\n          val expectedCompatibility =\n              CompatibilityLevelDTO.CompatibilityEnum.BACKWARD;\n          assertSchemaWhenGetLatest(subject, listEntityExchangeResult, expectedCompatibility);\n        });\n\n    // Now let's change compatibility level of this schema to FULL whereas the global\n    // level should be BACKWARD\n\n    webTestClient.put()\n        .uri(\"/api/clusters/{clusterName}/schemas/{subject}/compatibility\", LOCAL, subject)\n        .contentType(MediaType.APPLICATION_JSON)\n        .body(BodyInserters.fromValue(\"{\\\"compatibility\\\":\\\"FULL\\\"}\"))\n        .exchange()\n        .expectStatus().isOk();\n\n    //Get one more time to check the schema compatibility level is changed to FULL\n    webTestClient\n        .get()\n        .uri(\"/api/clusters/{clusterName}/schemas/{subject}/latest\", LOCAL, subject)\n        .exchange()\n        .expectStatus().isOk()\n        .expectBodyList(SchemaSubjectDTO.class)\n        .consumeWith(listEntityExchangeResult -> {\n          val expectedCompatibility =\n              CompatibilityLevelDTO.CompatibilityEnum.FULL;\n          assertSchemaWhenGetLatest(subject, listEntityExchangeResult, expectedCompatibility);\n        });\n  }\n\n  @Test\n  void shouldCreateNewSchemaWhenSubjectIncludesNonAsciiCharacters() {\n    String schema =\n        \"{\\\"subject\\\":\\\"test/test\\\",\\\"schemaType\\\":\\\"JSON\\\",\\\"schema\\\":\"\n            + \"\\\"{\\\\\\\"type\\\\\\\": \\\\\\\"string\\\\\\\"}\\\"}\";\n\n    webTestClient\n        .post()\n        .uri(\"/api/clusters/{clusterName}/schemas\", LOCAL)\n        .contentType(MediaType.APPLICATION_JSON)\n        .body(BodyInserters.fromValue(schema))\n        .exchange()\n        .expectStatus().isOk();\n  }\n\n  private void createNewSubjectAndAssert(String subject) {\n    webTestClient\n        .post()\n        .uri(\"/api/clusters/{clusterName}/schemas\", LOCAL)\n        .contentType(MediaType.APPLICATION_JSON)\n        .body(BodyInserters.fromValue(\n            String.format(\n                \"{\\\"subject\\\":\\\"%s\\\",\\\"schemaType\\\":\\\"AVRO\\\",\\\"schema\\\":\"\n                    + \"\\\"{\\\\\\\"type\\\\\\\": \\\\\\\"string\\\\\\\"}\\\"}\",\n                subject\n            )\n        ))\n        .exchange()\n        .expectStatus().isOk()\n        .expectBody(SchemaSubjectDTO.class)\n        .consumeWith(this::assertResponseBodyWhenCreateNewSchema);\n  }\n\n  private void assertSchemaWhenGetLatest(\n      String subject, EntityExchangeResult<List<SchemaSubjectDTO>> listEntityExchangeResult,\n      CompatibilityLevelDTO.CompatibilityEnum expectedCompatibility) {\n    List<SchemaSubjectDTO> responseBody = listEntityExchangeResult.getResponseBody();\n    Assertions.assertNotNull(responseBody);\n    Assertions.assertEquals(1, responseBody.size());\n    SchemaSubjectDTO actualSchema = responseBody.get(0);\n    Assertions.assertNotNull(actualSchema);\n    Assertions.assertEquals(subject, actualSchema.getSubject());\n    Assertions.assertEquals(\"\\\"string\\\"\", actualSchema.getSchema());\n\n    Assertions.assertNotNull(actualSchema.getCompatibilityLevel());\n    Assertions.assertEquals(SchemaTypeDTO.AVRO, actualSchema.getSchemaType());\n    Assertions.assertEquals(expectedCompatibility.name(), actualSchema.getCompatibilityLevel());\n  }\n\n  private void assertResponseBodyWhenCreateNewSchema(\n      EntityExchangeResult<SchemaSubjectDTO> exchangeResult) {\n    SchemaSubjectDTO responseBody = exchangeResult.getResponseBody();\n    Assertions.assertNotNull(responseBody);\n    Assertions.assertEquals(\"1\", responseBody.getVersion());\n    Assertions.assertNotNull(responseBody.getSchema());\n    Assertions.assertNotNull(responseBody.getSubject());\n    Assertions.assertNotNull(responseBody.getCompatibilityLevel());\n    Assertions.assertEquals(SchemaTypeDTO.AVRO, responseBody.getSchemaType());\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/config/ClustersPropertiesTest.java",
    "content": "package com.provectus.kafka.ui.config;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nimport java.util.Collections;\nimport org.junit.jupiter.api.Test;\n\nclass ClustersPropertiesTest {\n\n  @Test\n  void clusterNamesShouldBeUniq() {\n    ClustersProperties properties = new ClustersProperties();\n    var c1 = new ClustersProperties.Cluster();\n    c1.setName(\"test\");\n    var c2 = new ClustersProperties.Cluster();\n    c2.setName(\"test\"); //same name\n\n    Collections.addAll(properties.getClusters(), c1, c2);\n\n    assertThatThrownBy(properties::validateAndSetDefaults)\n        .hasMessageContaining(\"Application config isn't valid\");\n  }\n\n  @Test\n  void clusterNamesShouldSetIfMultipleClustersProvided() {\n    ClustersProperties properties = new ClustersProperties();\n    var c1 = new ClustersProperties.Cluster();\n    c1.setName(\"test1\");\n    var c2 = new ClustersProperties.Cluster(); //name not set\n\n    Collections.addAll(properties.getClusters(), c1, c2);\n\n    assertThatThrownBy(properties::validateAndSetDefaults)\n        .hasMessageContaining(\"Application config isn't valid\");\n  }\n\n  @Test\n  void ifOnlyOneClusterProvidedNameIsOptionalAndSetToDefault() {\n    ClustersProperties properties = new ClustersProperties();\n    properties.getClusters().add(new ClustersProperties.Cluster());\n\n    properties.validateAndSetDefaults();\n\n    assertThat(properties.getClusters())\n        .element(0)\n        .extracting(\"name\")\n        .isEqualTo(\"Default\");\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/container/KafkaConnectContainer.java",
    "content": "package com.provectus.kafka.ui.container;\n\nimport java.time.Duration;\nimport org.testcontainers.containers.GenericContainer;\nimport org.testcontainers.containers.KafkaContainer;\nimport org.testcontainers.containers.Network;\nimport org.testcontainers.containers.wait.strategy.Wait;\n\npublic class KafkaConnectContainer extends GenericContainer<KafkaConnectContainer> {\n  private static final int CONNECT_PORT = 8083;\n\n  public KafkaConnectContainer(String version) {\n    super(\"confluentinc/cp-kafka-connect:\" + version);\n    addExposedPort(CONNECT_PORT);\n\n    waitStrategy = Wait.forHttp(\"/\")\n        .withStartupTimeout(Duration.ofMinutes(5));\n  }\n\n\n  public KafkaConnectContainer withKafka(KafkaContainer kafka) {\n    String bootstrapServers = kafka.getNetworkAliases().get(0) + \":9092\";\n    return withKafka(kafka.getNetwork(), bootstrapServers);\n  }\n\n  public KafkaConnectContainer withKafka(Network network, String bootstrapServers) {\n    withNetwork(network);\n    withEnv(\"CONNECT_BOOTSTRAP_SERVERS\", \"PLAINTEXT://\" + bootstrapServers);\n    withEnv(\"CONNECT_GROUP_ID\", \"connect-group\");\n    withEnv(\"CONNECT_CONFIG_STORAGE_TOPIC\", \"_connect_configs\");\n    withEnv(\"CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR\", \"1\");\n    withEnv(\"CONNECT_OFFSET_STORAGE_TOPIC\", \"_connect_offset\");\n    withEnv(\"CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR\", \"1\");\n    withEnv(\"CONNECT_STATUS_STORAGE_TOPIC\", \"_connect_status\");\n    withEnv(\"CONNECT_STATUS_STORAGE_REPLICATION_FACTOR\", \"1\");\n    withEnv(\"CONNECT_KEY_CONVERTER\", \"org.apache.kafka.connect.storage.StringConverter\");\n    withEnv(\"CONNECT_VALUE_CONVERTER\", \"org.apache.kafka.connect.storage.StringConverter\");\n    withEnv(\"CONNECT_INTERNAL_KEY_CONVERTER\", \"org.apache.kafka.connect.json.JsonConverter\");\n    withEnv(\"CONNECT_INTERNAL_VALUE_CONVERTER\", \"org.apache.kafka.connect.json.JsonConverter\");\n    withEnv(\"CONNECT_REST_ADVERTISED_HOST_NAME\", \"kafka-connect\");\n    withEnv(\"CONNECT_REST_PORT\", String.valueOf(CONNECT_PORT));\n    withEnv(\"CONNECT_PLUGIN_PATH\",\n        \"/usr/share/java,/usr/share/confluent-hub-components,\"\n            // adding additional paths to find FileStreamSinkConnector\n            + \"/usr/local/share/kafka/plugins,/usr/share/filestream-connectors\");\n    return self();\n  }\n\n  public String getTarget() {\n    return \"http://\" + getContainerIpAddress() + \":\" + getMappedPort(CONNECT_PORT);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/container/KsqlDbContainer.java",
    "content": "package com.provectus.kafka.ui.container;\n\nimport java.time.Duration;\nimport org.testcontainers.containers.GenericContainer;\nimport org.testcontainers.containers.KafkaContainer;\nimport org.testcontainers.containers.Network;\nimport org.testcontainers.containers.wait.strategy.Wait;\nimport org.testcontainers.utility.DockerImageName;\n\npublic class KsqlDbContainer extends GenericContainer<KsqlDbContainer> {\n\n  private static final int PORT = 8088;\n\n  public KsqlDbContainer(DockerImageName imageName) {\n    super(imageName);\n    addExposedPort(PORT);\n    waitStrategy = Wait\n        .forHttp(\"/info\")\n        .forStatusCode(200)\n        .withStartupTimeout(Duration.ofMinutes(5));\n  }\n\n  public KsqlDbContainer withKafka(KafkaContainer kafka) {\n    dependsOn(kafka);\n    String bootstrapServers = kafka.getNetworkAliases().get(0) + \":9092\";\n    return withKafka(kafka.getNetwork(), bootstrapServers);\n  }\n\n  private KsqlDbContainer withKafka(Network network, String bootstrapServers) {\n    withNetwork(network);\n    withEnv(\"KSQL_LISTENERS\", \"http://0.0.0.0:\" + PORT);\n    withEnv(\"KSQL_BOOTSTRAP_SERVERS\", bootstrapServers);\n    return self();\n  }\n\n  public String url() {\n    return \"http://\" + getContainerIpAddress() + \":\" + getMappedPort(PORT);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/container/SchemaRegistryContainer.java",
    "content": "package com.provectus.kafka.ui.container;\n\nimport io.confluent.kafka.schemaregistry.client.CachedSchemaRegistryClient;\nimport io.confluent.kafka.schemaregistry.client.SchemaRegistryClient;\nimport org.testcontainers.containers.GenericContainer;\nimport org.testcontainers.containers.KafkaContainer;\nimport org.testcontainers.containers.Network;\n\npublic class SchemaRegistryContainer extends GenericContainer<SchemaRegistryContainer> {\n  private static final int SCHEMA_PORT = 8081;\n\n  public SchemaRegistryContainer(String version) {\n    super(\"confluentinc/cp-schema-registry:\" + version);\n    withExposedPorts(8081);\n  }\n\n  public SchemaRegistryContainer withKafka(KafkaContainer kafka) {\n    String bootstrapServers = kafka.getNetworkAliases().get(0) + \":9092\";\n    return withKafka(kafka.getNetwork(), bootstrapServers);\n  }\n\n  public SchemaRegistryContainer withKafka(Network network, String bootstrapServers) {\n    withNetwork(network);\n    withEnv(\"SCHEMA_REGISTRY_HOST_NAME\", \"schema-registry\");\n    withEnv(\"SCHEMA_REGISTRY_LISTENERS\", \"http://0.0.0.0:\" + SCHEMA_PORT);\n    withEnv(\"SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS\", \"PLAINTEXT://\" + bootstrapServers);\n    return self();\n  }\n\n  public String getUrl() {\n    return \"http://\" + getContainerIpAddress() + \":\" + getMappedPort(SCHEMA_PORT);\n  }\n\n  public SchemaRegistryClient schemaRegistryClient() {\n    return new CachedSchemaRegistryClient(getUrl(), 1000);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/controller/ApplicationConfigControllerTest.java",
    "content": "package com.provectus.kafka.ui.controller;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.provectus.kafka.ui.AbstractIntegrationTest;\nimport com.provectus.kafka.ui.model.UploadedFileInfoDTO;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.core.io.ClassPathResource;\nimport org.springframework.http.HttpEntity;\nimport org.springframework.http.client.MultipartBodyBuilder;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.util.MultiValueMap;\n\nclass ApplicationConfigControllerTest extends AbstractIntegrationTest {\n\n  @Autowired\n  private WebTestClient webTestClient;\n\n  @Test\n  public void testUpload() throws IOException {\n    var fileToUpload = new ClassPathResource(\"/fileForUploadTest.txt\", this.getClass());\n\n    UploadedFileInfoDTO result = webTestClient\n        .post()\n        .uri(\"/api/config/relatedfiles\")\n        .bodyValue(generateBody(fileToUpload))\n        .exchange()\n        .expectStatus()\n        .isOk()\n        .expectBody(UploadedFileInfoDTO.class)\n        .returnResult()\n        .getResponseBody();\n\n    assertThat(result).isNotNull();\n    assertThat(result.getLocation()).isNotNull();\n    assertThat(Path.of(result.getLocation()))\n        .hasSameBinaryContentAs(fileToUpload.getFile().toPath());\n  }\n\n  private MultiValueMap<String, HttpEntity<?>> generateBody(ClassPathResource resource) {\n    MultipartBodyBuilder builder = new MultipartBodyBuilder();\n    builder.part(\"file\", resource);\n    return builder.build();\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/MessageFiltersTest.java",
    "content": "package com.provectus.kafka.ui.emitter;\n\nimport static com.provectus.kafka.ui.emitter.MessageFilters.containsStringFilter;\nimport static com.provectus.kafka.ui.emitter.MessageFilters.groovyScriptFilter;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.model.TopicMessageDTO;\nimport java.time.OffsetDateTime;\nimport java.time.temporal.ChronoUnit;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Predicate;\nimport org.apache.commons.lang3.RandomStringUtils;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\n\nclass MessageFiltersTest {\n\n  @Nested\n  class StringContainsFilter {\n\n    Predicate<TopicMessageDTO> filter = containsStringFilter(\"abC\");\n\n    @Test\n    void returnsTrueWhenStringContainedInKeyOrContentOrInBoth() {\n      assertTrue(\n          filter.test(msg().key(\"contains abCd\").content(\"some str\"))\n      );\n\n      assertTrue(\n          filter.test(msg().key(\"some str\").content(\"contains abCd\"))\n      );\n\n      assertTrue(\n          filter.test(msg().key(\"contains abCd\").content(\"contains abCd\"))\n      );\n    }\n\n    @Test\n    void returnsFalseOtherwise() {\n      assertFalse(\n          filter.test(msg().key(\"some str\").content(\"some str\"))\n      );\n\n      assertFalse(\n          filter.test(msg().key(null).content(null))\n      );\n\n      assertFalse(\n          filter.test(msg().key(\"aBc\").content(\"AbC\"))\n      );\n    }\n\n  }\n\n  @Nested\n  class GroovyScriptFilter {\n\n    @Test\n    void throwsExceptionOnInvalidGroovySyntax() {\n      assertThrows(ValidationException.class,\n          () -> groovyScriptFilter(\"this is invalid groovy syntax = 1\"));\n    }\n\n    @Test\n    void canCheckPartition() {\n      var f = groovyScriptFilter(\"partition == 1\");\n      assertTrue(f.test(msg().partition(1)));\n      assertFalse(f.test(msg().partition(0)));\n    }\n\n    @Test\n    void canCheckOffset() {\n      var f = groovyScriptFilter(\"offset == 100\");\n      assertTrue(f.test(msg().offset(100L)));\n      assertFalse(f.test(msg().offset(200L)));\n    }\n\n    @Test\n    void canCheckHeaders() {\n      var f = groovyScriptFilter(\"headers.size() == 2 && headers['k1'] == 'v1'\");\n      assertTrue(f.test(msg().headers(Map.of(\"k1\", \"v1\", \"k2\", \"v2\"))));\n      assertFalse(f.test(msg().headers(Map.of(\"k1\", \"unexpected\", \"k2\", \"v2\"))));\n    }\n\n    @Test\n    void canCheckTimestampMs() {\n      var ts = OffsetDateTime.now();\n      var f = groovyScriptFilter(\"timestampMs == \" + ts.toInstant().toEpochMilli());\n      assertTrue(f.test(msg().timestamp(ts)));\n      assertFalse(f.test(msg().timestamp(ts.plus(1L, ChronoUnit.SECONDS))));\n    }\n\n    @Test\n    void canCheckValueAsText() {\n      var f = groovyScriptFilter(\"valueAsText == 'some text'\");\n      assertTrue(f.test(msg().content(\"some text\")));\n      assertFalse(f.test(msg().content(\"some other text\")));\n    }\n\n    @Test\n    void canCheckKeyAsText() {\n      var f = groovyScriptFilter(\"keyAsText == 'some text'\");\n      assertTrue(f.test(msg().key(\"some text\")));\n      assertFalse(f.test(msg().key(\"some other text\")));\n    }\n\n    @Test\n    void canCheckKeyAsJsonObjectIfItCanBeParsedToJson() {\n      var f = groovyScriptFilter(\"key.name.first == 'user1'\");\n      assertTrue(f.test(msg().key(\"{ \\\"name\\\" : { \\\"first\\\" : \\\"user1\\\" } }\")));\n      assertFalse(f.test(msg().key(\"{ \\\"name\\\" : { \\\"first\\\" : \\\"user2\\\" } }\")));\n    }\n\n    @Test\n    void keySetToKeyStringIfCantBeParsedToJson() {\n      var f = groovyScriptFilter(\"key == \\\"not json\\\"\");\n      assertTrue(f.test(msg().key(\"not json\")));\n    }\n\n    @Test\n    void keyAndKeyAsTextSetToNullIfRecordsKeyIsNull() {\n      var f = groovyScriptFilter(\"key == null\");\n      assertTrue(f.test(msg().key(null)));\n\n      f = groovyScriptFilter(\"keyAsText == null\");\n      assertTrue(f.test(msg().key(null)));\n    }\n\n    @Test\n    void canCheckValueAsJsonObjectIfItCanBeParsedToJson() {\n      var f = groovyScriptFilter(\"value.name.first == 'user1'\");\n      assertTrue(f.test(msg().content(\"{ \\\"name\\\" : { \\\"first\\\" : \\\"user1\\\" } }\")));\n      assertFalse(f.test(msg().content(\"{ \\\"name\\\" : { \\\"first\\\" : \\\"user2\\\" } }\")));\n    }\n\n    @Test\n    void valueSetToContentStringIfCantBeParsedToJson() {\n      var f = groovyScriptFilter(\"value == \\\"not json\\\"\");\n      assertTrue(f.test(msg().content(\"not json\")));\n    }\n\n    @Test\n    void valueAndValueAsTextSetToNullIfRecordsContentIsNull() {\n      var f = groovyScriptFilter(\"value == null\");\n      assertTrue(f.test(msg().content(null)));\n\n      f = groovyScriptFilter(\"valueAsText == null\");\n      assertTrue(f.test(msg().content(null)));\n    }\n\n    @Test\n    void canRunMultiStatementScripts() {\n      var f = groovyScriptFilter(\"def name = value.name.first \\n return name == 'user1' \");\n      assertTrue(f.test(msg().content(\"{ \\\"name\\\" : { \\\"first\\\" : \\\"user1\\\" } }\")));\n      assertFalse(f.test(msg().content(\"{ \\\"name\\\" : { \\\"first\\\" : \\\"user2\\\" } }\")));\n\n      f = groovyScriptFilter(\"def name = value.name.first; return name == 'user1' \");\n      assertTrue(f.test(msg().content(\"{ \\\"name\\\" : { \\\"first\\\" : \\\"user1\\\" } }\")));\n      assertFalse(f.test(msg().content(\"{ \\\"name\\\" : { \\\"first\\\" : \\\"user2\\\" } }\")));\n\n      f = groovyScriptFilter(\"def name = value.name.first; name == 'user1' \");\n      assertTrue(f.test(msg().content(\"{ \\\"name\\\" : { \\\"first\\\" : \\\"user1\\\" } }\")));\n      assertFalse(f.test(msg().content(\"{ \\\"name\\\" : { \\\"first\\\" : \\\"user2\\\" } }\")));\n    }\n\n\n    @Test\n    void filterSpeedIsAtLeast5kPerSec() {\n      var f = groovyScriptFilter(\"value.name.first == 'user1' && keyAsText.startsWith('a') \");\n\n      List<TopicMessageDTO> toFilter = new ArrayList<>();\n      for (int i = 0; i < 5_000; i++) {\n        String name = i % 2 == 0 ? \"user1\" : RandomStringUtils.randomAlphabetic(10);\n        String randString = RandomStringUtils.randomAlphabetic(30);\n        String jsonContent = String.format(\n            \"{ \\\"name\\\" : {  \\\"randomStr\\\": \\\"%s\\\", \\\"first\\\" : \\\"%s\\\"} }\",\n            randString, name);\n        toFilter.add(msg().content(jsonContent).key(randString));\n      }\n      // first iteration for warmup\n      toFilter.stream().filter(f).count();\n\n      long before = System.currentTimeMillis();\n      long matched = toFilter.stream().filter(f).count();\n      long took = System.currentTimeMillis() - before;\n\n      assertThat(took).isLessThan(1000);\n      assertThat(matched).isGreaterThan(0);\n    }\n  }\n\n  private TopicMessageDTO msg() {\n    return new TopicMessageDTO()\n        .timestamp(OffsetDateTime.now())\n        .partition(1);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/MessagesProcessingTest.java",
    "content": "package com.provectus.kafka.ui.emitter;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.time.OffsetDateTime;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Optional;\nimport org.apache.kafka.clients.consumer.ConsumerRecord;\nimport org.apache.kafka.common.header.internals.RecordHeaders;\nimport org.apache.kafka.common.record.TimestampType;\nimport org.apache.kafka.common.utils.Bytes;\nimport org.junit.jupiter.api.RepeatedTest;\n\nclass MessagesProcessingTest {\n\n\n  @RepeatedTest(5)\n  void testSortingAsc() {\n    var messagesInOrder = List.of(\n        consumerRecord(1, 100L, \"1999-01-01T00:00:00+00:00\"),\n        consumerRecord(0, 0L, \"2000-01-01T00:00:00+00:00\"),\n        consumerRecord(1, 200L, \"2000-01-05T00:00:00+00:00\"),\n        consumerRecord(0, 10L, \"2000-01-10T00:00:00+00:00\"),\n        consumerRecord(0, 20L, \"2000-01-20T00:00:00+00:00\"),\n        consumerRecord(1, 300L, \"3000-01-01T00:00:00+00:00\"),\n        consumerRecord(2, 1000L, \"4000-01-01T00:00:00+00:00\"),\n        consumerRecord(2, 1001L, \"2000-01-01T00:00:00+00:00\"),\n        consumerRecord(2, 1003L, \"3000-01-01T00:00:00+00:00\")\n    );\n\n    var shuffled = new ArrayList<>(messagesInOrder);\n    Collections.shuffle(shuffled);\n\n    var sortedList = MessagesProcessing.sortForSending(shuffled, true);\n    assertThat(sortedList).containsExactlyElementsOf(messagesInOrder);\n  }\n\n  @RepeatedTest(5)\n  void testSortingDesc() {\n    var messagesInOrder = List.of(\n        consumerRecord(1, 300L, \"3000-01-01T00:00:00+00:00\"),\n        consumerRecord(2, 1003L, \"3000-01-01T00:00:00+00:00\"),\n        consumerRecord(0, 20L, \"2000-01-20T00:00:00+00:00\"),\n        consumerRecord(0, 10L, \"2000-01-10T00:00:00+00:00\"),\n        consumerRecord(1, 200L, \"2000-01-05T00:00:00+00:00\"),\n        consumerRecord(0, 0L, \"2000-01-01T00:00:00+00:00\"),\n        consumerRecord(2, 1001L, \"2000-01-01T00:00:00+00:00\"),\n        consumerRecord(2, 1000L, \"4000-01-01T00:00:00+00:00\"),\n        consumerRecord(1, 100L, \"1999-01-01T00:00:00+00:00\")\n    );\n\n    var shuffled = new ArrayList<>(messagesInOrder);\n    Collections.shuffle(shuffled);\n\n    var sortedList = MessagesProcessing.sortForSending(shuffled, false);\n    assertThat(sortedList).containsExactlyElementsOf(messagesInOrder);\n  }\n\n  private ConsumerRecord<Bytes, Bytes> consumerRecord(int partition, long offset, String ts) {\n    return new ConsumerRecord<>(\n        \"topic\", partition, offset, OffsetDateTime.parse(ts).toInstant().toEpochMilli(),\n        TimestampType.CREATE_TIME,\n        0, 0, null, null, new RecordHeaders(), Optional.empty()\n    );\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/OffsetsInfoTest.java",
    "content": "package com.provectus.kafka.ui.emitter;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport org.apache.kafka.clients.consumer.MockConsumer;\nimport org.apache.kafka.clients.consumer.OffsetResetStrategy;\nimport org.apache.kafka.common.PartitionInfo;\nimport org.apache.kafka.common.TopicPartition;\nimport org.apache.kafka.common.utils.Bytes;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nclass OffsetsInfoTest {\n\n  final String topic = \"test\";\n  final TopicPartition tp0 = new TopicPartition(topic, 0); //offsets: start 0, end 0\n  final TopicPartition tp1 = new TopicPartition(topic, 1); //offsets: start 10, end 10\n  final TopicPartition tp2 = new TopicPartition(topic, 2); //offsets: start 0, end 20\n  final TopicPartition tp3 = new TopicPartition(topic, 3); //offsets: start 25, end 30\n\n  MockConsumer<Bytes, Bytes> consumer;\n\n  @BeforeEach\n  void initMockConsumer() {\n    consumer = new MockConsumer<>(OffsetResetStrategy.EARLIEST);\n    consumer.updatePartitions(\n        topic,\n        Stream.of(tp0, tp1, tp2, tp3)\n            .map(tp -> new PartitionInfo(topic, tp.partition(), null, null, null, null))\n            .collect(Collectors.toList()));\n    consumer.updateBeginningOffsets(Map.of(tp0, 0L, tp1, 10L, tp2, 0L, tp3, 25L));\n    consumer.updateEndOffsets(Map.of(tp0, 0L, tp1, 10L, tp2, 20L, tp3, 30L));\n  }\n\n  @Test\n  void fillsInnerFieldsAccordingToTopicState() {\n    var offsets = new OffsetsInfo(consumer, List.of(tp0, tp1, tp2, tp3));\n\n    assertThat(offsets.getBeginOffsets()).containsEntry(tp0, 0L).containsEntry(tp1, 10L).containsEntry(tp2, 0L)\n        .containsEntry(tp3, 25L);\n\n    assertThat(offsets.getEndOffsets()).containsEntry(tp0, 0L).containsEntry(tp1, 10L).containsEntry(tp2, 20L)\n        .containsEntry(tp3, 30L);\n\n    assertThat(offsets.getEmptyPartitions()).contains(tp0, tp1);\n    assertThat(offsets.getNonEmptyPartitions()).contains(tp2, tp3);\n  }\n\n}"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/SeekOperationsTest.java",
    "content": "package com.provectus.kafka.ui.emitter;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.provectus.kafka.ui.model.SeekTypeDTO;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport org.apache.kafka.clients.consumer.MockConsumer;\nimport org.apache.kafka.clients.consumer.OffsetResetStrategy;\nimport org.apache.kafka.common.PartitionInfo;\nimport org.apache.kafka.common.TopicPartition;\nimport org.apache.kafka.common.utils.Bytes;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\n\nclass SeekOperationsTest {\n\n  final String topic = \"test\";\n  final TopicPartition tp0 = new TopicPartition(topic, 0); //offsets: start 0, end 0\n  final TopicPartition tp1 = new TopicPartition(topic, 1); //offsets: start 10, end 10\n  final TopicPartition tp2 = new TopicPartition(topic, 2); //offsets: start 0, end 20\n  final TopicPartition tp3 = new TopicPartition(topic, 3); //offsets: start 25, end 30\n\n  MockConsumer<Bytes, Bytes> consumer;\n\n  @BeforeEach\n  void initMockConsumer() {\n    consumer = new MockConsumer<>(OffsetResetStrategy.EARLIEST);\n    consumer.updatePartitions(\n        topic,\n        Stream.of(tp0, tp1, tp2, tp3)\n            .map(tp -> new PartitionInfo(topic, tp.partition(), null, null, null, null))\n            .collect(Collectors.toList()));\n    consumer.updateBeginningOffsets(Map.of(tp0, 0L, tp1, 10L, tp2, 0L, tp3, 25L));\n    consumer.updateEndOffsets(Map.of(tp0, 0L, tp1, 10L, tp2, 20L, tp3, 30L));\n  }\n\n  @Nested\n  class GetOffsetsForSeek {\n\n    @Test\n    void latest() {\n      var offsets = SeekOperations.getOffsetsForSeek(\n          consumer,\n          new OffsetsInfo(consumer, topic),\n          SeekTypeDTO.LATEST,\n          null\n      );\n      assertThat(offsets).containsExactlyInAnyOrderEntriesOf(Map.of(tp2, 20L, tp3, 30L));\n    }\n\n    @Test\n    void beginning() {\n      var offsets = SeekOperations.getOffsetsForSeek(\n          consumer,\n          new OffsetsInfo(consumer, topic),\n          SeekTypeDTO.BEGINNING,\n          null\n      );\n      assertThat(offsets).containsExactlyInAnyOrderEntriesOf(Map.of(tp2, 0L, tp3, 25L));\n    }\n\n    @Test\n    void offsets() {\n      var offsets = SeekOperations.getOffsetsForSeek(\n          consumer,\n          new OffsetsInfo(consumer, topic),\n          SeekTypeDTO.OFFSET,\n          Map.of(tp1, 10L, tp2, 10L, tp3, 26L)\n      );\n      assertThat(offsets).containsExactlyInAnyOrderEntriesOf(Map.of(tp2, 10L, tp3, 26L));\n    }\n\n    @Test\n    void offsetsWithBoundsFixing() {\n      var offsets = SeekOperations.getOffsetsForSeek(\n          consumer,\n          new OffsetsInfo(consumer, topic),\n          SeekTypeDTO.OFFSET,\n          Map.of(tp1, 10L, tp2, 21L, tp3, 24L)\n      );\n      assertThat(offsets).containsExactlyInAnyOrderEntriesOf(Map.of(tp2, 20L, tp3, 25L));\n    }\n  }\n\n}"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/TailingEmitterTest.java",
    "content": "package com.provectus.kafka.ui.emitter;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.provectus.kafka.ui.AbstractIntegrationTest;\nimport com.provectus.kafka.ui.model.ConsumerPosition;\nimport com.provectus.kafka.ui.model.MessageFilterTypeDTO;\nimport com.provectus.kafka.ui.model.SeekDirectionDTO;\nimport com.provectus.kafka.ui.model.SeekTypeDTO;\nimport com.provectus.kafka.ui.model.TopicMessageEventDTO;\nimport com.provectus.kafka.ui.service.ClustersStorage;\nimport com.provectus.kafka.ui.service.MessagesService;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport org.apache.kafka.clients.admin.NewTopic;\nimport org.apache.kafka.clients.producer.KafkaProducer;\nimport org.apache.kafka.clients.producer.ProducerConfig;\nimport org.apache.kafka.clients.producer.ProducerRecord;\nimport org.apache.kafka.common.serialization.StringSerializer;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.testcontainers.shaded.org.awaitility.Awaitility;\nimport reactor.core.Disposable;\nimport reactor.core.publisher.Flux;\n\nclass TailingEmitterTest extends AbstractIntegrationTest {\n\n  private String topic;\n\n  private KafkaProducer<String, String> producer;\n\n  private Disposable tailingFluxDispose;\n\n  @BeforeEach\n  void init() {\n    topic = \"TopicTailingTest_\" + UUID.randomUUID();\n    createTopic(new NewTopic(topic, 2, (short) 1));\n    producer = new KafkaProducer<>(\n        Map.of(\n            ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers(),\n            ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class,\n            ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class\n        ));\n  }\n\n  @AfterEach\n  void tearDown() {\n    deleteTopic(topic);\n    if (tailingFluxDispose != null) {\n      tailingFluxDispose.dispose();\n    }\n  }\n\n  @Test\n  void allNewMessagesShouldBeEmitted() throws Exception {\n    var fluxOutput = startTailing(null);\n\n    List<String> expectedValues = new ArrayList<>();\n    for (int i = 0; i < 50; i++) {\n      producer.send(new ProducerRecord<>(topic, i + \"\", i + \"\")).get();\n      expectedValues.add(i + \"\");\n    }\n\n    Awaitility.await()\n        .atMost(Duration.ofSeconds(60))\n        .pollInSameThread()\n        .untilAsserted(() ->\n            assertThat(fluxOutput)\n              .filteredOn(msg -> msg.getType() == TopicMessageEventDTO.TypeEnum.MESSAGE)\n              .extracting(msg -> msg.getMessage().getContent())\n              .hasSameElementsAs(expectedValues)\n        );\n  }\n\n  @Test\n  void allNewMessageThatFitFilterConditionShouldBeEmitted() throws Exception {\n    var fluxOutput = startTailing(\"good\");\n\n    List<String> expectedValues = new ArrayList<>();\n    for (int i = 0; i < 50; i++) {\n      if (i % 2 == 0) {\n        producer.send(new ProducerRecord<>(topic, i + \"\", i + \"_good\")).get();\n        expectedValues.add(i + \"_good\");\n      } else {\n        producer.send(new ProducerRecord<>(topic, i + \"\", i + \"_bad\")).get();\n      }\n    }\n\n    Awaitility.await()\n        .atMost(Duration.ofSeconds(60))\n        .pollInSameThread()\n        .untilAsserted(() ->\n            assertThat(fluxOutput)\n              .filteredOn(msg -> msg.getType() == TopicMessageEventDTO.TypeEnum.MESSAGE)\n              .extracting(msg -> msg.getMessage().getContent())\n              .hasSameElementsAs(expectedValues)\n        );\n  }\n\n  private Flux<TopicMessageEventDTO> createTailingFlux(\n      String topicName,\n      String query) {\n    var cluster = applicationContext.getBean(ClustersStorage.class)\n        .getClusterByName(LOCAL)\n        .get();\n\n    return applicationContext.getBean(MessagesService.class)\n        .loadMessages(cluster, topicName,\n            new ConsumerPosition(SeekTypeDTO.LATEST, topic, null),\n            query,\n            MessageFilterTypeDTO.STRING_CONTAINS,\n            0,\n            SeekDirectionDTO.TAILING,\n            \"String\",\n            \"String\");\n  }\n\n  private List<TopicMessageEventDTO> startTailing(String filterQuery) {\n    List<TopicMessageEventDTO> fluxOutput = new CopyOnWriteArrayList<>();\n    tailingFluxDispose = createTailingFlux(topic, filterQuery)\n        .doOnNext(fluxOutput::add)\n        .subscribe();\n\n    // this is needed to be sure that tailing is initialized\n    // and we can start to produce test messages\n    waitUntilTailingInitialized(fluxOutput);\n\n    return fluxOutput;\n  }\n\n\n  private void waitUntilTailingInitialized(List<TopicMessageEventDTO> fluxOutput) {\n    Awaitility.await()\n        .pollInSameThread()\n        .pollDelay(Duration.ofMillis(100))\n        .atMost(Duration.ofSeconds(200))\n        .until(() -> fluxOutput.stream()\n            .anyMatch(msg -> msg.getType() == TopicMessageEventDTO.TypeEnum.CONSUMING));\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/model/PartitionDistributionStatsTest.java",
    "content": "package com.provectus.kafka.ui.model;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.provectus.kafka.ui.service.ReactiveAdminClient;\nimport java.math.BigDecimal;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport org.apache.kafka.clients.admin.TopicDescription;\nimport org.apache.kafka.common.Node;\nimport org.apache.kafka.common.TopicPartitionInfo;\nimport org.assertj.core.data.Percentage;\nimport org.junit.jupiter.api.Test;\n\nclass PartitionDistributionStatsTest {\n\n  @Test\n  void skewCalculatedBasedOnPartitionsCounts() {\n    Node n1 = new Node(1, \"n1\", 9092);\n    Node n2 = new Node(2, \"n2\", 9092);\n    Node n3 = new Node(3, \"n3\", 9092);\n    Node n4 = new Node(4, \"n4\", 9092);\n\n    var stats = PartitionDistributionStats.create(\n        Statistics.builder()\n            .clusterDescription(\n                new ReactiveAdminClient.ClusterDescription(null, \"test\", Set.of(n1, n2, n3), null))\n            .topicDescriptions(\n                Map.of(\n                    \"t1\", new TopicDescription(\n                        \"t1\", false,\n                        List.of(\n                            new TopicPartitionInfo(0, n1, List.of(n1, n2), List.of(n1, n2)),\n                            new TopicPartitionInfo(1, n2, List.of(n2, n3), List.of(n2, n3))\n                        )\n                    ),\n                    \"t2\", new TopicDescription(\n                        \"t2\", false,\n                        List.of(\n                            new TopicPartitionInfo(0, n1, List.of(n1, n2), List.of(n1, n2)),\n                            new TopicPartitionInfo(1, null, List.of(n2, n1), List.of(n1))\n                        )\n                    )\n                )\n            )\n            .build(), 4\n    );\n\n    assertThat(stats.getPartitionLeaders())\n        .containsExactlyInAnyOrderEntriesOf(Map.of(n1, 2, n2, 1));\n    assertThat(stats.getPartitionsCount())\n        .containsExactlyInAnyOrderEntriesOf(Map.of(n1, 3, n2, 4, n3, 1));\n    assertThat(stats.getInSyncPartitions())\n        .containsExactlyInAnyOrderEntriesOf(Map.of(n1, 3, n2, 3, n3, 1));\n\n    // Node(partitions): n1(3), n2(4), n3(1), n4(0)\n    // average partitions cnt = (3+4+1) / 3 = 2.666 (counting only nodes with partitions!)\n    assertThat(stats.getAvgPartitionsPerBroker())\n        .isCloseTo(2.666, Percentage.withPercentage(1));\n\n    assertThat(stats.partitionsSkew(n1))\n        .isCloseTo(BigDecimal.valueOf(12.5), Percentage.withPercentage(1));\n    assertThat(stats.partitionsSkew(n2))\n        .isCloseTo(BigDecimal.valueOf(50), Percentage.withPercentage(1));\n    assertThat(stats.partitionsSkew(n3))\n        .isCloseTo(BigDecimal.valueOf(-62.5), Percentage.withPercentage(1));\n    assertThat(stats.partitionsSkew(n4))\n        .isCloseTo(BigDecimal.valueOf(-100), Percentage.withPercentage(1));\n\n    //  Node(leaders): n1(2), n2(1), n3(0), n4(0)\n    //  average leaders cnt = (2+1) / 2 = 1.5 (counting only nodes with leaders!)\n    assertThat(stats.leadersSkew(n1))\n        .isCloseTo(BigDecimal.valueOf(33.33), Percentage.withPercentage(1));\n    assertThat(stats.leadersSkew(n2))\n        .isCloseTo(BigDecimal.valueOf(-33.33), Percentage.withPercentage(1));\n    assertThat(stats.leadersSkew(n3))\n        .isCloseTo(BigDecimal.valueOf(-100), Percentage.withPercentage(1));\n    assertThat(stats.leadersSkew(n4))\n        .isCloseTo(BigDecimal.valueOf(-100), Percentage.withPercentage(1));\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/producer/KafkaTestProducer.java",
    "content": "package com.provectus.kafka.ui.producer;\n\nimport java.util.Map;\nimport java.util.concurrent.CompletableFuture;\nimport org.apache.kafka.clients.producer.KafkaProducer;\nimport org.apache.kafka.clients.producer.ProducerConfig;\nimport org.apache.kafka.clients.producer.ProducerRecord;\nimport org.apache.kafka.clients.producer.RecordMetadata;\nimport org.apache.kafka.common.serialization.StringSerializer;\nimport org.testcontainers.containers.KafkaContainer;\n\npublic class KafkaTestProducer<KeyT, ValueT> implements AutoCloseable {\n  private final KafkaProducer<KeyT, ValueT> producer;\n\n  private KafkaTestProducer(KafkaProducer<KeyT, ValueT> producer) {\n    this.producer = producer;\n  }\n\n  public static KafkaTestProducer<String, String> forKafka(KafkaContainer kafkaContainer) {\n    return new KafkaTestProducer<>(new KafkaProducer<>(Map.of(\n        ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaContainer.getBootstrapServers(),\n        ProducerConfig.CLIENT_ID_CONFIG, \"KafkaTestProducer\",\n        ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class,\n        ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class\n    )));\n  }\n\n  public CompletableFuture<RecordMetadata> send(String topic, ValueT value) {\n    return send(new ProducerRecord<>(topic, value));\n  }\n\n  public CompletableFuture<RecordMetadata> send(ProducerRecord<KeyT, ValueT> record) {\n    CompletableFuture<RecordMetadata> cf = new CompletableFuture<>();\n    producer.send(record, (m, e) -> {\n      if (e != null) {\n        cf.completeExceptionally(e);\n      } else {\n        cf.complete(m);\n      }\n    });\n    return cf;\n  }\n\n  @Override\n  public void close() {\n    producer.close();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/ConsumerRecordDeserializerTest.java",
    "content": "package com.provectus.kafka.ui.serdes;\n\nimport static com.provectus.kafka.ui.serde.api.DeserializeResult.Type.STRING;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\n\nimport com.provectus.kafka.ui.model.TopicMessageDTO;\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport java.util.Map;\nimport java.util.function.UnaryOperator;\nimport org.apache.kafka.clients.consumer.ConsumerRecord;\nimport org.apache.kafka.common.utils.Bytes;\nimport org.junit.jupiter.api.Test;\n\nclass ConsumerRecordDeserializerTest {\n\n  @Test\n  void dataMaskingAppliedOnDeserializedMessage() {\n    UnaryOperator<TopicMessageDTO> maskerMock = mock();\n    Serde.Deserializer deser = (headers, data) -> new DeserializeResult(\"test\", STRING, Map.of());\n\n    var recordDeser = new ConsumerRecordDeserializer(\"test\", deser, \"test\", deser, \"test\", deser, deser, maskerMock);\n    recordDeser.deserialize(new ConsumerRecord<>(\"t\", 1, 1L, Bytes.wrap(\"t\".getBytes()), Bytes.wrap(\"t\".getBytes())));\n\n    verify(maskerMock).apply(any(TopicMessageDTO.class));\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/PropertyResolverImplTest.java",
    "content": "package com.provectus.kafka.ui.serdes;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatCode;\n\nimport java.util.List;\nimport java.util.Map;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.context.properties.bind.BindException;\nimport org.springframework.mock.env.MockEnvironment;\n\nclass PropertyResolverImplTest {\n\n  private static final String TEST_STRING_VALUE = \"testStr\";\n  private static final int TEST_INT_VALUE = 123;\n  private static final List<String> TEST_STRING_LIST = List.of(\"v1\", \"v2\", \"v3\");\n  private static final List<Integer> TEST_INT_LIST = List.of(1, 2, 3);\n\n  private final MockEnvironment env = new MockEnvironment();\n\n  @Data\n  @AllArgsConstructor\n  public static class CustomPropertiesClass {\n    private String f1;\n    private Integer f2;\n  }\n\n  @Test\n  void returnsEmptyOptionalWhenPropertyNotExist() {\n    var resolver = new PropertyResolverImpl(env);\n    assertThat(resolver.getProperty(\"nonExistingProp\", String.class)).isEmpty();\n    assertThat(resolver.getListProperty(\"nonExistingProp\", String.class)).isEmpty();\n    assertThat(resolver.getMapProperty(\"nonExistingProp\", String.class, String.class)).isEmpty();\n  }\n\n  @Test\n  void throwsExceptionWhenPropertyCantBeResolverToRequstedClass() {\n    env.setProperty(\"prop.0.strProp\", \"testStr\");\n    env.setProperty(\"prop.0.strLst\", \"v1,v2,v3\");\n    env.setProperty(\"prop.0.strMap.k1\", \"v1\");\n\n    var resolver = new PropertyResolverImpl(env);\n    assertThatCode(() -> resolver.getProperty(\"prop.0.strProp\", Integer.class))\n        .isInstanceOf(BindException.class);\n    assertThatCode(() -> resolver.getListProperty(\"prop.0.strLst\", Integer.class))\n        .isInstanceOf(BindException.class);\n    assertThatCode(() -> resolver.getMapProperty(\"prop.0.strMap\", Integer.class, String.class))\n        .isInstanceOf(BindException.class);\n  }\n\n  @Test\n  void resolvedSingleValueProperties() {\n    env.setProperty(\"prop.0.strProp\", \"testStr\");\n    env.setProperty(\"prop.0.intProp\", \"123\");\n\n    var resolver = new PropertyResolverImpl(env);\n    assertThat(resolver.getProperty(\"prop.0.strProp\", String.class))\n        .hasValue(\"testStr\");\n    assertThat(resolver.getProperty(\"prop.0.intProp\", Integer.class))\n        .hasValue(123);\n  }\n\n  @Test\n  void resolvesListProperties() {\n    env.setProperty(\"prop.0.strLst\", \"v1,v2,v3\");\n    env.setProperty(\"prop.0.intLst\", \"1,2,3\");\n\n    var resolver = new PropertyResolverImpl(env);\n    assertThat(resolver.getListProperty(\"prop.0.strLst\", String.class))\n        .hasValue(List.of(\"v1\", \"v2\", \"v3\"));\n    assertThat(resolver.getListProperty(\"prop.0.intLst\", Integer.class))\n        .hasValue(List.of(1, 2, 3));\n  }\n\n  @Test\n  void resolvesCustomConfigClassProperties() {\n    env.setProperty(\"prop.0.custProps.f1\", \"f1val\");\n    env.setProperty(\"prop.0.custProps.f2\", \"1234\");\n\n    var resolver = new PropertyResolverImpl(env);\n    assertThat(resolver.getProperty(\"prop.0.custProps\", CustomPropertiesClass.class))\n        .hasValue(new CustomPropertiesClass(\"f1val\", 1234));\n  }\n\n  @Test\n  void resolvesMapProperties() {\n    env.setProperty(\"prop.0.strMap.k1\", \"v1\");\n    env.setProperty(\"prop.0.strMap.k2\", \"v2\");\n    env.setProperty(\"prop.0.intToLongMap.100\", \"111\");\n    env.setProperty(\"prop.0.intToLongMap.200\", \"222\");\n\n    var resolver = new PropertyResolverImpl(env);\n    assertThat(resolver.getMapProperty(\"prop.0.strMap\", String.class, String.class))\n        .hasValue(Map.of(\"k1\", \"v1\", \"k2\", \"v2\"));\n    assertThat(resolver.getMapProperty(\"prop.0.intToLongMap\", Integer.class, Long.class))\n        .hasValue(Map.of(100, 111L, 200, 222L));\n  }\n\n\n  @Nested\n  class WithPrefix {\n\n    @Test\n    void resolvedSingleValueProperties() {\n      env.setProperty(\"prop.0.strProp\", \"testStr\");\n      env.setProperty(\"prop.0.intProp\", \"123\");\n\n      var resolver = new PropertyResolverImpl(env, \"prop.0\");\n      assertThat(resolver.getProperty(\"strProp\", String.class))\n          .hasValue(TEST_STRING_VALUE);\n\n      assertThat(resolver.getProperty(\"intProp\", Integer.class))\n          .hasValue(TEST_INT_VALUE);\n    }\n\n    @Test\n    void resolvesListProperties() {\n      env.setProperty(\"prop.0.strLst\", \"v1,v2,v3\");\n      env.setProperty(\"prop.0.intLst\", \"1,2,3\");\n\n      var resolver = new PropertyResolverImpl(env, \"prop.0\");\n      assertThat(resolver.getListProperty(\"strLst\", String.class))\n          .hasValue(TEST_STRING_LIST);\n      assertThat(resolver.getListProperty(\"intLst\", Integer.class))\n          .hasValue(TEST_INT_LIST);\n    }\n\n    @Test\n    void resolvesCustomConfigClassProperties() {\n      env.setProperty(\"prop.0.custProps.f1\", \"f1val\");\n      env.setProperty(\"prop.0.custProps.f2\", \"1234\");\n\n      var  resolver = new PropertyResolverImpl(env, \"prop.0\");\n      assertThat(resolver.getProperty(\"custProps\", CustomPropertiesClass.class))\n          .hasValue(new CustomPropertiesClass(\"f1val\", 1234));\n    }\n\n    @Test\n    void resolvesMapProperties() {\n      env.setProperty(\"prop.0.strMap.k1\", \"v1\");\n      env.setProperty(\"prop.0.strMap.k2\", \"v2\");\n      env.setProperty(\"prop.0.intToLongMap.100\", \"111\");\n      env.setProperty(\"prop.0.intToLongMap.200\", \"222\");\n\n      var resolver = new PropertyResolverImpl(env, \"prop.0.\");\n      assertThat(resolver.getMapProperty(\"strMap\", String.class, String.class))\n          .hasValue(Map.of(\"k1\", \"v1\", \"k2\", \"v2\"));\n      assertThat(resolver.getMapProperty(\"intToLongMap\", Integer.class, Long.class))\n          .hasValue(Map.of(100, 111L, 200, 222L));\n    }\n  }\n\n}"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/SerdesInitializerTest.java",
    "content": "package com.provectus.kafka.ui.serdes;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatCode;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.anyString;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.serde.api.PropertyResolver;\nimport com.provectus.kafka.ui.serdes.builtin.Int32Serde;\nimport com.provectus.kafka.ui.serdes.builtin.StringSerde;\nimport java.net.URL;\nimport java.net.URLClassLoader;\nimport java.util.List;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.core.env.Environment;\nimport org.springframework.mock.env.MockEnvironment;\n\nclass SerdesInitializerTest {\n\n  private final Environment env = new MockEnvironment();\n  private final CustomSerdeLoader customSerdeLoaderMock = mock(CustomSerdeLoader.class);\n\n  private final SerdesInitializer initializer = new SerdesInitializer(\n      Map.of(\n          \"BuiltIn1\", BuiltInSerdeWithAutoconfigure.class,\n          \"BuiltIn2\", BuiltInSerdeMock2NoAutoConfigure.class,\n          Int32Serde.name(), Int32Serde.class,\n          StringSerde.name(), StringSerde.class\n      ),\n      customSerdeLoaderMock\n  );\n\n  @Test\n  void pluggedSerdesInitializedByLoader() {\n    ClustersProperties.SerdeConfig customSerdeConfig = new ClustersProperties.SerdeConfig();\n    customSerdeConfig.setName(\"MyPluggedSerde\");\n    customSerdeConfig.setFilePath(\"/custom.jar\");\n    customSerdeConfig.setClassName(\"org.test.MyPluggedSerde\");\n    customSerdeConfig.setTopicKeysPattern(\"keys\");\n    customSerdeConfig.setTopicValuesPattern(\"values\");\n\n    when(customSerdeLoaderMock.loadAndConfigure(anyString(), anyString(), any(), any(), any()))\n        .thenReturn(new CustomSerdeLoader.CustomSerde(new StringSerde(), new URLClassLoader(new URL[]{})));\n\n    var serdes = init(customSerdeConfig);\n\n    SerdeInstance customSerdeInstance = serdes.serdes.get(\"MyPluggedSerde\");\n    verifyPatternsMatch(customSerdeConfig, customSerdeInstance);\n    assertThat(customSerdeInstance.classLoader).isNotNull();\n\n    verify(customSerdeLoaderMock).loadAndConfigure(\n        eq(customSerdeConfig.getClassName()),\n        eq(customSerdeConfig.getFilePath()),\n        any(), any(), any()\n    );\n  }\n\n  @Test\n  void serdeWithBuiltInNameAndNoPropertiesCantBeInitializedIfSerdeNotSupportAutoConfigure() {\n    ClustersProperties.SerdeConfig serdeConfig = new ClustersProperties.SerdeConfig();\n    serdeConfig.setName(\"BuiltIn2\"); //auto-configuration not supported\n    serdeConfig.setTopicKeysPattern(\"keys\");\n    serdeConfig.setTopicValuesPattern(\"vals\");\n\n    assertThatCode(() -> initializer.init(env, createProperties(serdeConfig), 0))\n        .isInstanceOf(ValidationException.class);\n  }\n\n  @Test\n  void serdeWithBuiltInNameAndNoPropertiesIsAutoConfiguredIfPossible() {\n    ClustersProperties.SerdeConfig serdeConfig = new ClustersProperties.SerdeConfig();\n    serdeConfig.setName(\"BuiltIn1\"); // supports auto-configuration\n    serdeConfig.setTopicKeysPattern(\"keys\");\n    serdeConfig.setTopicValuesPattern(\"vals\");\n\n    var serdes = init(serdeConfig);\n\n    SerdeInstance autoConfiguredSerde = serdes.serdes.get(\"BuiltIn1\");\n    verifyAutoConfigured(autoConfiguredSerde);\n    verifyPatternsMatch(serdeConfig, autoConfiguredSerde);\n  }\n\n  @Test\n  void serdeWithBuiltInNameAndSetPropertiesAreExplicitlyConfigured() {\n    ClustersProperties.SerdeConfig serdeConfig = new ClustersProperties.SerdeConfig();\n    serdeConfig.setName(\"BuiltIn1\");\n    serdeConfig.setProperties(Map.of(\"any\", \"property\"));\n    serdeConfig.setTopicKeysPattern(\"keys\");\n    serdeConfig.setTopicValuesPattern(\"vals\");\n\n    var serdes = init(serdeConfig);\n\n    SerdeInstance explicitlyConfiguredSerde = serdes.serdes.get(\"BuiltIn1\");\n    verifyExplicitlyConfigured(explicitlyConfiguredSerde);\n    verifyPatternsMatch(serdeConfig, explicitlyConfiguredSerde);\n  }\n\n  @Test\n  void serdeWithCustomNameAndBuiltInClassnameAreExplicitlyConfigured() {\n    ClustersProperties.SerdeConfig serdeConfig = new ClustersProperties.SerdeConfig();\n    serdeConfig.setName(\"SomeSerde\");\n    serdeConfig.setClassName(BuiltInSerdeWithAutoconfigure.class.getName());\n    serdeConfig.setTopicKeysPattern(\"keys\");\n    serdeConfig.setTopicValuesPattern(\"vals\");\n\n    var serdes = init(serdeConfig);\n\n    SerdeInstance explicitlyConfiguredSerde = serdes.serdes.get(\"SomeSerde\");\n    verifyExplicitlyConfigured(explicitlyConfiguredSerde);\n    verifyPatternsMatch(serdeConfig, explicitlyConfiguredSerde);\n  }\n\n  private ClusterSerdes init(ClustersProperties.SerdeConfig... serdeConfigs) {\n    return initializer.init(env, createProperties(serdeConfigs), 0);\n  }\n\n  private ClustersProperties createProperties(ClustersProperties.SerdeConfig... serdeConfigs) {\n    ClustersProperties.Cluster cluster = new ClustersProperties.Cluster();\n    cluster.setName(\"test\");\n    cluster.setSerde(List.of(serdeConfigs));\n\n    ClustersProperties clustersProperties = new ClustersProperties();\n    clustersProperties.setClusters(List.of(cluster));\n    return clustersProperties;\n  }\n\n  private void verifyExplicitlyConfigured(SerdeInstance serde) {\n    assertThat(((BuiltInSerdeWithAutoconfigure) serde.serde).autoConfigureCheckCalled).isFalse();\n    assertThat(((BuiltInSerdeWithAutoconfigure) serde.serde).autoConfigured).isFalse();\n    assertThat(((BuiltInSerdeWithAutoconfigure) serde.serde).explicitlyConfigured).isTrue();\n  }\n\n  private void verifyAutoConfigured(SerdeInstance serde) {\n    assertThat(((BuiltInSerdeWithAutoconfigure) serde.serde).autoConfigureCheckCalled).isTrue();\n    assertThat(((BuiltInSerdeWithAutoconfigure) serde.serde).autoConfigured).isTrue();\n    assertThat(((BuiltInSerdeWithAutoconfigure) serde.serde).explicitlyConfigured).isFalse();\n  }\n\n  private void verifyPatternsMatch(ClustersProperties.SerdeConfig config, SerdeInstance serde) {\n    assertThat(serde.topicKeyPattern.pattern()).isEqualTo(config.getTopicKeysPattern());\n    assertThat(serde.topicValuePattern.pattern()).isEqualTo(config.getTopicValuesPattern());\n  }\n\n  static class BuiltInSerdeWithAutoconfigure extends StringSerde {\n\n    boolean explicitlyConfigured = false;\n    boolean autoConfigured = false;\n    boolean autoConfigureCheckCalled = false;\n\n    @Override\n    public boolean canBeAutoConfigured(PropertyResolver kafkaClusterProperties, PropertyResolver globalProperties) {\n      this.autoConfigureCheckCalled = true;\n      return true;\n    }\n\n    @Override\n    public void autoConfigure(PropertyResolver kafkaClusterProperties, PropertyResolver globalProperties) {\n      this.autoConfigured = true;\n    }\n\n    @Override\n    public void configure(PropertyResolver serdeProperties,\n                          PropertyResolver kafkaClusterProperties,\n                          PropertyResolver globalProperties) {\n      this.explicitlyConfigured = true;\n    }\n  }\n\n  static class BuiltInSerdeMock2NoAutoConfigure extends BuiltInSerdeWithAutoconfigure {\n    @Override\n    public boolean canBeAutoConfigured(PropertyResolver kafkaClusterProperties, PropertyResolver globalProperties) {\n      this.autoConfigureCheckCalled = true;\n      return false;\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/AvroEmbeddedSerdeTest.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport com.provectus.kafka.ui.serdes.PropertyResolverImpl;\nimport io.confluent.kafka.schemaregistry.avro.AvroSchemaUtils;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport org.apache.avro.Schema;\nimport org.apache.avro.file.DataFileWriter;\nimport org.apache.avro.generic.GenericData;\nimport org.apache.avro.generic.GenericDatumWriter;\nimport org.apache.avro.generic.GenericRecord;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\n\nclass AvroEmbeddedSerdeTest {\n\n  private AvroEmbeddedSerde avroEmbeddedSerde;\n\n  @BeforeEach\n  void init() {\n    avroEmbeddedSerde = new AvroEmbeddedSerde();\n    avroEmbeddedSerde.configure(\n        PropertyResolverImpl.empty(),\n        PropertyResolverImpl.empty(),\n        PropertyResolverImpl.empty()\n    );\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void canDeserializeReturnsTrueForAllTargets(Serde.Target target) {\n    assertThat(avroEmbeddedSerde.canDeserialize(\"anyTopic\", target))\n        .isTrue();\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void canSerializeReturnsFalseForAllTargets(Serde.Target target) {\n    assertThat(avroEmbeddedSerde.canSerialize(\"anyTopic\", target))\n        .isFalse();\n  }\n\n  @Test\n  void deserializerParsesAvroDataWithEmbeddedSchema() throws Exception {\n    Schema schema = new Schema.Parser().parse(\"\"\"\n        {\n          \"type\": \"record\",\n          \"name\": \"TestAvroRecord\",\n          \"fields\": [\n            { \"name\": \"field1\", \"type\": \"string\" },\n            { \"name\": \"field2\", \"type\": \"int\" }\n          ]\n        }\n        \"\"\"\n    );\n    GenericRecord record = new GenericData.Record(schema);\n    record.put(\"field1\", \"this is test msg\");\n    record.put(\"field2\", 100500);\n\n    String jsonRecord = new String(AvroSchemaUtils.toJson(record));\n    byte[] serializedRecordBytes = serializeAvroWithEmbeddedSchema(record);\n\n    var deserializer = avroEmbeddedSerde.deserializer(\"anyTopic\", Serde.Target.KEY);\n    DeserializeResult result = deserializer.deserialize(null, serializedRecordBytes);\n    assertThat(result.getType()).isEqualTo(DeserializeResult.Type.JSON);\n    assertThat(result.getAdditionalProperties()).isEmpty();\n    assertJsonEquals(jsonRecord, result.getResult());\n  }\n\n  private void assertJsonEquals(String expected, String actual) throws IOException {\n    var mapper = new JsonMapper();\n    assertThat(mapper.readTree(actual)).isEqualTo(mapper.readTree(expected));\n  }\n\n  private byte[] serializeAvroWithEmbeddedSchema(GenericRecord record) throws IOException {\n    try (DataFileWriter<GenericRecord> writer = new DataFileWriter<>(new GenericDatumWriter<>());\n         ByteArrayOutputStream baos = new ByteArrayOutputStream()) {\n      writer.create(record.getSchema(), baos);\n      writer.append(record);\n      writer.flush();\n      return baos.toByteArray();\n    }\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/Base64SerdeTest.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport com.provectus.kafka.ui.serdes.PropertyResolverImpl;\nimport com.provectus.kafka.ui.serdes.RecordHeadersImpl;\nimport java.util.Base64;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\n\nclass Base64SerdeTest {\n\n  private static final byte[] TEST_BYTES = \"some bytes go here\".getBytes();\n  private static final String TEST_BYTES_BASE64_ENCODED = Base64.getEncoder().encodeToString(TEST_BYTES);\n\n  private Serde base64Serde;\n\n  @BeforeEach\n  void init() {\n    base64Serde = new Base64Serde();\n    base64Serde.configure(\n        PropertyResolverImpl.empty(),\n        PropertyResolverImpl.empty(),\n        PropertyResolverImpl.empty()\n    );\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void serializesInputAsBase64String(Serde.Target type) {\n    var serializer = base64Serde.serializer(\"anyTopic\", type);\n    byte[] bytes = serializer.serialize(TEST_BYTES_BASE64_ENCODED);\n    assertThat(bytes).isEqualTo(TEST_BYTES);\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void deserializesDataAsBase64Bytes(Serde.Target type) {\n    var deserializer = base64Serde.deserializer(\"anyTopic\", type);\n    var result = deserializer.deserialize(new RecordHeadersImpl(), TEST_BYTES);\n    assertThat(result.getResult()).isEqualTo(TEST_BYTES_BASE64_ENCODED);\n    assertThat(result.getType()).isEqualTo(DeserializeResult.Type.STRING);\n    assertThat(result.getAdditionalProperties()).isEmpty();\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void getSchemaReturnsEmpty(Serde.Target type) {\n    assertThat(base64Serde.getSchema(\"anyTopic\", type)).isEmpty();\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void canDeserializeReturnsTrueForAllInputs(Serde.Target type) {\n    assertThat(base64Serde.canDeserialize(\"anyTopic\", type)).isTrue();\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void canSerializeReturnsTrueForAllInput(Serde.Target type) {\n    assertThat(base64Serde.canSerialize(\"anyTopic\", type)).isTrue();\n  }\n}"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/ConsumerOffsetsSerdeTest.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport static com.provectus.kafka.ui.serdes.builtin.ConsumerOffsetsSerde.TOPIC;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport com.provectus.kafka.ui.AbstractIntegrationTest;\nimport com.provectus.kafka.ui.producer.KafkaTestProducer;\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Properties;\nimport java.util.Set;\nimport java.util.UUID;\nimport lombok.SneakyThrows;\nimport org.apache.kafka.clients.admin.NewTopic;\nimport org.apache.kafka.clients.consumer.ConsumerConfig;\nimport org.apache.kafka.clients.consumer.KafkaConsumer;\nimport org.apache.kafka.common.serialization.BytesDeserializer;\nimport org.apache.kafka.common.utils.Bytes;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.testcontainers.shaded.org.awaitility.Awaitility;\nimport reactor.util.function.Tuple2;\nimport reactor.util.function.Tuples;\n\nclass ConsumerOffsetsSerdeTest extends AbstractIntegrationTest {\n\n  private static final int MSGS_TO_GENERATE = 10;\n\n  private static String consumerGroupName;\n  private static String committedTopic;\n\n  @BeforeAll\n  static void createTopicAndCommitItsOffset() {\n    committedTopic = ConsumerOffsetsSerdeTest.class.getSimpleName() + \"-\" + UUID.randomUUID();\n    consumerGroupName = committedTopic + \"-group\";\n    createTopic(new NewTopic(committedTopic, 1, (short) 1));\n\n    try (var producer = KafkaTestProducer.forKafka(kafka)) {\n      for (int i = 0; i < MSGS_TO_GENERATE; i++) {\n        producer.send(committedTopic, \"i=\" + i);\n      }\n    }\n    try (var consumer = createConsumer(consumerGroupName)) {\n      consumer.subscribe(List.of(committedTopic));\n      int polled = 0;\n      while (polled < MSGS_TO_GENERATE) {\n        polled += consumer.poll(Duration.ofMillis(100)).count();\n      }\n      consumer.commitSync();\n    }\n  }\n\n  @AfterAll\n  static void cleanUp() {\n    deleteTopic(committedTopic);\n  }\n\n  @Test\n  void canOnlyDeserializeConsumerOffsetsTopic() {\n    var serde = new ConsumerOffsetsSerde();\n    assertThat(serde.canDeserialize(ConsumerOffsetsSerde.TOPIC, Serde.Target.KEY)).isTrue();\n    assertThat(serde.canDeserialize(ConsumerOffsetsSerde.TOPIC, Serde.Target.VALUE)).isTrue();\n    assertThat(serde.canDeserialize(\"anyOtherTopic\", Serde.Target.KEY)).isFalse();\n    assertThat(serde.canDeserialize(\"anyOtherTopic\", Serde.Target.VALUE)).isFalse();\n  }\n\n  @Test\n  void deserializesMessagesMadeByConsumerActivity() {\n    var serde = new ConsumerOffsetsSerde();\n    var keyDeserializer = serde.deserializer(TOPIC, Serde.Target.KEY);\n    var valueDeserializer = serde.deserializer(TOPIC, Serde.Target.VALUE);\n\n    try (var consumer = createConsumer(consumerGroupName + \"-check\")) {\n      consumer.subscribe(List.of(ConsumerOffsetsSerde.TOPIC));\n      List<Tuple2<DeserializeResult, DeserializeResult>> polled = new ArrayList<>();\n\n      Awaitility.await()\n          .pollInSameThread()\n          .atMost(Duration.ofMinutes(1))\n          .untilAsserted(() -> {\n            for (var rec : consumer.poll(Duration.ofMillis(200))) {\n              DeserializeResult key = rec.key() != null\n                  ? keyDeserializer.deserialize(null, rec.key().get())\n                  : null;\n              DeserializeResult val = rec.value() != null\n                  ? valueDeserializer.deserialize(null, rec.value().get())\n                  : null;\n              if (key != null && val != null) {\n                polled.add(Tuples.of(key, val));\n              }\n            }\n            assertThat(polled).anyMatch(t -> isCommitMessage(t.getT1(), t.getT2()));\n            assertThat(polled).anyMatch(t -> isGroupMetadataMessage(t.getT1(), t.getT2()));\n          });\n    }\n  }\n\n  // Sample commit record:\n  //\n  // key: {\n  //  \"group\": \"test_Members_3\",\n  //  \"topic\": \"test\",\n  //  \"partition\": 0\n  // }\n  //\n  // value:\n  // {\n  //  \"offset\": 2,\n  //  \"leader_epoch\": 0,\n  //  \"metadata\": \"\",\n  //  \"commit_timestamp\": 1683112980588\n  // }\n  private boolean isCommitMessage(DeserializeResult key, DeserializeResult value) {\n    var keyJson = toMapFromJsom(key);\n    boolean keyIsOk = consumerGroupName.equals(keyJson.get(\"group\"))\n        && committedTopic.equals(keyJson.get(\"topic\"))\n        && ((Integer) 0).equals(keyJson.get(\"partition\"));\n\n    var valueJson = toMapFromJsom(value);\n    boolean valueIsOk = valueJson.containsKey(\"offset\")\n        && valueJson.get(\"offset\").equals(MSGS_TO_GENERATE)\n        && valueJson.containsKey(\"commit_timestamp\");\n\n    return keyIsOk && valueIsOk;\n  }\n\n  // Sample group metadata record:\n  //\n  // key: {\n  //  \"group\": \"test_Members_3\"\n  // }\n  //\n  // value:\n  // {\n  //  \"protocol_type\": \"consumer\",\n  //  \"generation\": 1,\n  //  \"protocol\": \"range\",\n  //  \"leader\": \"consumer-test_Members_3-1-5a37876e-e42f-420e-9c7d-6902889bd5dd\",\n  //  \"current_state_timestamp\": 1683112974561,\n  //  \"members\": [\n  //    {\n  //      \"member_id\": \"consumer-test_Members_3-1-5a37876e-e42f-420e-9c7d-6902889bd5dd\",\n  //      \"group_instance_id\": null,\n  //      \"client_id\": \"consumer-test_Members_3-1\",\n  //      \"client_host\": \"/192.168.16.1\",\n  //      \"rebalance_timeout\": 300000,\n  //      \"session_timeout\": 45000,\n  //      \"subscription\": \"AAEAAAABAAR0ZXN0/////wAAAAA=\",\n  //      \"assignment\": \"AAEAAAABAAR0ZXN0AAAAAQAAAAD/////\"\n  //    }\n  //  ]\n  // }\n  private boolean isGroupMetadataMessage(DeserializeResult key, DeserializeResult value) {\n    var keyJson = toMapFromJsom(key);\n    boolean keyIsOk = consumerGroupName.equals(keyJson.get(\"group\")) && keyJson.size() == 1;\n\n    var valueJson = toMapFromJsom(value);\n    boolean valueIsOk = valueJson.keySet()\n        .containsAll(Set.of(\"protocol_type\", \"generation\", \"leader\", \"members\"));\n\n    return keyIsOk && valueIsOk;\n  }\n\n  @SneakyThrows\n  private Map<String, Object> toMapFromJsom(DeserializeResult result) {\n    return new JsonMapper().readValue(result.getResult(), Map.class);\n  }\n\n  private static KafkaConsumer<Bytes, Bytes> createConsumer(String groupId) {\n    Properties props = new Properties();\n    props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);\n    props.put(ConsumerConfig.CLIENT_ID_CONFIG, groupId);\n    props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());\n    props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, BytesDeserializer.class);\n    props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, BytesDeserializer.class);\n    props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, \"earliest\");\n    return new KafkaConsumer<>(props);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/HexSerdeTest.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport com.provectus.kafka.ui.serdes.PropertyResolverImpl;\nimport com.provectus.kafka.ui.serdes.RecordHeadersImpl;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.CsvSource;\nimport org.junit.jupiter.params.provider.EnumSource;\n\npublic class HexSerdeTest {\n\n  private static final byte[] TEST_BYTES = \"hello world\".getBytes();\n  private static final String TEST_BYTES_HEX_ENCODED = \"68 65 6C 6C 6F 20 77 6F 72 6C 64\";\n\n  private HexSerde hexSerde;\n\n  @BeforeEach\n  void init() {\n    hexSerde = new HexSerde();\n    hexSerde.autoConfigure(PropertyResolverImpl.empty(), PropertyResolverImpl.empty());\n  }\n\n\n  @ParameterizedTest\n  @CsvSource({\n      \"68656C6C6F20776F726C64\", // uppercase\n      \"68656c6c6f20776f726c64\", // lowercase\n      \"68:65:6c:6c:6f:20:77:6f:72:6c:64\", // ':' delim\n      \"68 65 6C 6C 6F 20 77 6F 72 6C 64\", // space delim, UC\n      \"68 65 6c 6c 6f 20 77 6f 72 6c 64\", // space delim, LC\n      \"#68 #65 #6C #6C #6F #20 #77 #6F #72 #6C #64\"  // '#' prefix, space delim\n  })\n  void serializesInputAsHexString(String hexString) {\n    for (Serde.Target type : Serde.Target.values()) {\n      var serializer = hexSerde.serializer(\"anyTopic\", type);\n      byte[] bytes = serializer.serialize(hexString);\n      assertThat(bytes).isEqualTo(TEST_BYTES);\n    }\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void serializesEmptyStringAsEmptyBytesArray(Serde.Target type) {\n    var serializer = hexSerde.serializer(\"anyTopic\", type);\n    byte[] bytes = serializer.serialize(\"\");\n    assertThat(bytes).isEqualTo(new byte[] {});\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void deserializesDataAsHexBytes(Serde.Target type) {\n    var deserializer = hexSerde.deserializer(\"anyTopic\", type);\n    var result = deserializer.deserialize(new RecordHeadersImpl(), TEST_BYTES);\n    assertThat(result.getResult()).isEqualTo(TEST_BYTES_HEX_ENCODED);\n    assertThat(result.getType()).isEqualTo(DeserializeResult.Type.STRING);\n    assertThat(result.getAdditionalProperties()).isEmpty();\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void getSchemaReturnsEmpty(Serde.Target type) {\n    assertThat(hexSerde.getSchema(\"anyTopic\", type)).isEmpty();\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void canDeserializeReturnsTrueForAllInputs(Serde.Target type) {\n    assertThat(hexSerde.canDeserialize(\"anyTopic\", type)).isTrue();\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void canSerializeReturnsTrueForAllInput(Serde.Target type) {\n    assertThat(hexSerde.canSerialize(\"anyTopic\", type)).isTrue();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/Int32SerdeTest.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.google.common.primitives.Ints;\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport com.provectus.kafka.ui.serdes.PropertyResolverImpl;\nimport com.provectus.kafka.ui.serdes.RecordHeadersImpl;\nimport org.apache.kafka.common.header.internals.RecordHeaders;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\n\nclass Int32SerdeTest {\n\n  private Int32Serde serde;\n\n  @BeforeEach\n  void init() {\n    serde = new Int32Serde();\n    serde.configure(\n        PropertyResolverImpl.empty(),\n        PropertyResolverImpl.empty(),\n        PropertyResolverImpl.empty()\n    );\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void serializeUses4BytesIntRepresentation(Serde.Target type) {\n    var serializer = serde.serializer(\"anyTopic\", type);\n    byte[] bytes = serializer.serialize(\"1234\");\n    assertThat(bytes).isEqualTo(Ints.toByteArray(1234));\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void deserializeUses4BytesIntRepresentation(Serde.Target type) {\n    var deserializer = serde.deserializer(\"anyTopic\", type);\n    var result = deserializer.deserialize(new RecordHeadersImpl(), Ints.toByteArray(1234));\n    assertThat(result.getResult()).isEqualTo(\"1234\");\n    assertThat(result.getType()).isEqualTo(DeserializeResult.Type.JSON);\n  }\n\n}"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/Int64SerdeTest.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.google.common.primitives.Longs;\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport com.provectus.kafka.ui.serdes.PropertyResolverImpl;\nimport com.provectus.kafka.ui.serdes.RecordHeadersImpl;\nimport org.apache.kafka.common.header.internals.RecordHeaders;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\n\n\nclass Int64SerdeTest {\n\n  private Int64Serde serde;\n\n  @BeforeEach\n  void init() {\n    serde = new Int64Serde();\n    serde.configure(\n        PropertyResolverImpl.empty(),\n        PropertyResolverImpl.empty(),\n        PropertyResolverImpl.empty()\n    );\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void serializeUses8BytesLongRepresentation(Serde.Target type) {\n    var serializer = serde.serializer(\"anyTopic\", type);\n    byte[] bytes = serializer.serialize(\"1234\");\n    assertThat(bytes).isEqualTo(Longs.toByteArray(1234));\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void deserializeUses8BytesLongRepresentation(Serde.Target type) {\n    var deserializer = serde.deserializer(\"anyTopic\", type);\n    var result = deserializer.deserialize(new RecordHeadersImpl(), Longs.toByteArray(1234));\n    assertThat(result.getResult()).isEqualTo(\"1234\");\n    assertThat(result.getType()).isEqualTo(DeserializeResult.Type.JSON);\n  }\n\n}"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/ProtobufFileSerdeTest.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport com.google.protobuf.Descriptors;\nimport com.google.protobuf.util.JsonFormat;\nimport com.provectus.kafka.ui.serde.api.PropertyResolver;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport com.provectus.kafka.ui.serdes.builtin.ProtobufFileSerde.Configuration;\nimport com.squareup.wire.schema.ProtoFile;\nimport io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport lombok.SneakyThrows;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.util.ResourceUtils;\n\nclass ProtobufFileSerdeTest {\n\n  private static final String samplePersonMsgJson =\n      \"{ \\\"name\\\": \\\"My Name\\\",\\\"id\\\": 101, \\\"email\\\": \\\"user1@example.com\\\", \\\"phones\\\":[] }\";\n\n  private static final String sampleBookMsgJson = \"{\\\"version\\\": 1, \\\"people\\\": [\"\n      + \"{ \\\"name\\\": \\\"My Name\\\",\\\"id\\\": 102, \\\"email\\\": \\\"addrBook@example.com\\\", \\\"phones\\\":[]}]}\";\n\n  private static final String sampleLangDescriptionMsgJson = \"{ \\\"lang\\\": \\\"EN\\\", \"\n      + \"\\\"descr\\\": \\\"Some description here\\\" }\";\n\n  // Sample message of type `test.Person`\n  private byte[] personMessageBytes;\n  // Sample message of type `test.AddressBook`\n  private byte[] addressBookMessageBytes;\n  private byte[] langDescriptionMessageBytes;\n  private Descriptors.Descriptor personDescriptor;\n  private Descriptors.Descriptor addressBookDescriptor;\n  private Descriptors.Descriptor langDescriptionDescriptor;\n  private Map<Descriptors.Descriptor, Path> descriptorPaths;\n\n  @BeforeEach\n  void setUp() throws Exception {\n    Map<Path, ProtobufSchema> files = ProtobufFileSerde.Configuration.loadSchemas(\n        Optional.empty(),\n        Optional.of(protoFilesDir())\n    );\n\n    Path addressBookSchemaPath = ResourceUtils.getFile(\"classpath:protobuf-serde/address-book.proto\").toPath();\n    var addressBookSchema = files.get(addressBookSchemaPath);\n    var builder = addressBookSchema.newMessageBuilder(\"test.Person\");\n    JsonFormat.parser().merge(samplePersonMsgJson, builder);\n    personMessageBytes = builder.build().toByteArray();\n\n    builder = addressBookSchema.newMessageBuilder(\"test.AddressBook\");\n    JsonFormat.parser().merge(sampleBookMsgJson, builder);\n    addressBookMessageBytes = builder.build().toByteArray();\n    personDescriptor = addressBookSchema.toDescriptor(\"test.Person\");\n    addressBookDescriptor = addressBookSchema.toDescriptor(\"test.AddressBook\");\n\n    Path languageDescriptionPath = ResourceUtils.getFile(\"classpath:protobuf-serde/lang-description.proto\").toPath();\n    var languageDescriptionSchema = files.get(languageDescriptionPath);\n    builder = languageDescriptionSchema.newMessageBuilder(\"test.LanguageDescription\");\n    JsonFormat.parser().merge(sampleLangDescriptionMsgJson, builder);\n    langDescriptionMessageBytes = builder.build().toByteArray();\n    langDescriptionDescriptor = languageDescriptionSchema.toDescriptor(\"test.LanguageDescription\");\n\n    descriptorPaths = Map.of(\n        personDescriptor, addressBookSchemaPath,\n        addressBookDescriptor, addressBookSchemaPath\n    );\n  }\n\n  @Test\n  void loadsAllProtoFiledFromTargetDirectory() throws Exception {\n    var protoDir = ResourceUtils.getFile(\"classpath:protobuf-serde/\").getPath();\n    List<ProtoFile> files = new ProtobufFileSerde.ProtoSchemaLoader(protoDir).load();\n    assertThat(files).hasSize(4);\n    assertThat(files)\n        .map(f -> f.getLocation().getPath())\n        .containsExactlyInAnyOrder(\n            \"language/language.proto\",\n            \"sensor.proto\",\n            \"address-book.proto\",\n            \"lang-description.proto\"\n        );\n  }\n\n  @SneakyThrows\n  private String protoFilesDir() {\n    return ResourceUtils.getFile(\"classpath:protobuf-serde/\").getPath();\n  }\n\n  @Nested\n  class ConfigurationTests {\n\n    @Test\n    void canBeAutoConfiguredReturnsNoProtoPropertiesProvided() {\n      PropertyResolver resolver = mock(PropertyResolver.class);\n      assertThat(Configuration.canBeAutoConfigured(resolver))\n          .isFalse();\n    }\n\n    @Test\n    void canBeAutoConfiguredReturnsTrueIfProtoFilesHasBeenProvided() {\n      PropertyResolver resolver = mock(PropertyResolver.class);\n      when(resolver.getListProperty(\"protobufFiles\", String.class))\n          .thenReturn(Optional.of(List.of(\"file.proto\")));\n      assertThat(Configuration.canBeAutoConfigured(resolver))\n          .isTrue();\n    }\n\n    @Test\n    void canBeAutoConfiguredReturnsTrueIfProtoFilesDirProvided() {\n      PropertyResolver resolver = mock(PropertyResolver.class);\n      when(resolver.getProperty(\"protobufFilesDir\", String.class))\n          .thenReturn(Optional.of(\"/filesDir\"));\n      assertThat(Configuration.canBeAutoConfigured(resolver))\n          .isTrue();\n    }\n\n    @Test\n    void unknownSchemaAsDefaultThrowsException() {\n      PropertyResolver resolver = mock(PropertyResolver.class);\n      when(resolver.getProperty(\"protobufFilesDir\", String.class))\n          .thenReturn(Optional.of(protoFilesDir()));\n\n      when(resolver.getProperty(\"protobufMessageName\", String.class))\n          .thenReturn(Optional.of(\"test.NotExistent\"));\n\n      assertThatThrownBy(() -> Configuration.create(resolver))\n          .isInstanceOf(NullPointerException.class)\n          .hasMessage(\"The given message type not found in protobuf definition: test.NotExistent\");\n    }\n\n    @Test\n    void unknownSchemaAsDefaultForKeyThrowsException() {\n      PropertyResolver resolver = mock(PropertyResolver.class);\n      when(resolver.getProperty(\"protobufFilesDir\", String.class))\n          .thenReturn(Optional.of(protoFilesDir()));\n\n      when(resolver.getProperty(\"protobufMessageNameForKey\", String.class))\n          .thenReturn(Optional.of(\"test.NotExistent\"));\n\n      assertThatThrownBy(() -> Configuration.create(resolver))\n          .isInstanceOf(NullPointerException.class)\n          .hasMessage(\"The given message type not found in protobuf definition: test.NotExistent\");\n    }\n\n    @Test\n    void unknownSchemaAsTopicSchemaThrowsException() {\n      PropertyResolver resolver = mock(PropertyResolver.class);\n      when(resolver.getProperty(\"protobufFilesDir\", String.class))\n          .thenReturn(Optional.of(protoFilesDir()));\n\n      when(resolver.getMapProperty(\"protobufMessageNameByTopic\", String.class, String.class))\n          .thenReturn(Optional.of(Map.of(\"persons\", \"test.NotExistent\")));\n\n      assertThatThrownBy(() -> Configuration.create(resolver))\n          .isInstanceOf(NullPointerException.class)\n          .hasMessage(\"The given message type not found in protobuf definition: test.NotExistent\");\n    }\n\n    @Test\n    void unknownSchemaAsTopicSchemaForKeyThrowsException() {\n      PropertyResolver resolver = mock(PropertyResolver.class);\n      when(resolver.getProperty(\"protobufFilesDir\", String.class))\n          .thenReturn(Optional.of(protoFilesDir()));\n\n      when(resolver.getMapProperty(\"protobufMessageNameForKeyByTopic\", String.class, String.class))\n          .thenReturn(Optional.of(Map.of(\"persons\", \"test.NotExistent\")));\n\n      assertThatThrownBy(() -> Configuration.create(resolver))\n          .isInstanceOf(NullPointerException.class)\n          .hasMessage(\"The given message type not found in protobuf definition: test.NotExistent\");\n    }\n\n    @Test\n    void createConfigureFillsDescriptorMappingsWhenProtoFilesListProvided() throws Exception {\n      PropertyResolver resolver = mock(PropertyResolver.class);\n      when(resolver.getListProperty(\"protobufFiles\", String.class))\n          .thenReturn(Optional.of(\n              List.of(\n                  ResourceUtils.getFile(\"classpath:protobuf-serde/sensor.proto\").getPath(),\n                  ResourceUtils.getFile(\"classpath:protobuf-serde/address-book.proto\").getPath())));\n\n      when(resolver.getProperty(\"protobufMessageName\", String.class))\n          .thenReturn(Optional.of(\"test.Sensor\"));\n\n      when(resolver.getProperty(\"protobufMessageNameForKey\", String.class))\n          .thenReturn(Optional.of(\"test.AddressBook\"));\n\n      when(resolver.getMapProperty(\"protobufMessageNameByTopic\", String.class, String.class))\n          .thenReturn(Optional.of(\n              Map.of(\n                  \"topic1\", \"test.Sensor\",\n                  \"topic2\", \"test.AddressBook\")));\n\n      when(resolver.getMapProperty(\"protobufMessageNameForKeyByTopic\", String.class, String.class))\n          .thenReturn(Optional.of(\n              Map.of(\n                  \"topic1\", \"test.Person\",\n                  \"topic2\", \"test.AnotherPerson\")));\n\n      var configuration = Configuration.create(resolver);\n\n      assertThat(configuration.defaultMessageDescriptor())\n          .matches(d -> d.getFullName().equals(\"test.Sensor\"));\n      assertThat(configuration.defaultKeyMessageDescriptor())\n          .matches(d -> d.getFullName().equals(\"test.AddressBook\"));\n\n      assertThat(configuration.messageDescriptorMap())\n          .containsOnlyKeys(\"topic1\", \"topic2\")\n          .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo(\"test.Sensor\"))\n          .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo(\"test.AddressBook\"));\n\n      assertThat(configuration.keyMessageDescriptorMap())\n          .containsOnlyKeys(\"topic1\", \"topic2\")\n          .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo(\"test.Person\"))\n          .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo(\"test.AnotherPerson\"));\n    }\n\n    @Test\n    void createConfigureFillsDescriptorMappingsWhenProtoFileDirProvided() throws Exception {\n      PropertyResolver resolver = mock(PropertyResolver.class);\n      when(resolver.getProperty(\"protobufFilesDir\", String.class))\n          .thenReturn(Optional.of(protoFilesDir()));\n\n      when(resolver.getProperty(\"protobufMessageName\", String.class))\n          .thenReturn(Optional.of(\"test.Sensor\"));\n\n      when(resolver.getProperty(\"protobufMessageNameForKey\", String.class))\n          .thenReturn(Optional.of(\"test.AddressBook\"));\n\n      when(resolver.getMapProperty(\"protobufMessageNameByTopic\", String.class, String.class))\n          .thenReturn(Optional.of(\n              Map.of(\n                  \"topic1\", \"test.Sensor\",\n                  \"topic2\", \"test.LanguageDescription\")));\n\n      when(resolver.getMapProperty(\"protobufMessageNameForKeyByTopic\", String.class, String.class))\n          .thenReturn(Optional.of(\n              Map.of(\n                  \"topic1\", \"test.Person\",\n                  \"topic2\", \"test.AnotherPerson\")));\n\n      var configuration = Configuration.create(resolver);\n\n      assertThat(configuration.defaultMessageDescriptor())\n          .matches(d -> d.getFullName().equals(\"test.Sensor\"));\n      assertThat(configuration.defaultKeyMessageDescriptor())\n          .matches(d -> d.getFullName().equals(\"test.AddressBook\"));\n\n      assertThat(configuration.messageDescriptorMap())\n          .containsOnlyKeys(\"topic1\", \"topic2\")\n          .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo(\"test.Sensor\"))\n          .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo(\"test.LanguageDescription\"));\n\n      assertThat(configuration.keyMessageDescriptorMap())\n          .containsOnlyKeys(\"topic1\", \"topic2\")\n          .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo(\"test.Person\"))\n          .anySatisfy((topic, descr) -> assertThat(descr.getFullName()).isEqualTo(\"test.AnotherPerson\"));\n    }\n  }\n\n  @Test\n  void deserializeUsesTopicsMappingToFindMsgDescriptor() {\n    var messageNameMap = Map.of(\n        \"persons\", personDescriptor,\n        \"books\", addressBookDescriptor,\n        \"langs\", langDescriptionDescriptor\n    );\n    var keyMessageNameMap = Map.of(\n        \"books\", addressBookDescriptor);\n    var serde = new ProtobufFileSerde();\n    serde.configure(\n        new Configuration(\n            null,\n            null,\n            descriptorPaths,\n            messageNameMap,\n            keyMessageNameMap\n        )\n    );\n\n    var deserializedPerson = serde.deserializer(\"persons\", Serde.Target.VALUE)\n        .deserialize(null, personMessageBytes);\n    assertJsonEquals(samplePersonMsgJson, deserializedPerson.getResult());\n\n    var deserializedBook = serde.deserializer(\"books\", Serde.Target.KEY)\n        .deserialize(null, addressBookMessageBytes);\n    assertJsonEquals(sampleBookMsgJson, deserializedBook.getResult());\n\n    var deserializedSensor = serde.deserializer(\"langs\", Serde.Target.VALUE)\n        .deserialize(null, langDescriptionMessageBytes);\n    assertJsonEquals(sampleLangDescriptionMsgJson, deserializedSensor.getResult());\n  }\n\n  @Test\n  void deserializeUsesDefaultDescriptorIfTopicMappingNotFound() {\n    var serde = new ProtobufFileSerde();\n    serde.configure(\n        new Configuration(\n            personDescriptor,\n            addressBookDescriptor,\n            descriptorPaths,\n            Map.of(),\n            Map.of()\n        )\n    );\n\n    var deserializedPerson = serde.deserializer(\"persons\", Serde.Target.VALUE)\n        .deserialize(null, personMessageBytes);\n    assertJsonEquals(samplePersonMsgJson, deserializedPerson.getResult());\n\n    var deserializedBook = serde.deserializer(\"books\", Serde.Target.KEY)\n        .deserialize(null, addressBookMessageBytes);\n    assertJsonEquals(sampleBookMsgJson, deserializedBook.getResult());\n  }\n\n  @Test\n  void serializeUsesTopicsMappingToFindMsgDescriptor() {\n    var messageNameMap = Map.of(\n        \"persons\", personDescriptor,\n        \"books\", addressBookDescriptor,\n        \"langs\", langDescriptionDescriptor\n    );\n    var keyMessageNameMap = Map.of(\n        \"books\", addressBookDescriptor);\n\n    var serde = new ProtobufFileSerde();\n    serde.configure(\n        new Configuration(\n            null,\n            null,\n            descriptorPaths,\n            messageNameMap,\n            keyMessageNameMap\n        )\n    );\n\n    var personBytes = serde.serializer(\"langs\", Serde.Target.VALUE)\n        .serialize(sampleLangDescriptionMsgJson);\n    assertThat(personBytes).isEqualTo(langDescriptionMessageBytes);\n\n    var booksBytes = serde.serializer(\"books\", Serde.Target.KEY)\n        .serialize(sampleBookMsgJson);\n    assertThat(booksBytes).isEqualTo(addressBookMessageBytes);\n  }\n\n  @Test\n  void serializeUsesDefaultDescriptorIfTopicMappingNotFound() {\n    var serde = new ProtobufFileSerde();\n    serde.configure(\n        new Configuration(\n            personDescriptor,\n            addressBookDescriptor,\n            descriptorPaths,\n            Map.of(),\n            Map.of()\n        )\n    );\n\n    var personBytes = serde.serializer(\"persons\", Serde.Target.VALUE)\n        .serialize(samplePersonMsgJson);\n    assertThat(personBytes).isEqualTo(personMessageBytes);\n\n    var booksBytes = serde.serializer(\"books\", Serde.Target.KEY)\n        .serialize(sampleBookMsgJson);\n    assertThat(booksBytes).isEqualTo(addressBookMessageBytes);\n  }\n\n  @SneakyThrows\n  private void assertJsonEquals(String expectedJson, String actualJson) {\n    var mapper = new JsonMapper();\n    assertThat(mapper.readTree(actualJson)).isEqualTo(mapper.readTree(expectedJson));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/ProtobufRawSerdeTest.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nimport com.google.protobuf.DescriptorProtos;\nimport com.google.protobuf.Descriptors;\nimport com.google.protobuf.DynamicMessage;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema;\nimport lombok.SneakyThrows;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nclass ProtobufRawSerdeTest {\n\n  private static final String DUMMY_TOPIC = \"dummy-topic\";\n\n  private ProtobufRawSerde serde;\n\n  @BeforeEach\n  void init() {\n    serde = new ProtobufRawSerde();\n  }\n\n  @SneakyThrows\n  ProtobufSchema getSampleSchema() {\n    return new ProtobufSchema(\n        \"\"\"\n          syntax = \"proto3\";\n          message Message1 {\n            int32 my_field = 1;\n          }\n        \"\"\"\n    );\n  }\n\n  @SneakyThrows\n  private byte[] getProtobufMessage() {\n    DynamicMessage.Builder builder = DynamicMessage.newBuilder(getSampleSchema().toDescriptor(\"Message1\"));\n    builder.setField(builder.getDescriptorForType().findFieldByName(\"my_field\"), 5);\n    return builder.build().toByteArray();\n  }\n\n  @Test\n  void deserializeSimpleMessage() {\n    var deserialized = serde.deserializer(DUMMY_TOPIC, Serde.Target.VALUE)\n        .deserialize(null, getProtobufMessage());\n    assertThat(deserialized.getResult()).isEqualTo(\"1: 5\\n\");\n  }\n\n  @Test\n  void deserializeEmptyMessage() {\n    var deserialized = serde.deserializer(DUMMY_TOPIC, Serde.Target.VALUE)\n        .deserialize(null, new byte[0]);\n    assertThat(deserialized.getResult()).isEqualTo(\"\");\n  }\n\n  @Test\n  void deserializeInvalidMessage() {\n    var deserializer = serde.deserializer(DUMMY_TOPIC, Serde.Target.VALUE);\n    assertThatThrownBy(() -> deserializer.deserialize(null, new byte[] { 1, 2, 3 }))\n        .isInstanceOf(ValidationException.class)\n        .hasMessageContaining(\"Protocol message contained an invalid tag\");\n  }\n  \n  @Test\n  void deserializeNullMessage() {\n    var deserializer = serde.deserializer(DUMMY_TOPIC, Serde.Target.VALUE);\n    assertThatThrownBy(() -> deserializer.deserialize(null, null))\n        .isInstanceOf(ValidationException.class)\n        .hasMessageContaining(\"Cannot read the array length\");\n  }\n\n  ProtobufSchema getSampleNestedSchema() {\n    return new ProtobufSchema(\n      \"\"\"\n        syntax = \"proto3\";\n        message Message2 {\n          int32 my_nested_field = 1;\n        }\n        message Message1 {\n          int32 my_field = 1;\n          Message2 my_nested_message = 2;\n        }\n      \"\"\"\n    );\n  }\n\n  @SneakyThrows\n  private byte[] getComplexProtobufMessage() {\n    DynamicMessage.Builder builder = DynamicMessage.newBuilder(getSampleNestedSchema().toDescriptor(\"Message1\"));\n    builder.setField(builder.getDescriptorForType().findFieldByName(\"my_field\"), 5);\n    DynamicMessage.Builder nestedBuilder = DynamicMessage.newBuilder(getSampleNestedSchema().toDescriptor(\"Message2\"));\n    nestedBuilder.setField(nestedBuilder.getDescriptorForType().findFieldByName(\"my_nested_field\"), 10);\n    builder.setField(builder.getDescriptorForType().findFieldByName(\"my_nested_message\"), nestedBuilder.build());\n\n    return builder.build().toByteArray();\n  }\n\n  @Test\n  void deserializeNestedMessage() {\n    var deserialized = serde.deserializer(DUMMY_TOPIC, Serde.Target.VALUE)\n        .deserialize(null, getComplexProtobufMessage());\n    assertThat(deserialized.getResult()).isEqualTo(\"1: 5\\n2: {\\n  1: 10\\n}\\n\");\n  }\n}"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/UInt32SerdeTest.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nimport com.google.common.primitives.Ints;\nimport com.google.common.primitives.UnsignedInteger;\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport com.provectus.kafka.ui.serdes.PropertyResolverImpl;\nimport com.provectus.kafka.ui.serdes.RecordHeadersImpl;\nimport org.apache.kafka.common.header.internals.RecordHeaders;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\n\nclass UInt32SerdeTest {\n\n  private UInt32Serde serde;\n\n  @BeforeEach\n  void init() {\n    serde = new UInt32Serde();\n    serde.configure(\n        PropertyResolverImpl.empty(),\n        PropertyResolverImpl.empty(),\n        PropertyResolverImpl.empty()\n    );\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void serializeUses4BytesUInt32Representation(Serde.Target type) {\n    var serializer = serde.serializer(\"anyTopic\", type);\n    String uint32String = UnsignedInteger.MAX_VALUE.toString();\n    byte[] bytes = serializer.serialize(uint32String);\n    assertThat(bytes).isEqualTo(Ints.toByteArray(UnsignedInteger.MAX_VALUE.intValue()));\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void serializeThrowsNfeIfNegativeValuePassed(Serde.Target type) {\n    var serializer = serde.serializer(\"anyTopic\", type);\n    String negativeIntString = \"-100\";\n    assertThatThrownBy(() -> serializer.serialize(negativeIntString))\n        .isInstanceOf(NumberFormatException.class);\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void deserializeUses4BytesUInt32Representation(Serde.Target type) {\n    var deserializer = serde.deserializer(\"anyTopic\", type);\n    byte[] uint32Bytes = Ints.toByteArray(UnsignedInteger.MAX_VALUE.intValue());\n    var result = deserializer.deserialize(new RecordHeadersImpl(), uint32Bytes);\n    assertThat(result.getResult()).isEqualTo(UnsignedInteger.MAX_VALUE.toString());\n    assertThat(result.getType()).isEqualTo(DeserializeResult.Type.JSON);\n  }\n\n}"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/UInt64SerdeTest.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nimport com.google.common.primitives.Longs;\nimport com.google.common.primitives.UnsignedLong;\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport com.provectus.kafka.ui.serdes.PropertyResolverImpl;\nimport com.provectus.kafka.ui.serdes.RecordHeadersImpl;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\n\nclass UInt64SerdeTest {\n\n  private UInt64Serde serde;\n\n  @BeforeEach\n  void init() {\n    serde = new UInt64Serde();\n    serde.configure(\n        PropertyResolverImpl.empty(),\n        PropertyResolverImpl.empty(),\n        PropertyResolverImpl.empty()\n    );\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void serializeUses8BytesUInt64Representation(Serde.Target type) {\n    var serializer = serde.serializer(\"anyTopic\", type);\n    String uint64String = UnsignedLong.MAX_VALUE.toString();\n    byte[] bytes = serializer.serialize(uint64String);\n    assertThat(bytes).isEqualTo(Longs.toByteArray(UnsignedLong.MAX_VALUE.longValue()));\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void serializeThrowsNfeIfNegativeValuePassed(Serde.Target type) {\n    var serializer = serde.serializer(\"anyTopic\", type);\n    String negativeIntString = \"-100\";\n    assertThatThrownBy(() -> serializer.serialize(negativeIntString))\n        .isInstanceOf(NumberFormatException.class);\n  }\n\n  @ParameterizedTest\n  @EnumSource\n  void deserializeUses8BytesUIn64tRepresentation(Serde.Target type) {\n    var deserializer = serde.deserializer(\"anyTopic\", type);\n    byte[] uint64Bytes = Longs.toByteArray(UnsignedLong.MAX_VALUE.longValue());\n    var result = deserializer.deserialize(new RecordHeadersImpl(), uint64Bytes);\n    assertThat(result.getResult()).isEqualTo(UnsignedLong.MAX_VALUE.toString());\n    assertThat(result.getType()).isEqualTo(DeserializeResult.Type.JSON);\n  }\n\n}"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/UuidBinarySerdeTest.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport com.provectus.kafka.ui.serdes.PropertyResolverImpl;\nimport com.provectus.kafka.ui.serdes.RecordHeadersImpl;\nimport java.nio.ByteBuffer;\nimport java.util.UUID;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.EnumSource;\nimport org.springframework.mock.env.MockEnvironment;\n\nclass UuidBinarySerdeTest {\n\n  @Nested\n  class MsbFirst {\n\n    private UuidBinarySerde serde;\n\n    @BeforeEach\n    void init() {\n      serde = new UuidBinarySerde();\n      serde.configure(\n          PropertyResolverImpl.empty(),\n          PropertyResolverImpl.empty(),\n          PropertyResolverImpl.empty()\n      );\n    }\n\n    @ParameterizedTest\n    @EnumSource\n    void serializerUses16bytesUuidBinaryRepresentation(Serde.Target type) {\n      var serializer = serde.serializer(\"anyTopic\", type);\n      var uuid = UUID.randomUUID();\n      byte[] bytes = serializer.serialize(uuid.toString());\n      var bb = ByteBuffer.wrap(bytes);\n      assertThat(bb.getLong()).isEqualTo(uuid.getMostSignificantBits());\n      assertThat(bb.getLong()).isEqualTo(uuid.getLeastSignificantBits());\n    }\n\n    @ParameterizedTest\n    @EnumSource\n    void deserializerUses16bytesUuidBinaryRepresentation(Serde.Target type) {\n      var uuid = UUID.randomUUID();\n      var bb = ByteBuffer.allocate(16);\n      bb.putLong(uuid.getMostSignificantBits());\n      bb.putLong(uuid.getLeastSignificantBits());\n\n      var result = serde.deserializer(\"anyTopic\", type).deserialize(new RecordHeadersImpl(), bb.array());\n      assertThat(result.getType()).isEqualTo(DeserializeResult.Type.STRING);\n      assertThat(result.getAdditionalProperties()).isEmpty();\n      assertThat(result.getResult()).isEqualTo(uuid.toString());\n    }\n  }\n\n  @Nested\n  class MsbLast {\n\n    private UuidBinarySerde serde;\n\n    @BeforeEach\n    void init() {\n      serde = new UuidBinarySerde();\n      serde.configure(\n          new PropertyResolverImpl(new MockEnvironment().withProperty(\"mostSignificantBitsFirst\", \"false\")),\n          PropertyResolverImpl.empty(),\n          PropertyResolverImpl.empty()\n      );\n    }\n\n    @ParameterizedTest\n    @EnumSource\n    void serializerUses16bytesUuidBinaryRepresentation(Serde.Target type) {\n      var serializer = serde.serializer(\"anyTopic\", type);\n      var uuid = UUID.randomUUID();\n      byte[] bytes = serializer.serialize(uuid.toString());\n      var bb = ByteBuffer.wrap(bytes);\n      assertThat(bb.getLong()).isEqualTo(uuid.getLeastSignificantBits());\n      assertThat(bb.getLong()).isEqualTo(uuid.getMostSignificantBits());\n    }\n\n    @ParameterizedTest\n    @EnumSource\n    void deserializerUses16bytesUuidBinaryRepresentation(Serde.Target type) {\n      var uuid = UUID.randomUUID();\n      var bb = ByteBuffer.allocate(16);\n      bb.putLong(uuid.getLeastSignificantBits());\n      bb.putLong(uuid.getMostSignificantBits());\n\n      var result = serde.deserializer(\"anyTopic\", type).deserialize(new RecordHeadersImpl(), bb.array());\n      assertThat(result.getType()).isEqualTo(DeserializeResult.Type.STRING);\n      assertThat(result.getAdditionalProperties()).isEmpty();\n      assertThat(result.getResult()).isEqualTo(uuid.toString());\n    }\n  }\n\n}"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/serdes/builtin/sr/SchemaRegistrySerdeTest.java",
    "content": "package com.provectus.kafka.ui.serdes.builtin.sr;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport com.provectus.kafka.ui.serde.api.DeserializeResult;\nimport com.provectus.kafka.ui.serde.api.SchemaDescription;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport com.provectus.kafka.ui.util.jsonschema.JsonAvroConversion;\nimport io.confluent.kafka.schemaregistry.avro.AvroSchema;\nimport io.confluent.kafka.schemaregistry.client.MockSchemaRegistryClient;\nimport io.confluent.kafka.schemaregistry.client.rest.exceptions.RestClientException;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.SneakyThrows;\nimport net.bytebuddy.utility.RandomString;\nimport org.apache.avro.generic.GenericDatumWriter;\nimport org.apache.avro.io.Encoder;\nimport org.apache.avro.io.EncoderFactory;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.CsvSource;\n\nclass SchemaRegistrySerdeTest {\n\n  private final MockSchemaRegistryClient registryClient = new MockSchemaRegistryClient();\n\n  private SchemaRegistrySerde serde;\n\n  @BeforeEach\n  void init() {\n    serde = new SchemaRegistrySerde();\n    serde.configure(List.of(\"wontbeused\"), registryClient, \"%s-key\", \"%s-value\", true);\n  }\n\n  @ParameterizedTest\n  @CsvSource({\n      \"test_topic, test_topic-key, KEY\",\n      \"test_topic, test_topic-value, VALUE\"\n  })\n  @SneakyThrows\n  void returnsSchemaDescriptionIfSchemaRegisteredInSR(String topic, String subject, Serde.Target target) {\n    int schemaId = registryClient.register(subject, new AvroSchema(\"{ \\\"type\\\": \\\"int\\\" }\"));\n    int registeredVersion = registryClient.getLatestSchemaMetadata(subject).getVersion();\n\n    var schemaOptional = serde.getSchema(topic, target);\n    assertThat(schemaOptional).isPresent();\n\n    SchemaDescription schemaDescription = schemaOptional.get();\n    assertThat(schemaDescription.getSchema())\n        .contains(\n            \"{\\\"$id\\\":\\\"int\\\",\\\"$schema\\\":\\\"https://json-schema.org/draft/2020-12/schema\\\",\\\"type\\\":\\\"integer\\\"}\");\n    assertThat(schemaDescription.getAdditionalProperties())\n        .containsOnlyKeys(\"subject\", \"schemaId\", \"latestVersion\", \"type\")\n        .containsEntry(\"subject\", subject)\n        .containsEntry(\"schemaId\", schemaId)\n        .containsEntry(\"latestVersion\", registeredVersion)\n        .containsEntry(\"type\", \"AVRO\");\n  }\n\n  @Test\n  void returnsEmptyDescriptorIfSchemaNotRegisteredInSR() {\n    String topic = \"test\";\n    assertThat(serde.getSchema(topic, Serde.Target.KEY)).isEmpty();\n    assertThat(serde.getSchema(topic, Serde.Target.VALUE)).isEmpty();\n  }\n\n  @Test\n  void serializeTreatsInputAsJsonAvroSchemaPayload() throws RestClientException, IOException {\n    AvroSchema schema = new AvroSchema(\n        \"{\"\n            + \"  \\\"type\\\": \\\"record\\\",\"\n            + \"  \\\"name\\\": \\\"TestAvroRecord1\\\",\"\n            + \"  \\\"fields\\\": [\"\n            + \"    {\"\n            + \"      \\\"name\\\": \\\"field1\\\",\"\n            + \"      \\\"type\\\": \\\"string\\\"\"\n            + \"    },\"\n            + \"    {\"\n            + \"      \\\"name\\\": \\\"field2\\\",\"\n            + \"      \\\"type\\\": \\\"int\\\"\"\n            + \"    }\"\n            + \"  ]\"\n            + \"}\"\n    );\n    String jsonValue = \"{ \\\"field1\\\":\\\"testStr\\\", \\\"field2\\\": 123 }\";\n    String topic = \"test\";\n\n    int schemaId = registryClient.register(topic + \"-value\", schema);\n    byte[] serialized = serde.serializer(topic, Serde.Target.VALUE).serialize(jsonValue);\n    byte[] expected = toBytesWithMagicByteAndSchemaId(schemaId, jsonValue, schema);\n    assertThat(serialized).isEqualTo(expected);\n  }\n\n  @Test\n  void deserializeReturnsJsonAvroMsgJsonRepresentation() throws RestClientException, IOException {\n    AvroSchema schema = new AvroSchema(\n        \"{\"\n            + \"  \\\"type\\\": \\\"record\\\",\"\n            + \"  \\\"name\\\": \\\"TestAvroRecord1\\\",\"\n            + \"  \\\"fields\\\": [\"\n            + \"    {\"\n            + \"      \\\"name\\\": \\\"field1\\\",\"\n            + \"      \\\"type\\\": \\\"string\\\"\"\n            + \"    },\"\n            + \"    {\"\n            + \"      \\\"name\\\": \\\"field2\\\",\"\n            + \"      \\\"type\\\": \\\"int\\\"\"\n            + \"    }\"\n            + \"  ]\"\n            + \"}\"\n    );\n    String jsonValue = \"{ \\\"field1\\\":\\\"testStr\\\", \\\"field2\\\": 123 }\";\n\n    String topic = \"test\";\n    int schemaId = registryClient.register(topic + \"-value\", schema);\n\n    byte[] data = toBytesWithMagicByteAndSchemaId(schemaId, jsonValue, schema);\n    var result = serde.deserializer(topic, Serde.Target.VALUE).deserialize(null, data);\n\n    assertJsonsEqual(jsonValue, result.getResult());\n    assertThat(result.getType()).isEqualTo(DeserializeResult.Type.JSON);\n    assertThat(result.getAdditionalProperties())\n        .contains(Map.entry(\"type\", \"AVRO\"))\n        .contains(Map.entry(\"schemaId\", schemaId));\n  }\n\n  @Nested\n  class SerdeWithDisabledSubjectExistenceCheck {\n\n    @BeforeEach\n    void init() {\n      serde.configure(List.of(\"wontbeused\"), registryClient, \"%s-key\", \"%s-value\", false);\n    }\n\n    @Test\n    void canDeserializeAlwaysReturnsTrue() {\n      String topic = RandomString.make(10);\n      assertThat(serde.canDeserialize(topic, Serde.Target.KEY)).isTrue();\n      assertThat(serde.canDeserialize(topic, Serde.Target.VALUE)).isTrue();\n    }\n  }\n\n  @Nested\n  class SerdeWithEnabledSubjectExistenceCheck {\n\n    @BeforeEach\n    void init() {\n      serde.configure(List.of(\"wontbeused\"), registryClient, \"%s-key\", \"%s-value\", true);\n    }\n\n    @Test\n    void canDeserializeReturnsTrueIfSubjectExists() throws Exception {\n      String topic = RandomString.make(10);\n      registryClient.register(topic + \"-key\", new AvroSchema(\"\\\"int\\\"\"));\n      registryClient.register(topic + \"-value\", new AvroSchema(\"\\\"int\\\"\"));\n\n      assertThat(serde.canDeserialize(topic, Serde.Target.KEY)).isTrue();\n      assertThat(serde.canDeserialize(topic, Serde.Target.VALUE)).isTrue();\n    }\n\n    @Test\n    void canDeserializeReturnsFalseIfSubjectDoesNotExist() {\n      String topic = RandomString.make(10);\n      assertThat(serde.canDeserialize(topic, Serde.Target.KEY)).isFalse();\n      assertThat(serde.canDeserialize(topic, Serde.Target.VALUE)).isFalse();\n    }\n  }\n\n  @Test\n  void canDeserializeAndCanSerializeReturnsTrueIfSubjectExists() throws Exception {\n    String topic = RandomString.make(10);\n    registryClient.register(topic + \"-key\", new AvroSchema(\"\\\"int\\\"\"));\n    registryClient.register(topic + \"-value\", new AvroSchema(\"\\\"int\\\"\"));\n\n    assertThat(serde.canSerialize(topic, Serde.Target.KEY)).isTrue();\n    assertThat(serde.canSerialize(topic, Serde.Target.VALUE)).isTrue();\n  }\n\n  @Test\n  void canSerializeReturnsFalseIfSubjectDoesNotExist() {\n    String topic = RandomString.make(10);\n    assertThat(serde.canSerialize(topic, Serde.Target.KEY)).isFalse();\n    assertThat(serde.canSerialize(topic, Serde.Target.VALUE)).isFalse();\n  }\n\n  @SneakyThrows\n  private void assertJsonsEqual(String expected, String actual) {\n    var mapper = new JsonMapper();\n    assertThat(mapper.readTree(actual)).isEqualTo(mapper.readTree(expected));\n  }\n\n  private byte[] toBytesWithMagicByteAndSchemaId(int schemaId, String json, AvroSchema schema) {\n    return toBytesWithMagicByteAndSchemaId(schemaId, jsonToAvro(json, schema));\n  }\n\n  private byte[] toBytesWithMagicByteAndSchemaId(int schemaId, byte[] body) {\n    return ByteBuffer.allocate(1 + 4 + body.length)\n        .put((byte) 0)\n        .putInt(schemaId)\n        .put(body)\n        .array();\n  }\n\n  @SneakyThrows\n  private byte[] jsonToAvro(String json, AvroSchema schema) {\n    GenericDatumWriter<Object> writer = new GenericDatumWriter<>(schema.rawSchema());\n    ByteArrayOutputStream output = new ByteArrayOutputStream();\n    Encoder encoder = EncoderFactory.get().binaryEncoder(output, null);\n    writer.write(JsonAvroConversion.convertJsonToAvro(json, schema.rawSchema()), encoder);\n    encoder.flush();\n    return output.toByteArray();\n  }\n\n  @Test\n  void avroFieldsRepresentationIsConsistentForSerializationAndDeserialization() throws Exception {\n    AvroSchema schema = new AvroSchema(\n        \"\"\"\n             {\n               \"type\": \"record\",\n               \"name\": \"TestAvroRecord\",\n               \"fields\": [\n                 {\n                   \"name\": \"f_int\",\n                   \"type\": \"int\"\n                 },\n                 {\n                   \"name\": \"f_long\",\n                   \"type\": \"long\"\n                 },\n                 {\n                   \"name\": \"f_string\",\n                   \"type\": \"string\"\n                 },\n                 {\n                   \"name\": \"f_boolean\",\n                   \"type\": \"boolean\"\n                 },\n                 {\n                   \"name\": \"f_float\",\n                   \"type\": \"float\"\n                 },\n                 {\n                   \"name\": \"f_double\",\n                   \"type\": \"double\"\n                 },\n                 {\n                   \"name\": \"f_enum\",\n                   \"type\" : {\n                    \"type\": \"enum\",\n                    \"name\": \"Suit\",\n                    \"symbols\" : [\"SPADES\", \"HEARTS\", \"DIAMONDS\", \"CLUBS\"]\n                   }\n                 },\n                 {\n                  \"name\": \"f_map\",\n                  \"type\": {\n                     \"type\": \"map\",\n                     \"values\" : \"string\",\n                     \"default\": {}\n                   }\n                 },\n                 {\n                  \"name\": \"f_union\",\n                  \"type\": [\"null\", \"string\", \"int\" ]\n                 },\n                 {\n                  \"name\": \"f_optional_to_test_not_filled_case\",\n                  \"type\": [ \"null\", \"string\"]\n                 },\n                 {\n                     \"name\" : \"f_fixed\",\n                     \"type\" : { \"type\" : \"fixed\" ,\"size\" : 8, \"name\": \"long_encoded\" }\n                   },\n                   {\n                     \"name\" : \"f_bytes\",\n                     \"type\": \"bytes\"\n                   }\n               ]\n            }\"\"\"\n    );\n\n    String jsonPayload = \"\"\"\n        {\n          \"f_int\": 123,\n          \"f_long\": 4294967294,\n          \"f_string\": \"string here\",\n          \"f_boolean\": true,\n          \"f_float\": 123.1,\n          \"f_double\": 123456.123456,\n          \"f_enum\": \"SPADES\",\n          \"f_map\": { \"k1\": \"string value\" },\n          \"f_union\": { \"int\": 123 },\n          \"f_fixed\": \"\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\u0004Ò\",\n          \"f_bytes\": \"\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\t)\"\n        }\n        \"\"\";\n\n    registryClient.register(\"test-value\", schema);\n    assertSerdeCycle(\"test\", jsonPayload);\n  }\n\n  @Test\n  void avroLogicalTypesRepresentationIsConsistentForSerializationAndDeserialization() throws Exception {\n    AvroSchema schema = new AvroSchema(\n        \"\"\"\n             {\n               \"type\": \"record\",\n               \"name\": \"TestAvroRecord\",\n               \"fields\": [\n                 {\n                   \"name\": \"lt_date\",\n                   \"type\": { \"type\": \"int\", \"logicalType\": \"date\" }\n                 },\n                 {\n                   \"name\": \"lt_uuid\",\n                   \"type\": { \"type\": \"string\", \"logicalType\": \"uuid\" }\n                 },\n                 {\n                   \"name\": \"lt_decimal\",\n                   \"type\": { \"type\": \"bytes\", \"logicalType\": \"decimal\", \"precision\": 22, \"scale\":10 }\n                 },\n                 {\n                   \"name\": \"lt_time_millis\",\n                   \"type\": { \"type\": \"int\", \"logicalType\": \"time-millis\"}\n                 },\n                 {\n                   \"name\": \"lt_time_micros\",\n                   \"type\": { \"type\": \"long\", \"logicalType\": \"time-micros\"}\n                 },\n                 {\n                   \"name\": \"lt_timestamp_millis\",\n                   \"type\": { \"type\": \"long\", \"logicalType\": \"timestamp-millis\" }\n                 },\n                 {\n                   \"name\": \"lt_timestamp_micros\",\n                   \"type\": { \"type\": \"long\", \"logicalType\": \"timestamp-micros\" }\n                 },\n                 {\n                   \"name\": \"lt_local_timestamp_millis\",\n                   \"type\": { \"type\": \"long\", \"logicalType\": \"local-timestamp-millis\" }\n                 },\n                 {\n                   \"name\": \"lt_local_timestamp_micros\",\n                   \"type\": { \"type\": \"long\", \"logicalType\": \"local-timestamp-micros\" }\n                 }\n               ]\n            }\"\"\"\n    );\n\n    String jsonPayload = \"\"\"\n        {\n          \"lt_date\":\"1991-08-14\",\n          \"lt_decimal\": 2.1617413862327545E11,\n          \"lt_time_millis\": \"10:15:30.001\",\n          \"lt_time_micros\": \"10:15:30.123456\",\n          \"lt_uuid\": \"a37b75ca-097c-5d46-6119-f0637922e908\",\n          \"lt_timestamp_millis\": \"2007-12-03T10:15:30.123Z\",\n          \"lt_timestamp_micros\": \"2007-12-03T10:15:30.123456Z\",\n          \"lt_local_timestamp_millis\": \"2017-12-03T10:15:30.123\",\n          \"lt_local_timestamp_micros\": \"2017-12-03T10:15:30.123456\"\n        }\n        \"\"\";\n\n    registryClient.register(\"test-value\", schema);\n    assertSerdeCycle(\"test\", jsonPayload);\n  }\n\n  // 1. serialize input json to binary\n  // 2. deserialize from binary\n  // 3. check that deserialized version equal to input\n  void assertSerdeCycle(String topic, String jsonInput) {\n    byte[] serializedBytes = serde.serializer(topic, Serde.Target.VALUE).serialize(jsonInput);\n    var deserializedJson = serde.deserializer(topic, Serde.Target.VALUE)\n        .deserialize(null, serializedBytes)\n        .getResult();\n    assertJsonsEqual(jsonInput, deserializedJson);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/BrokerServiceTest.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport com.provectus.kafka.ui.AbstractIntegrationTest;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport reactor.test.StepVerifier;\n\nclass BrokerServiceTest extends AbstractIntegrationTest {\n\n  @Autowired\n  private BrokerService brokerService;\n\n  @Autowired\n  private ClustersStorage clustersStorage;\n\n  @Test\n  void getBrokersReturnsFilledBrokerDto() {\n    var localCluster = clustersStorage.getClusterByName(LOCAL).get();\n    StepVerifier.create(brokerService.getBrokers(localCluster))\n        .expectNextMatches(b -> b.getId().equals(1)\n            && b.getHost().equals(kafka.getHost())\n            && b.getPort().equals(kafka.getFirstMappedPort()))\n        .verifyComplete();\n  }\n\n}"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ConfigTest.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.provectus.kafka.ui.AbstractIntegrationTest;\nimport com.provectus.kafka.ui.model.BrokerConfigDTO;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.ServerStatusDTO;\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.core.ParameterizedTypeReference;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.testcontainers.shaded.org.awaitility.Awaitility;\n\npublic class ConfigTest extends AbstractIntegrationTest {\n\n  @Autowired\n  private WebTestClient webTestClient;\n\n  @BeforeEach\n  void waitUntilStatsInitialized() {\n    Awaitility.await()\n        .atMost(Duration.ofSeconds(10))\n        .pollInSameThread()\n        .until(() -> {\n          var stats = applicationContext.getBean(StatisticsCache.class)\n              .get(KafkaCluster.builder().name(LOCAL).build());\n          return stats.getStatus() == ServerStatusDTO.ONLINE;\n        });\n  }\n\n  @Test\n  public void testAlterConfig() {\n    String name = \"background.threads\";\n\n    Optional<BrokerConfigDTO> bc = getConfig(name);\n    assertThat(bc.isPresent()).isTrue();\n    assertThat(bc.get().getValue()).isEqualTo(\"10\");\n\n    final String newValue = \"5\";\n\n    webTestClient.put()\n        .uri(\"/api/clusters/{clusterName}/brokers/{id}/configs/{name}\", LOCAL, 1, name)\n        .bodyValue(Map.of(\n            \"name\", name,\n            \"value\", newValue\n            )\n        )\n        .exchange()\n        .expectStatus().isOk();\n\n    Awaitility.await()\n        .atMost(Duration.ofSeconds(10))\n        .pollInSameThread()\n        .untilAsserted(() -> {\n          Optional<BrokerConfigDTO> bcc = getConfig(name);\n          assertThat(bcc.isPresent()).isTrue();\n          assertThat(bcc.get().getValue()).isEqualTo(newValue);\n        });\n  }\n\n  @Test\n  public void testAlterReadonlyConfig() {\n    String name = \"log.dirs\";\n\n    webTestClient.put()\n        .uri(\"/api/clusters/{clusterName}/brokers/{id}/configs/{name}\", LOCAL, 1, name)\n        .bodyValue(Map.of(\n            \"name\", name,\n            \"value\", \"/var/lib/kafka2\"\n            )\n        )\n        .exchange()\n        .expectStatus().isBadRequest();\n  }\n\n  private Optional<BrokerConfigDTO> getConfig(String name) {\n    List<BrokerConfigDTO> configs = webTestClient.get()\n        .uri(\"/api/clusters/{clusterName}/brokers/{id}/configs\", LOCAL, 1)\n        .exchange()\n        .expectStatus().isOk()\n        .expectBody(new ParameterizedTypeReference<List<BrokerConfigDTO>>() {\n        })\n        .returnResult()\n        .getResponseBody();\n\n    return configs.stream()\n        .filter(c -> c.getName().equals(name))\n        .findAny();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/KafkaConfigSanitizerTest.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\n\nclass KafkaConfigSanitizerTest {\n\n  @Test\n  void doNothingIfEnabledPropertySetToFalse() {\n    final var sanitizer = new KafkaConfigSanitizer(false, List.of());\n    assertThat(sanitizer.sanitize(\"password\", \"secret\")).isEqualTo(\"secret\");\n    assertThat(sanitizer.sanitize(\"sasl.jaas.config\", \"secret\")).isEqualTo(\"secret\");\n    assertThat(sanitizer.sanitize(\"database.password\", \"secret\")).isEqualTo(\"secret\");\n  }\n\n  @Test\n  void obfuscateCredentials() {\n    final var sanitizer = new KafkaConfigSanitizer(true, List.of());\n    assertThat(sanitizer.sanitize(\"sasl.jaas.config\", \"secret\")).isEqualTo(\"******\");\n    assertThat(sanitizer.sanitize(\"consumer.sasl.jaas.config\", \"secret\")).isEqualTo(\"******\");\n    assertThat(sanitizer.sanitize(\"producer.sasl.jaas.config\", \"secret\")).isEqualTo(\"******\");\n    assertThat(sanitizer.sanitize(\"main.consumer.sasl.jaas.config\", \"secret\")).isEqualTo(\"******\");\n    assertThat(sanitizer.sanitize(\"database.password\", \"secret\")).isEqualTo(\"******\");\n    assertThat(sanitizer.sanitize(\"basic.auth.user.info\", \"secret\")).isEqualTo(\"******\");\n\n    //AWS var sanitizing\n    assertThat(sanitizer.sanitize(\"aws.access.key.id\", \"secret\")).isEqualTo(\"******\");\n    assertThat(sanitizer.sanitize(\"aws.accessKeyId\", \"secret\")).isEqualTo(\"******\");\n    assertThat(sanitizer.sanitize(\"aws.secret.access.key\", \"secret\")).isEqualTo(\"******\");\n    assertThat(sanitizer.sanitize(\"aws.secretAccessKey\", \"secret\")).isEqualTo(\"******\");\n    assertThat(sanitizer.sanitize(\"aws.sessionToken\", \"secret\")).isEqualTo(\"******\");\n  }\n\n  @Test\n  void notObfuscateNormalConfigs() {\n    final var sanitizer = new KafkaConfigSanitizer(true, List.of());\n    assertThat(sanitizer.sanitize(\"security.protocol\", \"SASL_SSL\")).isEqualTo(\"SASL_SSL\");\n    final String[] bootstrapServer = new String[] {\"test1:9092\", \"test2:9092\"};\n    assertThat(sanitizer.sanitize(\"bootstrap.servers\", bootstrapServer)).isEqualTo(bootstrapServer);\n  }\n\n  @Test\n  void obfuscateCredentialsWithDefinedPatterns() {\n    final var sanitizer = new KafkaConfigSanitizer(true, Arrays.asList(\"kafka.ui\", \".*test.*\"));\n    assertThat(sanitizer.sanitize(\"consumer.kafka.ui\", \"secret\")).isEqualTo(\"******\");\n    assertThat(sanitizer.sanitize(\"this.is.test.credentials\", \"secret\")).isEqualTo(\"******\");\n    assertThat(sanitizer.sanitize(\"this.is.not.credential\", \"not.credential\"))\n            .isEqualTo(\"not.credential\");\n    assertThat(sanitizer.sanitize(\"database.password\", \"no longer credential\"))\n            .isEqualTo(\"no longer credential\");\n  }\n\n  @Test\n  void sanitizeConnectorConfigDoNotFailOnNullableValues() {\n    Map<String, Object> originalConfig = new HashMap<>();\n    originalConfig.put(\"password\", \"secret\");\n    originalConfig.put(\"asIs\", \"normal\");\n    originalConfig.put(\"nullVal\", null);\n\n    var sanitizedConfig = new KafkaConfigSanitizer(true, List.of())\n        .sanitizeConnectorConfig(originalConfig);\n\n    assertThat(sanitizedConfig)\n        .hasSize(3)\n        .containsEntry(\"password\", \"******\")\n        .containsEntry(\"asIs\", \"normal\")\n        .containsEntry(\"nullVal\", null);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/LogDirsTest.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.provectus.kafka.ui.AbstractIntegrationTest;\nimport com.provectus.kafka.ui.exception.LogDirNotFoundApiException;\nimport com.provectus.kafka.ui.exception.TopicOrPartitionNotFoundException;\nimport com.provectus.kafka.ui.model.BrokerTopicLogdirsDTO;\nimport com.provectus.kafka.ui.model.BrokersLogdirsDTO;\nimport com.provectus.kafka.ui.model.ErrorResponseDTO;\nimport java.util.List;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.core.ParameterizedTypeReference;\nimport org.springframework.test.web.reactive.server.WebTestClient;\n\npublic class LogDirsTest extends AbstractIntegrationTest {\n\n  @Autowired\n  private WebTestClient webTestClient;\n\n  @Test\n  public void testAllBrokers() {\n    List<BrokersLogdirsDTO> dirs = webTestClient.get()\n        .uri(\"/api/clusters/{clusterName}/brokers/logdirs\", LOCAL)\n        .exchange()\n        .expectStatus().isOk()\n        .expectBody(new ParameterizedTypeReference<List<BrokersLogdirsDTO>>() {})\n        .returnResult()\n        .getResponseBody();\n\n    assertThat(dirs).hasSize(1);\n    BrokersLogdirsDTO dir = dirs.get(0);\n    assertThat(dir.getName()).isEqualTo(\"/var/lib/kafka/data\");\n    assertThat(dir.getTopics().stream().anyMatch(t -> t.getName().equals(\"__consumer_offsets\")))\n        .isTrue();\n\n    BrokerTopicLogdirsDTO topic = dir.getTopics().stream()\n        .filter(t -> t.getName().equals(\"__consumer_offsets\"))\n        .findAny().get();\n\n    assertThat(topic.getPartitions()).hasSize(1);\n    assertThat(topic.getPartitions().get(0).getBroker()).isEqualTo(1);\n    assertThat(topic.getPartitions().get(0).getSize()).isPositive();\n  }\n\n  @Test\n  public void testOneBrokers() {\n    List<BrokersLogdirsDTO> dirs = webTestClient.get()\n        .uri(\"/api/clusters/{clusterName}/brokers/logdirs?broker=1\", LOCAL)\n        .exchange()\n        .expectStatus().isOk()\n        .expectBody(new ParameterizedTypeReference<List<BrokersLogdirsDTO>>() {})\n        .returnResult()\n        .getResponseBody();\n\n    assertThat(dirs).hasSize(1);\n    BrokersLogdirsDTO dir = dirs.get(0);\n    assertThat(dir.getName()).isEqualTo(\"/var/lib/kafka/data\");\n    assertThat(dir.getTopics().stream().anyMatch(t -> t.getName().equals(\"__consumer_offsets\")))\n        .isTrue();\n\n    BrokerTopicLogdirsDTO topic = dir.getTopics().stream()\n        .filter(t -> t.getName().equals(\"__consumer_offsets\"))\n        .findAny().get();\n\n    assertThat(topic.getPartitions()).hasSize(1);\n    assertThat(topic.getPartitions().get(0).getBroker()).isEqualTo(1);\n    assertThat(topic.getPartitions().get(0).getSize()).isPositive();\n  }\n\n  @Test\n  public void testWrongBrokers() {\n    List<BrokersLogdirsDTO> dirs = webTestClient.get()\n        .uri(\"/api/clusters/{clusterName}/brokers/logdirs?broker=2\", LOCAL)\n        .exchange()\n        .expectStatus().isOk()\n        .expectBody(new ParameterizedTypeReference<List<BrokersLogdirsDTO>>() {})\n        .returnResult()\n        .getResponseBody();\n\n    assertThat(dirs).isEmpty();\n  }\n\n  @Test\n  public void testChangeDirToWrongDir() {\n    ErrorResponseDTO dirs = webTestClient.patch()\n        .uri(\"/api/clusters/{clusterName}/brokers/{id}/logdirs\", LOCAL, 1)\n        .bodyValue(Map.of(\n            \"topic\", \"__consumer_offsets\",\n            \"partition\", \"0\",\n            \"logDir\", \"/asdf/as\"\n            )\n        )\n        .exchange()\n        .expectStatus().isBadRequest()\n        .expectBody(ErrorResponseDTO.class)\n        .returnResult()\n        .getResponseBody();\n\n    assertThat(dirs.getMessage())\n        .isEqualTo(new LogDirNotFoundApiException().getMessage());\n\n    dirs = webTestClient.patch()\n        .uri(\"/api/clusters/{clusterName}/brokers/{id}/logdirs\", LOCAL, 1)\n        .bodyValue(Map.of(\n            \"topic\", \"asdf\",\n            \"partition\", \"0\",\n            \"logDir\", \"/var/lib/kafka/data\"\n            )\n        )\n        .exchange()\n        .expectStatus().isBadRequest()\n        .expectBody(ErrorResponseDTO.class)\n        .returnResult()\n        .getResponseBody();\n\n    assertThat(dirs.getMessage())\n        .isEqualTo(new TopicOrPartitionNotFoundException().getMessage());\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/MessagesServiceTest.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport static com.provectus.kafka.ui.service.MessagesService.execSmartFilterTest;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.provectus.kafka.ui.AbstractIntegrationTest;\nimport com.provectus.kafka.ui.exception.TopicNotFoundException;\nimport com.provectus.kafka.ui.model.ConsumerPosition;\nimport com.provectus.kafka.ui.model.CreateTopicMessageDTO;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.SeekDirectionDTO;\nimport com.provectus.kafka.ui.model.SeekTypeDTO;\nimport com.provectus.kafka.ui.model.SmartFilterTestExecutionDTO;\nimport com.provectus.kafka.ui.model.TopicMessageDTO;\nimport com.provectus.kafka.ui.model.TopicMessageEventDTO;\nimport com.provectus.kafka.ui.producer.KafkaTestProducer;\nimport com.provectus.kafka.ui.serdes.builtin.StringSerde;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport org.apache.kafka.clients.admin.NewTopic;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport reactor.core.publisher.Flux;\nimport reactor.test.StepVerifier;\n\nclass MessagesServiceTest extends AbstractIntegrationTest {\n\n  private static final String MASKED_TOPICS_PREFIX = \"masking-test-\";\n  private static final String NON_EXISTING_TOPIC = UUID.randomUUID().toString();\n\n  @Autowired\n  MessagesService messagesService;\n\n  KafkaCluster cluster;\n\n  @BeforeEach\n  void init() {\n    cluster = applicationContext\n        .getBean(ClustersStorage.class)\n        .getClusterByName(LOCAL)\n        .get();\n  }\n\n  @Test\n  void deleteTopicMessagesReturnsExceptionWhenTopicNotFound() {\n    StepVerifier.create(messagesService.deleteTopicMessages(cluster, NON_EXISTING_TOPIC, List.of()))\n        .expectError(TopicNotFoundException.class)\n        .verify();\n  }\n\n  @Test\n  void sendMessageReturnsExceptionWhenTopicNotFound() {\n    StepVerifier.create(messagesService.sendMessage(cluster, NON_EXISTING_TOPIC, new CreateTopicMessageDTO()))\n        .expectError(TopicNotFoundException.class)\n        .verify();\n  }\n\n  @Test\n  void loadMessagesReturnsExceptionWhenTopicNotFound() {\n    StepVerifier.create(messagesService\n            .loadMessages(cluster, NON_EXISTING_TOPIC, null, null, null, 1, null, \"String\", \"String\"))\n        .expectError(TopicNotFoundException.class)\n        .verify();\n  }\n\n  @Test\n  void maskingAppliedOnConfiguredClusters() throws Exception {\n    String testTopic = MASKED_TOPICS_PREFIX + UUID.randomUUID();\n    try (var producer = KafkaTestProducer.forKafka(kafka)) {\n      createTopic(new NewTopic(testTopic, 1, (short) 1));\n      producer.send(testTopic, \"message1\");\n      producer.send(testTopic, \"message2\").get();\n\n      Flux<TopicMessageDTO> msgsFlux = messagesService.loadMessages(\n          cluster,\n          testTopic,\n          new ConsumerPosition(SeekTypeDTO.BEGINNING, testTopic, null),\n          null,\n          null,\n          100,\n          SeekDirectionDTO.FORWARD,\n          StringSerde.name(),\n          StringSerde.name()\n      ).filter(evt -> evt.getType() == TopicMessageEventDTO.TypeEnum.MESSAGE)\n          .map(TopicMessageEventDTO::getMessage);\n\n      // both messages should be masked\n      StepVerifier.create(msgsFlux)\n          .expectNextMatches(msg -> msg.getContent().equals(\"***\"))\n          .expectNextMatches(msg -> msg.getContent().equals(\"***\"))\n          .verifyComplete();\n    } finally {\n      deleteTopic(testTopic);\n    }\n  }\n\n  @Test\n  void execSmartFilterTestReturnsExecutionResult() {\n    var params = new SmartFilterTestExecutionDTO()\n        .filterCode(\"key != null && value != null && headers != null && timestampMs != null && offset != null\")\n        .key(\"1234\")\n        .value(\"{ \\\"some\\\" : \\\"value\\\" } \")\n        .headers(Map.of(\"h1\", \"hv1\"))\n        .offset(12345L)\n        .timestampMs(System.currentTimeMillis())\n        .partition(1);\n    assertThat(execSmartFilterTest(params).getResult()).isTrue();\n\n    params.setFilterCode(\"return false\");\n    assertThat(execSmartFilterTest(params).getResult()).isFalse();\n  }\n\n  @Test\n  void execSmartFilterTestReturnsErrorOnFilterApplyError() {\n    var result = execSmartFilterTest(\n        new SmartFilterTestExecutionDTO()\n            .filterCode(\"return 1/0\")\n    );\n    assertThat(result.getResult()).isNull();\n    assertThat(result.getError()).containsIgnoringCase(\"execution error\");\n  }\n\n  @Test\n  void execSmartFilterTestReturnsErrorOnFilterCompilationError() {\n    var result = execSmartFilterTest(\n        new SmartFilterTestExecutionDTO()\n            .filterCode(\"this is invalid groovy syntax = 1\")\n    );\n    assertThat(result.getResult()).isNull();\n    assertThat(result.getError()).containsIgnoringCase(\"Compilation error\");\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/OffsetsResetServiceTest.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.provectus.kafka.ui.AbstractIntegrationTest;\nimport com.provectus.kafka.ui.exception.NotFoundException;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Properties;\nimport java.util.UUID;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport java.util.stream.Stream;\nimport org.apache.kafka.clients.admin.NewTopic;\nimport org.apache.kafka.clients.consumer.Consumer;\nimport org.apache.kafka.clients.consumer.ConsumerConfig;\nimport org.apache.kafka.clients.consumer.KafkaConsumer;\nimport org.apache.kafka.clients.consumer.OffsetAndMetadata;\nimport org.apache.kafka.clients.producer.KafkaProducer;\nimport org.apache.kafka.clients.producer.ProducerConfig;\nimport org.apache.kafka.clients.producer.ProducerRecord;\nimport org.apache.kafka.common.TopicPartition;\nimport org.apache.kafka.common.serialization.BytesDeserializer;\nimport org.apache.kafka.common.serialization.BytesSerializer;\nimport org.apache.kafka.common.utils.Bytes;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\n\npublic class OffsetsResetServiceTest extends AbstractIntegrationTest {\n\n  private static final int PARTITIONS = 5;\n\n  private final String groupId = \"OffsetsResetServiceTestGroup-\" + UUID.randomUUID();\n  private final String topic = \"OffsetsResetServiceTestTopic-\" + UUID.randomUUID();\n\n  private KafkaCluster cluster;\n  private OffsetsResetService offsetsResetService;\n\n  @BeforeEach\n  void init() {\n    cluster = applicationContext.getBean(ClustersStorage.class).getClusterByName(LOCAL).get();\n    offsetsResetService = new OffsetsResetService(applicationContext.getBean(AdminClientService.class));\n    createTopic(new NewTopic(topic, PARTITIONS, (short) 1));\n    createConsumerGroup();\n  }\n\n  @AfterEach\n  void cleanUp() {\n    deleteTopic(topic);\n  }\n\n  private void createConsumerGroup() {\n    try (var consumer = groupConsumer()) {\n      consumer.subscribe(Pattern.compile(\"no-such-topic-pattern\"));\n      consumer.poll(Duration.ofMillis(200));\n      consumer.commitSync();\n    }\n  }\n\n  @Test\n  void failsIfGroupDoesNotExists() {\n    List<Mono<?>> expectedNotFound = List.of(\n        offsetsResetService\n            .resetToEarliest(cluster, \"non-existing-group\", topic, null),\n        offsetsResetService\n            .resetToLatest(cluster, \"non-existing-group\", topic, null),\n        offsetsResetService\n            .resetToTimestamp(cluster, \"non-existing-group\", topic, null, System.currentTimeMillis()),\n        offsetsResetService\n            .resetToOffsets(cluster, \"non-existing-group\", topic, Map.of())\n    );\n\n    for (Mono<?> mono : expectedNotFound) {\n      StepVerifier.create(mono)\n          .expectErrorMatches(t -> t instanceof NotFoundException)\n          .verify();\n    }\n  }\n\n  @Test\n  void failsIfGroupIsActive() {\n    // starting consumer to activate group\n    try (var consumer = groupConsumer()) {\n\n      consumer.subscribe(Pattern.compile(\"no-such-topic-pattern\"));\n      consumer.poll(Duration.ofMillis(100));\n\n      List<Mono<?>> expectedValidationError = List.of(\n          offsetsResetService.resetToEarliest(cluster, groupId, topic, null),\n          offsetsResetService.resetToLatest(cluster, groupId, topic, null),\n          offsetsResetService\n              .resetToTimestamp(cluster, groupId, topic, null, System.currentTimeMillis()),\n          offsetsResetService.resetToOffsets(cluster, groupId, topic, Map.of())\n      );\n\n      for (Mono<?> mono : expectedValidationError) {\n        StepVerifier.create(mono)\n            .expectErrorMatches(t -> t instanceof ValidationException)\n            .verify();\n      }\n    }\n  }\n\n  @Test\n  void resetToOffsets() {\n    sendMsgsToPartition(Map.of(0, 10, 1, 10, 2, 10));\n\n    var expectedOffsets = Map.of(0, 5L, 1, 5L, 2, 5L);\n    offsetsResetService.resetToOffsets(cluster, groupId, topic, expectedOffsets).block();\n    assertOffsets(expectedOffsets);\n  }\n\n  @Test\n  void resetToOffsetsCommitsEarliestOrLatestOffsetsIfOffsetsBoundsNotValid() {\n    sendMsgsToPartition(Map.of(0, 10, 1, 10, 2, 10));\n\n    var offsetsWithInValidBounds = Map.of(0, -2L, 1, 5L, 2, 500L);\n    var expectedOffsets = Map.of(0, 0L, 1, 5L, 2, 10L);\n    offsetsResetService.resetToOffsets(cluster, groupId, topic, offsetsWithInValidBounds).block();\n    assertOffsets(expectedOffsets);\n  }\n\n  @Test\n  void resetToEarliest() {\n    sendMsgsToPartition(Map.of(0, 10, 1, 10, 2, 10));\n\n    commit(Map.of(0, 5L, 1, 5L, 2, 5L));\n    offsetsResetService.resetToEarliest(cluster, groupId, topic, List.of(0, 1)).block();\n    assertOffsets(Map.of(0, 0L, 1, 0L, 2, 5L));\n\n    commit(Map.of(0, 5L, 1, 5L, 2, 5L));\n    offsetsResetService.resetToEarliest(cluster, groupId, topic, null).block();\n    assertOffsets(Map.of(0, 0L, 1, 0L, 2, 0L, 3, 0L, 4, 0L));\n  }\n\n  @Test\n  void resetToLatest() {\n    sendMsgsToPartition(Map.of(0, 10, 1, 10, 2, 10, 3, 10, 4, 10));\n\n    commit(Map.of(0, 5L, 1, 5L, 2, 5L));\n    offsetsResetService.resetToLatest(cluster, groupId, topic, List.of(0, 1)).block();\n    assertOffsets(Map.of(0, 10L, 1, 10L, 2, 5L));\n\n    commit(Map.of(0, 5L, 1, 5L, 2, 5L));\n    offsetsResetService.resetToLatest(cluster, groupId, topic, null).block();\n    assertOffsets(Map.of(0, 10L, 1, 10L, 2, 10L, 3, 10L, 4, 10L));\n  }\n\n  @Test\n  void resetToTimestamp() {\n    send(\n        Stream.of(\n            new ProducerRecord<Bytes, Bytes>(topic, 0, 1000L, null, null),\n            new ProducerRecord<Bytes, Bytes>(topic, 0, 1500L, null, null),\n            new ProducerRecord<Bytes, Bytes>(topic, 0, 2000L, null, null),\n            new ProducerRecord<Bytes, Bytes>(topic, 1, 1000L, null, null),\n            new ProducerRecord<Bytes, Bytes>(topic, 1, 2000L, null, null),\n            new ProducerRecord<Bytes, Bytes>(topic, 2, 1000L, null, null),\n            new ProducerRecord<Bytes, Bytes>(topic, 2, 1100L, null, null),\n            new ProducerRecord<Bytes, Bytes>(topic, 2, 1200L, null, null)));\n\n    offsetsResetService.resetToTimestamp(\n        cluster, groupId, topic, List.of(0, 1, 2, 3), 1600L\n    ).block();\n    assertOffsets(Map.of(0, 2L, 1, 1L, 2, 3L, 3, 0L));\n  }\n\n\n  private void commit(Map<Integer, Long> offsetsToCommit) {\n    try (var consumer = groupConsumer()) {\n      consumer.commitSync(\n          offsetsToCommit.entrySet().stream()\n              .collect(Collectors.toMap(\n                  e -> new TopicPartition(topic, e.getKey()),\n                  e -> new OffsetAndMetadata(e.getValue())))\n      );\n    }\n  }\n\n  private void sendMsgsToPartition(Map<Integer, Integer> msgsCountForPartitions) {\n    Bytes bytes = new Bytes(\"noMatter\".getBytes());\n    send(\n        msgsCountForPartitions.entrySet().stream()\n            .flatMap(e ->\n                IntStream.range(0, e.getValue())\n                    .mapToObj(i -> new ProducerRecord<>(topic, e.getKey(), bytes, bytes))));\n  }\n\n  private void send(Stream<ProducerRecord<Bytes, Bytes>> toSend) {\n    var properties = new Properties();\n    properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());\n    var serializer = new BytesSerializer();\n    try (var producer = new KafkaProducer<>(properties, serializer, serializer)) {\n      toSend.forEach(producer::send);\n      producer.flush();\n    }\n  }\n\n  private void assertOffsets(Map<Integer, Long> expectedOffsets) {\n    try (var consumer = groupConsumer()) {\n      var tps = expectedOffsets.keySet().stream()\n          .map(idx -> new TopicPartition(topic, idx))\n          .collect(Collectors.toSet());\n\n      var actualOffsets = consumer.committed(tps).entrySet().stream()\n          .collect(Collectors.toMap(e -> e.getKey().partition(), e -> e.getValue().offset()));\n\n      assertThat(actualOffsets).isEqualTo(expectedOffsets);\n    }\n  }\n\n  private Consumer<?, ?> groupConsumer() {\n    Properties props = new Properties();\n    props.put(ConsumerConfig.CLIENT_ID_CONFIG, \"kafka-ui-\" + UUID.randomUUID());\n    props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers());\n    props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, BytesDeserializer.class);\n    props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, BytesDeserializer.class);\n    props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, \"earliest\");\n    props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);\n    return new KafkaConsumer<>(props);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ReactiveAdminClientTest.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport static com.provectus.kafka.ui.service.ReactiveAdminClient.toMonoWithExceptionFilter;\nimport static java.util.Objects.requireNonNull;\nimport static org.apache.kafka.clients.admin.ListOffsetsResult.ListOffsetsResultInfo;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static org.assertj.core.api.ThrowableAssert.ThrowingCallable;\n\nimport com.provectus.kafka.ui.AbstractIntegrationTest;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport com.provectus.kafka.ui.producer.KafkaTestProducer;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Properties;\nimport java.util.UUID;\nimport java.util.function.Function;\nimport java.util.stream.Stream;\nimport lombok.SneakyThrows;\nimport org.apache.kafka.clients.admin.AdminClient;\nimport org.apache.kafka.clients.admin.AlterConfigOp;\nimport org.apache.kafka.clients.admin.Config;\nimport org.apache.kafka.clients.admin.ConfigEntry;\nimport org.apache.kafka.clients.admin.NewTopic;\nimport org.apache.kafka.clients.admin.OffsetSpec;\nimport org.apache.kafka.clients.admin.TopicDescription;\nimport org.apache.kafka.clients.consumer.ConsumerConfig;\nimport org.apache.kafka.clients.consumer.KafkaConsumer;\nimport org.apache.kafka.clients.consumer.OffsetAndMetadata;\nimport org.apache.kafka.clients.producer.ProducerRecord;\nimport org.apache.kafka.common.KafkaFuture;\nimport org.apache.kafka.common.Node;\nimport org.apache.kafka.common.TopicPartition;\nimport org.apache.kafka.common.TopicPartitionInfo;\nimport org.apache.kafka.common.config.ConfigResource;\nimport org.apache.kafka.common.errors.UnknownTopicOrPartitionException;\nimport org.apache.kafka.common.internals.KafkaFutureImpl;\nimport org.apache.kafka.common.serialization.StringDeserializer;\nimport org.assertj.core.api.ThrowableAssert;\nimport org.junit.function.ThrowingRunnable;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport reactor.test.StepVerifier;\n\nclass ReactiveAdminClientTest extends AbstractIntegrationTest {\n\n  private final List<ThrowingRunnable> clearings = new ArrayList<>();\n\n  private AdminClient adminClient;\n  private ReactiveAdminClient reactiveAdminClient;\n\n  @BeforeEach\n  void init() {\n    AdminClientService adminClientService = applicationContext.getBean(AdminClientService.class);\n    ClustersStorage clustersStorage = applicationContext.getBean(ClustersStorage.class);\n    reactiveAdminClient = requireNonNull(adminClientService.get(clustersStorage.getClusterByName(LOCAL).get()).block());\n    adminClient = reactiveAdminClient.getClient();\n  }\n\n  @AfterEach\n  void tearDown() {\n    for (ThrowingRunnable clearing : clearings) {\n      try {\n        clearing.run();\n      } catch (Throwable th) {\n        //NOOP\n      }\n    }\n  }\n\n  @Test\n  void testUpdateTopicConfigs() throws Exception {\n    String topic = UUID.randomUUID().toString();\n    createTopics(new NewTopic(topic, 1, (short) 1));\n\n    var configResource = new ConfigResource(ConfigResource.Type.TOPIC, topic);\n\n    adminClient.incrementalAlterConfigs(\n        Map.of(\n            configResource,\n            List.of(\n                new AlterConfigOp(new ConfigEntry(\"compression.type\", \"gzip\"), AlterConfigOp.OpType.SET),\n                new AlterConfigOp(new ConfigEntry(\"retention.bytes\", \"12345678\"), AlterConfigOp.OpType.SET)\n            )\n        )\n    ).all().get();\n\n    StepVerifier.create(\n        reactiveAdminClient.updateTopicConfig(\n            topic,\n            Map.of(\n                \"compression.type\", \"snappy\", //changing existing config\n                \"file.delete.delay.ms\", \"12345\" // adding new one\n            )\n        )\n    ).expectComplete().verify();\n\n    Config config = adminClient.describeConfigs(List.of(configResource)).values().get(configResource).get();\n    assertThat(config.get(\"retention.bytes\").value()).isNotEqualTo(\"12345678\"); // wes reset to default\n    assertThat(config.get(\"compression.type\").value()).isEqualTo(\"snappy\");\n    assertThat(config.get(\"file.delete.delay.ms\").value()).isEqualTo(\"12345\");\n  }\n\n\n  @SneakyThrows\n  void createTopics(NewTopic... topics) {\n    adminClient.createTopics(List.of(topics)).all().get();\n    clearings.add(() -> adminClient.deleteTopics(Stream.of(topics).map(NewTopic::name).toList()).all().get());\n  }\n\n  void fillTopic(String topic, int msgsCnt) {\n    try (var producer = KafkaTestProducer.forKafka(kafka)) {\n      for (int i = 0; i < msgsCnt; i++) {\n        producer.send(topic, UUID.randomUUID().toString());\n      }\n    }\n  }\n\n  @Test\n  void testToMonoWithExceptionFilter() {\n    var failedFuture = new KafkaFutureImpl<String>();\n    failedFuture.completeExceptionally(new UnknownTopicOrPartitionException());\n\n    var okFuture = new KafkaFutureImpl<String>();\n    okFuture.complete(\"done\");\n\n    var emptyFuture = new KafkaFutureImpl<String>();\n    emptyFuture.complete(null);\n\n    Map<String, KafkaFuture<String>> arg = Map.of(\n        \"failure\", failedFuture,\n        \"ok\", okFuture,\n        \"empty\", emptyFuture\n    );\n    StepVerifier.create(toMonoWithExceptionFilter(arg, UnknownTopicOrPartitionException.class))\n        .assertNext(result -> assertThat(result).hasSize(1).containsEntry(\"ok\", \"done\"))\n        .verifyComplete();\n  }\n\n  @Test\n  void filterPartitionsWithLeaderCheckSkipsPartitionsFromTopicWhereSomePartitionsHaveNoLeader() {\n    var filteredPartitions = ReactiveAdminClient.filterPartitionsWithLeaderCheck(\n        List.of(\n            // contains partitions with no leader\n            new TopicDescription(\"noLeaderTopic\", false,\n                List.of(\n                    new TopicPartitionInfo(0, new Node(1, \"n1\", 9092), List.of(), List.of()),\n                    new TopicPartitionInfo(1, null, List.of(), List.of()))),\n            // should be skipped by predicate\n            new TopicDescription(\"skippingByPredicate\", false,\n                List.of(\n                    new TopicPartitionInfo(0, new Node(1, \"n1\", 9092), List.of(), List.of()))),\n            // good topic\n            new TopicDescription(\"good\", false,\n                List.of(\n                    new TopicPartitionInfo(0, new Node(1, \"n1\", 9092), List.of(), List.of()),\n                    new TopicPartitionInfo(1, new Node(2, \"n2\", 9092), List.of(), List.of()))\n            )),\n        p -> !p.topic().equals(\"skippingByPredicate\"),\n        false\n    );\n\n    assertThat(filteredPartitions)\n        .containsExactlyInAnyOrder(\n            new TopicPartition(\"good\", 0),\n            new TopicPartition(\"good\", 1)\n        );\n  }\n\n  @Test\n  void filterPartitionsWithLeaderCheckThrowExceptionIfThereIsSomePartitionsWithoutLeaderAndFlagSet() {\n    ThrowingCallable call = () -> ReactiveAdminClient.filterPartitionsWithLeaderCheck(\n        List.of(\n            // contains partitions with no leader\n            new TopicDescription(\"t1\", false,\n                List.of(\n                    new TopicPartitionInfo(0, new Node(1, \"n1\", 9092), List.of(), List.of()),\n                    new TopicPartitionInfo(1, null, List.of(), List.of()))),\n            new TopicDescription(\"t2\", false,\n                List.of(\n                    new TopicPartitionInfo(0, new Node(1, \"n1\", 9092), List.of(), List.of()))\n            )),\n        p -> true,\n        // setting failOnNoLeader flag\n        true\n    );\n    assertThatThrownBy(call).isInstanceOf(ValidationException.class);\n  }\n\n  @Test\n  void testListOffsetsUnsafe() {\n    String topic = UUID.randomUUID().toString();\n    createTopics(new NewTopic(topic, 2, (short) 1));\n\n    // sending messages to have non-zero offsets for tp\n    try (var producer = KafkaTestProducer.forKafka(kafka)) {\n      producer.send(new ProducerRecord<>(topic, 1, \"k\", \"v\"));\n      producer.send(new ProducerRecord<>(topic, 1, \"k\", \"v\"));\n    }\n\n    var requestedPartitions = List.of(\n        new TopicPartition(topic, 0),\n        new TopicPartition(topic, 1)\n    );\n\n    StepVerifier.create(reactiveAdminClient.listOffsetsUnsafe(requestedPartitions, OffsetSpec.earliest()))\n        .assertNext(offsets -> {\n          assertThat(offsets)\n              .hasSize(2)\n              .containsEntry(new TopicPartition(topic, 0), 0L)\n              .containsEntry(new TopicPartition(topic, 1), 0L);\n        })\n        .verifyComplete();\n\n    StepVerifier.create(reactiveAdminClient.listOffsetsUnsafe(requestedPartitions, OffsetSpec.latest()))\n        .assertNext(offsets -> {\n          assertThat(offsets)\n              .hasSize(2)\n              .containsEntry(new TopicPartition(topic, 0), 0L)\n              .containsEntry(new TopicPartition(topic, 1), 2L);\n        })\n        .verifyComplete();\n  }\n\n\n  @Test\n  void testListConsumerGroupOffsets() throws Exception {\n    String topic = UUID.randomUUID().toString();\n    String anotherTopic = UUID.randomUUID().toString();\n    createTopics(new NewTopic(topic, 2, (short) 1), new NewTopic(anotherTopic, 1, (short) 1));\n    fillTopic(topic, 10);\n\n    Function<String, KafkaConsumer<String, String>> consumerSupplier = groupName -> {\n      Properties p = new Properties();\n      p.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());\n      p.setProperty(ConsumerConfig.GROUP_ID_CONFIG, groupName);\n      p.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, \"earliest\");\n      p.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());\n      p.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());\n      p.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, \"false\");\n      return new KafkaConsumer<String, String>(p);\n    };\n\n    String fullyPolledConsumer = UUID.randomUUID().toString();\n    try (KafkaConsumer<String, String> c = consumerSupplier.apply(fullyPolledConsumer)) {\n      c.subscribe(List.of(topic));\n      int polled = 0;\n      while (polled < 10) {\n        polled += c.poll(Duration.ofMillis(50)).count();\n      }\n      c.commitSync();\n    }\n\n    String polled1MsgConsumer = UUID.randomUUID().toString();\n    try (KafkaConsumer<String, String> c = consumerSupplier.apply(polled1MsgConsumer)) {\n      c.subscribe(List.of(topic));\n      c.poll(Duration.ofMillis(100));\n      c.commitSync(Map.of(tp(topic, 0), new OffsetAndMetadata(1)));\n    }\n\n    String noCommitConsumer = UUID.randomUUID().toString();\n    try (KafkaConsumer<String, String> c = consumerSupplier.apply(noCommitConsumer)) {\n      c.subscribe(List.of(topic));\n      c.poll(Duration.ofMillis(100));\n    }\n\n    Map<TopicPartition, ListOffsetsResultInfo> endOffsets = adminClient.listOffsets(Map.of(\n        tp(topic, 0), OffsetSpec.latest(),\n        tp(topic, 1), OffsetSpec.latest())).all().get();\n\n    StepVerifier.create(\n            reactiveAdminClient.listConsumerGroupOffsets(\n                List.of(fullyPolledConsumer, polled1MsgConsumer, noCommitConsumer),\n                List.of(\n                    tp(topic, 0),\n                    tp(topic, 1),\n                    tp(anotherTopic, 0))\n            )\n        ).assertNext(table -> {\n\n          assertThat(table.row(polled1MsgConsumer))\n              .containsEntry(tp(topic, 0), 1L)\n              .hasSize(1);\n\n          assertThat(table.row(noCommitConsumer))\n              .isEmpty();\n\n          assertThat(table.row(fullyPolledConsumer))\n              .containsEntry(tp(topic, 0), endOffsets.get(tp(topic, 0)).offset())\n              .containsEntry(tp(topic, 1), endOffsets.get(tp(topic, 1)).offset())\n              .hasSize(2);\n        })\n        .verifyComplete();\n  }\n\n  private static TopicPartition tp(String topic, int partition) {\n    return new TopicPartition(topic, partition);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/RecordEmitterTest.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport static com.provectus.kafka.ui.model.SeekTypeDTO.BEGINNING;\nimport static com.provectus.kafka.ui.model.SeekTypeDTO.LATEST;\nimport static com.provectus.kafka.ui.model.SeekTypeDTO.OFFSET;\nimport static com.provectus.kafka.ui.model.SeekTypeDTO.TIMESTAMP;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.provectus.kafka.ui.AbstractIntegrationTest;\nimport com.provectus.kafka.ui.emitter.BackwardEmitter;\nimport com.provectus.kafka.ui.emitter.EnhancedConsumer;\nimport com.provectus.kafka.ui.emitter.ForwardEmitter;\nimport com.provectus.kafka.ui.emitter.PollingSettings;\nimport com.provectus.kafka.ui.emitter.PollingThrottler;\nimport com.provectus.kafka.ui.model.ConsumerPosition;\nimport com.provectus.kafka.ui.model.TopicMessageDTO;\nimport com.provectus.kafka.ui.model.TopicMessageEventDTO;\nimport com.provectus.kafka.ui.producer.KafkaTestProducer;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport com.provectus.kafka.ui.serdes.ConsumerRecordDeserializer;\nimport com.provectus.kafka.ui.serdes.PropertyResolverImpl;\nimport com.provectus.kafka.ui.serdes.builtin.StringSerde;\nimport com.provectus.kafka.ui.util.ApplicationMetrics;\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Properties;\nimport java.util.UUID;\nimport java.util.concurrent.ThreadLocalRandom;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\nimport lombok.Value;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.kafka.clients.admin.NewTopic;\nimport org.apache.kafka.clients.consumer.ConsumerConfig;\nimport org.apache.kafka.clients.producer.ProducerRecord;\nimport org.apache.kafka.common.TopicPartition;\nimport org.apache.kafka.common.header.internals.RecordHeader;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.FluxSink;\nimport reactor.test.StepVerifier;\n\n@Slf4j\nclass RecordEmitterTest extends AbstractIntegrationTest {\n\n  static final int PARTITIONS = 5;\n  static final int MSGS_PER_PARTITION = 100;\n\n  static final String TOPIC = RecordEmitterTest.class.getSimpleName() + \"_\" + UUID.randomUUID();\n  static final String EMPTY_TOPIC = TOPIC + \"_empty\";\n  static final List<Record> SENT_RECORDS = new ArrayList<>();\n  static final ConsumerRecordDeserializer RECORD_DESERIALIZER = createRecordsDeserializer();\n  static final Predicate<TopicMessageDTO> NOOP_FILTER = m -> true;\n\n  @BeforeAll\n  static void generateMsgs() throws Exception {\n    createTopic(new NewTopic(TOPIC, PARTITIONS, (short) 1));\n    createTopic(new NewTopic(EMPTY_TOPIC, PARTITIONS, (short) 1));\n    try (var producer = KafkaTestProducer.forKafka(kafka)) {\n      for (int partition = 0; partition < PARTITIONS; partition++) {\n        for (int i = 0; i < MSGS_PER_PARTITION; i++) {\n          long ts = System.currentTimeMillis() + i;\n          var value = \"msg_\" + partition + \"_\" + i;\n          var metadata = producer.send(\n              new ProducerRecord<>(\n                  TOPIC, partition, ts, null, value, List.of(\n                      new RecordHeader(\"name\", null),\n                      new RecordHeader(\"name2\", \"value\".getBytes())\n                  )\n              )\n          ).get();\n          SENT_RECORDS.add(\n              new Record(\n                  value,\n                  new TopicPartition(metadata.topic(), metadata.partition()),\n                  metadata.offset(),\n                  ts\n              )\n          );\n        }\n      }\n    }\n  }\n\n  @AfterAll\n  static void cleanup() {\n    deleteTopic(TOPIC);\n    deleteTopic(EMPTY_TOPIC);\n    SENT_RECORDS.clear();\n  }\n\n  private static ConsumerRecordDeserializer createRecordsDeserializer() {\n    Serde s = new StringSerde();\n    s.configure(PropertyResolverImpl.empty(), PropertyResolverImpl.empty(), PropertyResolverImpl.empty());\n    return new ConsumerRecordDeserializer(\n        StringSerde.name(),\n        s.deserializer(null, Serde.Target.KEY),\n        StringSerde.name(),\n        s.deserializer(null, Serde.Target.VALUE),\n        StringSerde.name(),\n        s.deserializer(null, Serde.Target.KEY),\n        s.deserializer(null, Serde.Target.VALUE),\n        msg -> msg\n    );\n  }\n\n  @Test\n  void pollNothingOnEmptyTopic() {\n    var forwardEmitter = new ForwardEmitter(\n        this::createConsumer,\n        new ConsumerPosition(BEGINNING, EMPTY_TOPIC, null),\n        100,\n        RECORD_DESERIALIZER,\n        NOOP_FILTER,\n        PollingSettings.createDefault()\n    );\n\n    var backwardEmitter = new BackwardEmitter(\n        this::createConsumer,\n        new ConsumerPosition(BEGINNING, EMPTY_TOPIC, null),\n        100,\n        RECORD_DESERIALIZER,\n        NOOP_FILTER,\n        PollingSettings.createDefault()\n    );\n\n    StepVerifier.create(Flux.create(forwardEmitter))\n        .expectNextMatches(m -> m.getType().equals(TopicMessageEventDTO.TypeEnum.PHASE))\n        .expectNextMatches(m -> m.getType().equals(TopicMessageEventDTO.TypeEnum.DONE))\n        .expectComplete()\n        .verify();\n\n    StepVerifier.create(Flux.create(backwardEmitter))\n        .expectNextMatches(m -> m.getType().equals(TopicMessageEventDTO.TypeEnum.PHASE))\n        .expectNextMatches(m -> m.getType().equals(TopicMessageEventDTO.TypeEnum.DONE))\n        .expectComplete()\n        .verify();\n  }\n\n  @Test\n  void pollFullTopicFromBeginning() {\n    var forwardEmitter = new ForwardEmitter(\n        this::createConsumer,\n        new ConsumerPosition(BEGINNING, TOPIC, null),\n        PARTITIONS * MSGS_PER_PARTITION,\n        RECORD_DESERIALIZER,\n        NOOP_FILTER,\n        PollingSettings.createDefault()\n    );\n\n    var backwardEmitter = new BackwardEmitter(\n        this::createConsumer,\n        new ConsumerPosition(LATEST, TOPIC, null),\n        PARTITIONS * MSGS_PER_PARTITION,\n        RECORD_DESERIALIZER,\n        NOOP_FILTER,\n        PollingSettings.createDefault()\n    );\n\n    List<String> expectedValues = SENT_RECORDS.stream().map(Record::getValue).collect(Collectors.toList());\n\n    expectEmitter(forwardEmitter, expectedValues);\n    expectEmitter(backwardEmitter, expectedValues);\n  }\n\n  @Test\n  void pollWithOffsets() {\n    Map<TopicPartition, Long> targetOffsets = new HashMap<>();\n    for (int i = 0; i < PARTITIONS; i++) {\n      long offset = ThreadLocalRandom.current().nextLong(MSGS_PER_PARTITION);\n      targetOffsets.put(new TopicPartition(TOPIC, i), offset);\n    }\n\n    var forwardEmitter = new ForwardEmitter(\n        this::createConsumer,\n        new ConsumerPosition(OFFSET, TOPIC, targetOffsets),\n        PARTITIONS * MSGS_PER_PARTITION,\n        RECORD_DESERIALIZER,\n        NOOP_FILTER,\n        PollingSettings.createDefault()\n    );\n\n    var backwardEmitter = new BackwardEmitter(\n        this::createConsumer,\n        new ConsumerPosition(OFFSET, TOPIC, targetOffsets),\n        PARTITIONS * MSGS_PER_PARTITION,\n        RECORD_DESERIALIZER,\n        NOOP_FILTER,\n        PollingSettings.createDefault()\n    );\n\n    var expectedValues = SENT_RECORDS.stream()\n        .filter(r -> r.getOffset() >= targetOffsets.get(r.getTp()))\n        .map(Record::getValue)\n        .collect(Collectors.toList());\n\n    expectEmitter(forwardEmitter, expectedValues);\n\n    expectedValues = SENT_RECORDS.stream()\n        .filter(r -> r.getOffset() < targetOffsets.get(r.getTp()))\n        .map(Record::getValue)\n        .collect(Collectors.toList());\n\n    expectEmitter(backwardEmitter, expectedValues);\n  }\n\n  @Test\n  void pollWithTimestamps() {\n    Map<TopicPartition, Long> targetTimestamps = new HashMap<>();\n    final Map<TopicPartition, List<Record>> perPartition =\n        SENT_RECORDS.stream().collect(Collectors.groupingBy((r) -> r.tp));\n    for (int i = 0; i < PARTITIONS; i++) {\n      final List<Record> records = perPartition.get(new TopicPartition(TOPIC, i));\n      int randRecordIdx = ThreadLocalRandom.current().nextInt(records.size());\n      log.info(\"partition: {} position: {}\", i, randRecordIdx);\n      targetTimestamps.put(\n          new TopicPartition(TOPIC, i),\n          records.get(randRecordIdx).getTimestamp()\n      );\n    }\n\n    var forwardEmitter = new ForwardEmitter(\n        this::createConsumer,\n        new ConsumerPosition(TIMESTAMP, TOPIC, targetTimestamps),\n        PARTITIONS * MSGS_PER_PARTITION,\n        RECORD_DESERIALIZER,\n        NOOP_FILTER,\n        PollingSettings.createDefault()\n    );\n\n    var backwardEmitter = new BackwardEmitter(\n        this::createConsumer,\n        new ConsumerPosition(TIMESTAMP, TOPIC, targetTimestamps),\n        PARTITIONS * MSGS_PER_PARTITION,\n        RECORD_DESERIALIZER,\n        NOOP_FILTER,\n        PollingSettings.createDefault()\n    );\n\n    var expectedValues = SENT_RECORDS.stream()\n        .filter(r -> r.getTimestamp() >= targetTimestamps.get(r.getTp()))\n        .map(Record::getValue)\n        .collect(Collectors.toList());\n\n    expectEmitter(forwardEmitter, expectedValues);\n\n    expectedValues = SENT_RECORDS.stream()\n        .filter(r -> r.getTimestamp() < targetTimestamps.get(r.getTp()))\n        .map(Record::getValue)\n        .collect(Collectors.toList());\n\n    expectEmitter(backwardEmitter, expectedValues);\n  }\n\n  @Test\n  void backwardEmitterSeekToEnd() {\n    final int numMessages = 100;\n    final Map<TopicPartition, Long> targetOffsets = new HashMap<>();\n    for (int i = 0; i < PARTITIONS; i++) {\n      targetOffsets.put(new TopicPartition(TOPIC, i), (long) MSGS_PER_PARTITION);\n    }\n\n    var backwardEmitter = new BackwardEmitter(\n        this::createConsumer,\n        new ConsumerPosition(OFFSET, TOPIC, targetOffsets),\n        numMessages,\n        RECORD_DESERIALIZER,\n        NOOP_FILTER,\n        PollingSettings.createDefault()\n    );\n\n    var expectedValues = SENT_RECORDS.stream()\n        .filter(r -> r.getOffset() < targetOffsets.get(r.getTp()))\n        .filter(r -> r.getOffset() >= (targetOffsets.get(r.getTp()) - (numMessages / PARTITIONS)))\n        .map(Record::getValue)\n        .collect(Collectors.toList());\n\n    assertThat(expectedValues).size().isEqualTo(numMessages);\n\n    expectEmitter(backwardEmitter, expectedValues);\n  }\n\n  @Test\n  void backwardEmitterSeekToBegin() {\n    Map<TopicPartition, Long> offsets = new HashMap<>();\n    for (int i = 0; i < PARTITIONS; i++) {\n      offsets.put(new TopicPartition(TOPIC, i), 0L);\n    }\n\n    var backwardEmitter = new BackwardEmitter(\n        this::createConsumer,\n        new ConsumerPosition(OFFSET, TOPIC, offsets),\n        100,\n        RECORD_DESERIALIZER,\n        NOOP_FILTER,\n        PollingSettings.createDefault()\n    );\n\n    expectEmitter(backwardEmitter,\n        100,\n        e -> e.expectNextCount(0),\n        StepVerifier.Assertions::hasNotDroppedElements\n    );\n  }\n\n  private void expectEmitter(Consumer<FluxSink<TopicMessageEventDTO>> emitter, List<String> expectedValues) {\n    expectEmitter(emitter,\n        expectedValues.size(),\n        e -> e.recordWith(ArrayList::new)\n            .expectNextCount(expectedValues.size())\n            .expectRecordedMatches(r -> r.containsAll(expectedValues))\n            .consumeRecordedWith(r -> log.info(\"Collected collection: {}\", r)),\n        v -> {\n        }\n    );\n  }\n\n  private void expectEmitter(\n      Consumer<FluxSink<TopicMessageEventDTO>> emitter,\n      int take,\n      Function<StepVerifier.Step<String>, StepVerifier.Step<String>> stepConsumer,\n      Consumer<StepVerifier.Assertions> assertionsConsumer) {\n\n    StepVerifier.FirstStep<String> firstStep = StepVerifier.create(\n        Flux.create(emitter)\n            .filter(m -> m.getType().equals(TopicMessageEventDTO.TypeEnum.MESSAGE))\n            .take(take)\n            .map(m -> m.getMessage().getContent())\n    );\n\n    StepVerifier.Step<String> step = stepConsumer.apply(firstStep);\n    assertionsConsumer.accept(step.expectComplete().verifyThenAssertThat());\n  }\n\n  private EnhancedConsumer createConsumer() {\n    return createConsumer(Map.of());\n  }\n\n  private EnhancedConsumer createConsumer(Map<String, Object> properties) {\n    final Map<String, ? extends Serializable> map = Map.of(\n        ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers(),\n        ConsumerConfig.GROUP_ID_CONFIG, UUID.randomUUID().toString(),\n        ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 19 // to check multiple polls\n    );\n    Properties props = new Properties();\n    props.putAll(map);\n    props.putAll(properties);\n    return new EnhancedConsumer(props, PollingThrottler.noop(), ApplicationMetrics.noop());\n  }\n\n  @Value\n  static class Record {\n    String value;\n    TopicPartition tp;\n    long offset;\n    long timestamp;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/SchemaRegistryPaginationTest.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.anyList;\nimport static org.mockito.ArgumentMatchers.isA;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport com.provectus.kafka.ui.controller.SchemasController;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.SchemaSubjectDTO;\nimport com.provectus.kafka.ui.service.audit.AuditService;\nimport com.provectus.kafka.ui.sr.model.Compatibility;\nimport com.provectus.kafka.ui.sr.model.SchemaSubject;\nimport com.provectus.kafka.ui.util.AccessControlServiceMock;\nimport com.provectus.kafka.ui.util.ReactiveFailover;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.stream.IntStream;\nimport org.junit.jupiter.api.Test;\nimport reactor.core.publisher.Mono;\n\npublic class SchemaRegistryPaginationTest {\n\n  private static final String LOCAL_KAFKA_CLUSTER_NAME = \"local\";\n\n  private SchemasController controller;\n\n  private void init(List<String> subjects) {\n    ClustersStorage clustersStorage = mock(ClustersStorage.class);\n    when(clustersStorage.getClusterByName(isA(String.class)))\n        .thenReturn(Optional.of(buildKafkaCluster(LOCAL_KAFKA_CLUSTER_NAME)));\n\n    SchemaRegistryService schemaRegistryService = mock(SchemaRegistryService.class);\n    when(schemaRegistryService.getAllSubjectNames(isA(KafkaCluster.class)))\n                .thenReturn(Mono.just(subjects));\n    when(schemaRegistryService\n            .getAllLatestVersionSchemas(isA(KafkaCluster.class), anyList())).thenCallRealMethod();\n    when(schemaRegistryService.getLatestSchemaVersionBySubject(isA(KafkaCluster.class), isA(String.class)))\n            .thenAnswer(a -> Mono.just(\n                new SchemaRegistryService.SubjectWithCompatibilityLevel(\n                    new SchemaSubject().subject(a.getArgument(1)), Compatibility.FULL)));\n\n    this.controller = new SchemasController(schemaRegistryService);\n    this.controller.setAccessControlService(new AccessControlServiceMock().getMock());\n    this.controller.setAuditService(mock(AuditService.class));\n    this.controller.setClustersStorage(clustersStorage);\n  }\n\n  @Test\n  void shouldListFirst25andThen10Schemas() {\n    init(\n            IntStream.rangeClosed(1, 100)\n                    .boxed()\n                    .map(num -> \"subject\" + num)\n                    .toList()\n    );\n    var schemasFirst25 = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME,\n            null, null, null, null).block();\n    assertThat(schemasFirst25.getBody().getPageCount()).isEqualTo(4);\n    assertThat(schemasFirst25.getBody().getSchemas()).hasSize(25);\n    assertThat(schemasFirst25.getBody().getSchemas())\n            .isSortedAccordingTo(Comparator.comparing(SchemaSubjectDTO::getSubject));\n\n    var schemasFirst10 = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME,\n            null, 10, null, null).block();\n\n    assertThat(schemasFirst10.getBody().getPageCount()).isEqualTo(10);\n    assertThat(schemasFirst10.getBody().getSchemas()).hasSize(10);\n    assertThat(schemasFirst10.getBody().getSchemas())\n            .isSortedAccordingTo(Comparator.comparing(SchemaSubjectDTO::getSubject));\n  }\n\n  @Test\n  void shouldListSchemasContaining_1() {\n    init(\n              IntStream.rangeClosed(1, 100)\n                      .boxed()\n                      .map(num -> \"subject\" + num)\n                      .toList()\n    );\n    var schemasSearch7 = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME,\n            null, null, \"1\", null).block();\n    assertThat(schemasSearch7.getBody().getPageCount()).isEqualTo(1);\n    assertThat(schemasSearch7.getBody().getSchemas()).hasSize(20);\n  }\n\n  @Test\n  void shouldCorrectlyHandleNonPositivePageNumberAndPageSize() {\n    init(\n                IntStream.rangeClosed(1, 100)\n                        .boxed()\n                        .map(num -> \"subject\" + num)\n                        .toList()\n    );\n    var schemas = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME,\n            0, -1, null, null).block();\n\n    assertThat(schemas.getBody().getPageCount()).isEqualTo(4);\n    assertThat(schemas.getBody().getSchemas()).hasSize(25);\n    assertThat(schemas.getBody().getSchemas()).isSortedAccordingTo(Comparator.comparing(SchemaSubjectDTO::getSubject));\n  }\n\n  @Test\n  void shouldCalculateCorrectPageCountForNonDivisiblePageSize() {\n    init(\n                IntStream.rangeClosed(1, 100)\n                        .boxed()\n                        .map(num -> \"subject\" + num)\n                        .toList()\n    );\n\n    var schemas = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME,\n            4, 33, null, null).block();\n\n    assertThat(schemas.getBody().getPageCount()).isEqualTo(4);\n    assertThat(schemas.getBody().getSchemas()).hasSize(1);\n    assertThat(schemas.getBody().getSchemas().get(0).getSubject()).isEqualTo(\"subject99\");\n  }\n\n  private KafkaCluster buildKafkaCluster(String clusterName) {\n    return KafkaCluster.builder()\n            .name(clusterName)\n            .schemaRegistryClient(mock(ReactiveFailover.class))\n            .build();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/SendAndReadTests.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.provectus.kafka.ui.AbstractIntegrationTest;\nimport com.provectus.kafka.ui.model.ConsumerPosition;\nimport com.provectus.kafka.ui.model.CreateTopicMessageDTO;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.SeekDirectionDTO;\nimport com.provectus.kafka.ui.model.SeekTypeDTO;\nimport com.provectus.kafka.ui.model.TopicMessageDTO;\nimport com.provectus.kafka.ui.model.TopicMessageEventDTO;\nimport com.provectus.kafka.ui.serdes.builtin.Int32Serde;\nimport com.provectus.kafka.ui.serdes.builtin.Int64Serde;\nimport com.provectus.kafka.ui.serdes.builtin.StringSerde;\nimport com.provectus.kafka.ui.serdes.builtin.sr.SchemaRegistrySerde;\nimport io.confluent.kafka.schemaregistry.ParsedSchema;\nimport io.confluent.kafka.schemaregistry.avro.AvroSchema;\nimport io.confluent.kafka.schemaregistry.json.JsonSchema;\nimport io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema;\nimport java.time.Duration;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.UUID;\nimport java.util.function.Consumer;\nimport lombok.SneakyThrows;\nimport org.apache.kafka.clients.admin.NewTopic;\nimport org.apache.kafka.common.TopicPartition;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport reactor.test.StepVerifier;\n\npublic class SendAndReadTests extends AbstractIntegrationTest {\n\n  private static final AvroSchema AVRO_SCHEMA_1 = new AvroSchema(\n      \"{\"\n          + \"  \\\"type\\\": \\\"record\\\",\"\n          + \"  \\\"name\\\": \\\"TestAvroRecord1\\\",\"\n          + \"  \\\"fields\\\": [\"\n          + \"    {\"\n          + \"      \\\"name\\\": \\\"field1\\\",\"\n          + \"      \\\"type\\\": \\\"string\\\"\"\n          + \"    },\"\n          + \"    {\"\n          + \"      \\\"name\\\": \\\"field2\\\",\"\n          + \"      \\\"type\\\": \\\"int\\\"\"\n          + \"    }\"\n          + \"  ]\"\n          + \"}\"\n  );\n\n  private static final AvroSchema AVRO_SCHEMA_2 = new AvroSchema(\n      \"{\"\n          + \"  \\\"type\\\": \\\"record\\\",\"\n          + \"  \\\"name\\\": \\\"TestAvroRecord2\\\",\"\n          + \"  \\\"fields\\\": [\"\n          + \"    {\"\n          + \"      \\\"name\\\": \\\"f1\\\",\"\n          + \"      \\\"type\\\": \\\"int\\\"\"\n          + \"    },\"\n          + \"    {\"\n          + \"      \\\"name\\\": \\\"f2\\\",\"\n          + \"      \\\"type\\\": \\\"string\\\"\"\n          + \"    }\"\n          + \"  ]\"\n          + \"}\"\n  );\n\n  private static final AvroSchema AVRO_SCHEMA_PRIMITIVE_STRING =\n      new AvroSchema(\"{ \\\"type\\\": \\\"string\\\" }\");\n\n  private static final AvroSchema AVRO_SCHEMA_PRIMITIVE_INT =\n      new AvroSchema(\"{ \\\"type\\\": \\\"int\\\" }\");\n\n\n  private static final String AVRO_SCHEMA_1_JSON_RECORD\n      = \"{ \\\"field1\\\":\\\"testStr\\\", \\\"field2\\\": 123 }\";\n\n  private static final String AVRO_SCHEMA_2_JSON_RECORD = \"{ \\\"f1\\\": 111, \\\"f2\\\": \\\"testStr\\\" }\";\n\n  private static final ProtobufSchema PROTOBUF_SCHEMA = new ProtobufSchema(\n      \"syntax = \\\"proto3\\\";\\n\"\n          + \"package com.provectus;\\n\"\n          + \"\\n\"\n          + \"message TestProtoRecord {\\n\"\n          + \"  string f1 = 1;\\n\"\n          + \"  int32 f2 = 2;\\n\"\n          + \"}\\n\"\n          + \"\\n\"\n  );\n\n  private static final String PROTOBUF_SCHEMA_JSON_RECORD\n      = \"{ \\\"f1\\\" : \\\"test str\\\", \\\"f2\\\" : 123 }\";\n\n\n  private static final JsonSchema JSON_SCHEMA = new JsonSchema(\n      \"{ \"\n          + \"  \\\"$schema\\\": \\\"http://json-schema.org/draft-07/schema#\\\", \"\n          + \"  \\\"$id\\\": \\\"http://example.com/myURI.schema.json\\\", \"\n          + \"  \\\"title\\\": \\\"TestRecord\\\",\"\n          + \"  \\\"type\\\": \\\"object\\\",\"\n          + \"  \\\"additionalProperties\\\": false,\"\n          + \"  \\\"properties\\\": {\"\n          + \"    \\\"f1\\\": {\"\n          + \"      \\\"type\\\": \\\"integer\\\"\"\n          + \"    },\"\n          + \"    \\\"f2\\\": {\"\n          + \"      \\\"type\\\": \\\"string\\\"\"\n          + \"    },\"\n          // it is important special case since there is code in KafkaJsonSchemaSerializer\n          // that checks fields with this name (it should be worked around)\n          + \"    \\\"schema\\\": {\"\n          + \"      \\\"type\\\": \\\"string\\\"\"\n          + \"    }\"\n          + \"  }\"\n          + \"}\"\n  );\n\n  private static final String JSON_SCHEMA_RECORD\n      = \"{ \\\"f1\\\": 12, \\\"f2\\\": \\\"testJsonSchema1\\\", \\\"schema\\\": \\\"some txt\\\" }\";\n\n  private KafkaCluster targetCluster;\n\n  @Autowired\n  private MessagesService messagesService;\n\n  @Autowired\n  private ClustersStorage clustersStorage;\n\n  @BeforeEach\n  void init() {\n    targetCluster = clustersStorage.getClusterByName(LOCAL).get();\n  }\n\n  @Test\n  void noSchemaStringKeyStringValue() {\n    new SendAndReadSpec()\n        .withMsgToSend(\n            new CreateTopicMessageDTO()\n                .key(\"testKey\")\n                .keySerde(StringSerde.name())\n                .content(\"testValue\")\n                .valueSerde(StringSerde.name())\n        )\n        .doAssert(polled -> {\n          assertThat(polled.getKey()).isEqualTo(\"testKey\");\n          assertThat(polled.getContent()).isEqualTo(\"testValue\");\n        });\n  }\n\n  @Test\n  void keyIsIntValueIsLong() {\n    new SendAndReadSpec()\n        .withMsgToSend(\n            new CreateTopicMessageDTO()\n                .key(\"123\")\n                .keySerde(Int32Serde.name())\n                .content(\"21474836470\")\n                .valueSerde(Int64Serde.name())\n        )\n        .doAssert(polled -> {\n          assertThat(polled.getKey()).isEqualTo(\"123\");\n          assertThat(polled.getContent()).isEqualTo(\"21474836470\");\n        });\n  }\n\n  @Test\n  void keyIsNull() {\n    new SendAndReadSpec()\n        .withMsgToSend(\n            new CreateTopicMessageDTO()\n                .key(null)\n                .keySerde(StringSerde.name())\n                .content(\"testValue\")\n                .valueSerde(StringSerde.name())\n        )\n        .doAssert(polled -> {\n          assertThat(polled.getKey()).isNull();\n          assertThat(polled.getContent()).isEqualTo(\"testValue\");\n        });\n  }\n\n  @Test\n  void valueIsNull() {\n    new SendAndReadSpec()\n        .withMsgToSend(\n            new CreateTopicMessageDTO()\n                .key(\"testKey\")\n                .keySerde(StringSerde.name())\n                .content(null)\n                .valueSerde(StringSerde.name())\n        )\n        .doAssert(polled -> {\n          assertThat(polled.getKey()).isEqualTo(\"testKey\");\n          assertThat(polled.getContent()).isNull();\n        });\n  }\n\n  @Test\n  void primitiveAvroSchemas() {\n    new SendAndReadSpec()\n        .withKeySchema(AVRO_SCHEMA_PRIMITIVE_STRING)\n        .withValueSchema(AVRO_SCHEMA_PRIMITIVE_INT)\n        .withMsgToSend(\n            new CreateTopicMessageDTO()\n                .key(\"\\\"some string\\\"\")\n                .keySerde(SchemaRegistrySerde.name())\n                .content(\"123\")\n                .valueSerde(SchemaRegistrySerde.name())\n        )\n        .doAssert(polled -> {\n          assertThat(polled.getKey()).isEqualTo(\"\\\"some string\\\"\");\n          assertThat(polled.getContent()).isEqualTo(\"123\");\n        });\n  }\n\n  @Test\n  void recordAvroSchema() {\n    new SendAndReadSpec()\n        .withKeySchema(AVRO_SCHEMA_1)\n        .withValueSchema(AVRO_SCHEMA_2)\n        .withMsgToSend(\n            new CreateTopicMessageDTO()\n                .key(AVRO_SCHEMA_1_JSON_RECORD)\n                .keySerde(SchemaRegistrySerde.name())\n                .content(AVRO_SCHEMA_2_JSON_RECORD)\n                .valueSerde(SchemaRegistrySerde.name())\n        )\n        .doAssert(polled -> {\n          assertJsonEqual(polled.getKey(), AVRO_SCHEMA_1_JSON_RECORD);\n          assertJsonEqual(polled.getContent(), AVRO_SCHEMA_2_JSON_RECORD);\n        });\n  }\n\n  @Test\n  void keyWithNoSchemaValueWithProtoSchema() {\n    new SendAndReadSpec()\n        .withValueSchema(PROTOBUF_SCHEMA)\n        .withMsgToSend(\n            new CreateTopicMessageDTO()\n                .key(\"testKey\")\n                .keySerde(StringSerde.name())\n                .content(PROTOBUF_SCHEMA_JSON_RECORD)\n                .valueSerde(SchemaRegistrySerde.name())\n        )\n        .doAssert(polled -> {\n          assertThat(polled.getKey()).isEqualTo(\"testKey\");\n          assertJsonEqual(polled.getContent(), PROTOBUF_SCHEMA_JSON_RECORD);\n        });\n  }\n\n  @Test\n  void keyWithAvroSchemaValueWithAvroSchemaKeyIsNull() {\n    new SendAndReadSpec()\n        .withKeySchema(AVRO_SCHEMA_1)\n        .withValueSchema(AVRO_SCHEMA_2)\n        .withMsgToSend(\n            new CreateTopicMessageDTO()\n                .key(null)\n                .keySerde(SchemaRegistrySerde.name())\n                .content(AVRO_SCHEMA_2_JSON_RECORD)\n                .valueSerde(SchemaRegistrySerde.name())\n\n        )\n        .doAssert(polled -> {\n          assertThat(polled.getKey()).isNull();\n          assertJsonEqual(polled.getContent(), AVRO_SCHEMA_2_JSON_RECORD);\n        });\n  }\n\n  @Test\n  void valueWithAvroSchemaShouldThrowExceptionIfArgIsNotValidJsonObject() {\n    new SendAndReadSpec()\n        .withValueSchema(AVRO_SCHEMA_2)\n        .withMsgToSend(\n            new CreateTopicMessageDTO()\n                .keySerde(StringSerde.name())\n                // f2 has type int instead of string\n                .content(\"{ \\\"f1\\\": 111, \\\"f2\\\": 123 }\")\n                .valueSerde(SchemaRegistrySerde.name())\n        )\n        .assertSendThrowsException();\n  }\n\n  @Test\n  void keyWithAvroSchemaValueWithProtoSchema() {\n    new SendAndReadSpec()\n        .withKeySchema(AVRO_SCHEMA_1)\n        .withValueSchema(PROTOBUF_SCHEMA)\n        .withMsgToSend(\n            new CreateTopicMessageDTO()\n                .key(AVRO_SCHEMA_1_JSON_RECORD)\n                .keySerde(SchemaRegistrySerde.name())\n                .content(PROTOBUF_SCHEMA_JSON_RECORD)\n                .valueSerde(SchemaRegistrySerde.name())\n        )\n        .doAssert(polled -> {\n          assertJsonEqual(polled.getKey(), AVRO_SCHEMA_1_JSON_RECORD);\n          assertJsonEqual(polled.getContent(), PROTOBUF_SCHEMA_JSON_RECORD);\n        });\n  }\n\n  @Test\n  void valueWithProtoSchemaShouldThrowExceptionArgIsNotValidJsonObject() {\n    new SendAndReadSpec()\n        .withValueSchema(PROTOBUF_SCHEMA)\n        .withMsgToSend(\n            new CreateTopicMessageDTO()\n                .key(null)\n                .keySerde(StringSerde.name())\n                // f2 field has type object instead of int\n                .content(\"{ \\\"f1\\\" : \\\"test str\\\", \\\"f2\\\" : {} }\")\n                .valueSerde(SchemaRegistrySerde.name())\n        )\n        .assertSendThrowsException();\n  }\n\n  @Test\n  void keyWithProtoSchemaValueWithJsonSchema() {\n    new SendAndReadSpec()\n        .withKeySchema(PROTOBUF_SCHEMA)\n        .withValueSchema(JSON_SCHEMA)\n        .withMsgToSend(\n            new CreateTopicMessageDTO()\n                .key(PROTOBUF_SCHEMA_JSON_RECORD)\n                .keySerde(SchemaRegistrySerde.name())\n                .content(JSON_SCHEMA_RECORD)\n                .valueSerde(SchemaRegistrySerde.name())\n        )\n        .doAssert(polled -> {\n          assertJsonEqual(polled.getKey(), PROTOBUF_SCHEMA_JSON_RECORD);\n          assertJsonEqual(polled.getContent(), JSON_SCHEMA_RECORD);\n        });\n  }\n\n  @Test\n  void valueWithJsonSchemaThrowsExceptionIfArgIsNotValidJsonObject() {\n    new SendAndReadSpec()\n        .withValueSchema(JSON_SCHEMA)\n        .withMsgToSend(\n            new CreateTopicMessageDTO()\n                .key(null)\n                .keySerde(StringSerde.name())\n                // 'f2' field has has type object instead of string\n                .content(\"{ \\\"f1\\\": 12, \\\"f2\\\": {}, \\\"schema\\\": \\\"some txt\\\" }\")\n                .valueSerde(SchemaRegistrySerde.name())\n        )\n        .assertSendThrowsException();\n  }\n\n  @Test\n  void topicMessageMetadataAvro() {\n    new SendAndReadSpec()\n        .withKeySchema(AVRO_SCHEMA_1)\n        .withValueSchema(AVRO_SCHEMA_2)\n        .withMsgToSend(\n            new CreateTopicMessageDTO()\n                .key(AVRO_SCHEMA_1_JSON_RECORD)\n                .keySerde(SchemaRegistrySerde.name())\n                .content(AVRO_SCHEMA_2_JSON_RECORD)\n                .valueSerde(SchemaRegistrySerde.name())\n        )\n        .doAssert(polled -> {\n          assertJsonEqual(polled.getKey(), AVRO_SCHEMA_1_JSON_RECORD);\n          assertJsonEqual(polled.getContent(), AVRO_SCHEMA_2_JSON_RECORD);\n          assertThat(polled.getKeySize()).isEqualTo(15L);\n          assertThat(polled.getValueSize()).isEqualTo(15L);\n          assertThat(polled.getKeyDeserializeProperties().get(\"schemaId\")).isNotNull();\n          assertThat(polled.getValueDeserializeProperties().get(\"schemaId\")).isNotNull();\n          assertThat(polled.getKeyDeserializeProperties().get(\"type\")).isEqualTo(\"AVRO\");\n          assertThat(polled.getValueDeserializeProperties().get(\"schemaId\")).isNotNull();\n          assertThat(polled.getValueDeserializeProperties().get(\"type\")).isEqualTo(\"AVRO\");\n        });\n  }\n\n  @Test\n  void topicMessageMetadataProtobuf() {\n    new SendAndReadSpec()\n        .withKeySchema(PROTOBUF_SCHEMA)\n        .withValueSchema(PROTOBUF_SCHEMA)\n        .withMsgToSend(\n            new CreateTopicMessageDTO()\n                .key(PROTOBUF_SCHEMA_JSON_RECORD)\n                .keySerde(SchemaRegistrySerde.name())\n                .content(PROTOBUF_SCHEMA_JSON_RECORD)\n                .valueSerde(SchemaRegistrySerde.name())\n        )\n        .doAssert(polled -> {\n          assertJsonEqual(polled.getKey(), PROTOBUF_SCHEMA_JSON_RECORD);\n          assertJsonEqual(polled.getContent(), PROTOBUF_SCHEMA_JSON_RECORD);\n          assertThat(polled.getKeySize()).isEqualTo(18L);\n          assertThat(polled.getValueSize()).isEqualTo(18L);\n          assertThat(polled.getValueDeserializeProperties().get(\"schemaId\")).isNotNull();\n          assertThat(polled.getKeyDeserializeProperties().get(\"type\")).isEqualTo(\"PROTOBUF\");\n          assertThat(polled.getValueDeserializeProperties().get(\"schemaId\")).isNotNull();\n          assertThat(polled.getValueDeserializeProperties().get(\"type\")).isEqualTo(\"PROTOBUF\");\n        });\n  }\n\n  @Test\n  void topicMessageMetadataJson() {\n    new SendAndReadSpec()\n        .withKeySchema(JSON_SCHEMA)\n        .withValueSchema(JSON_SCHEMA)\n        .withMsgToSend(\n            new CreateTopicMessageDTO()\n                .key(JSON_SCHEMA_RECORD)\n                .keySerde(SchemaRegistrySerde.name())\n                .content(JSON_SCHEMA_RECORD)\n                .valueSerde(SchemaRegistrySerde.name())\n                .headers(Map.of(\"header1\", \"value1\"))\n        )\n        .doAssert(polled -> {\n          assertJsonEqual(polled.getKey(), JSON_SCHEMA_RECORD);\n          assertJsonEqual(polled.getContent(), JSON_SCHEMA_RECORD);\n          assertThat(polled.getKeySize()).isEqualTo(57L);\n          assertThat(polled.getValueSize()).isEqualTo(57L);\n          assertThat(polled.getHeadersSize()).isEqualTo(13L);\n          assertThat(polled.getValueDeserializeProperties().get(\"schemaId\")).isNotNull();\n          assertThat(polled.getKeyDeserializeProperties().get(\"type\")).isEqualTo(\"JSON\");\n          assertThat(polled.getValueDeserializeProperties().get(\"schemaId\")).isNotNull();\n          assertThat(polled.getValueDeserializeProperties().get(\"type\")).isEqualTo(\"JSON\");\n        });\n  }\n\n  @Test\n  void noKeyAndNoContentPresentTest() {\n    new SendAndReadSpec()\n        .withMsgToSend(\n            new CreateTopicMessageDTO()\n                .key(null)\n                .keySerde(StringSerde.name()) // any serde\n                .content(null)\n                .valueSerde(StringSerde.name()) // any serde\n        )\n        .doAssert(polled -> {\n          assertThat(polled.getKey()).isNull();\n          assertThat(polled.getContent()).isNull();\n        });\n  }\n\n  @SneakyThrows\n  private void assertJsonEqual(String actual, String expected) {\n    var mapper = new ObjectMapper();\n    assertThat(mapper.readTree(actual)).isEqualTo(mapper.readTree(expected));\n  }\n\n  class SendAndReadSpec {\n    CreateTopicMessageDTO msgToSend;\n    ParsedSchema keySchema;\n    ParsedSchema valueSchema;\n\n    public SendAndReadSpec withMsgToSend(CreateTopicMessageDTO msg) {\n      this.msgToSend = msg;\n      return this;\n    }\n\n    public SendAndReadSpec withKeySchema(ParsedSchema keyScheam) {\n      this.keySchema = keyScheam;\n      return this;\n    }\n\n    public SendAndReadSpec withValueSchema(ParsedSchema valueSchema) {\n      this.valueSchema = valueSchema;\n      return this;\n    }\n\n    @SneakyThrows\n    private String createTopicAndCreateSchemas() {\n      Objects.requireNonNull(msgToSend);\n      String topic = UUID.randomUUID().toString();\n      createTopic(new NewTopic(topic, 1, (short) 1));\n      if (keySchema != null) {\n        schemaRegistry.schemaRegistryClient().register(topic + \"-key\", keySchema);\n      }\n      if (valueSchema != null) {\n        schemaRegistry.schemaRegistryClient().register(topic + \"-value\", valueSchema);\n      }\n      return topic;\n    }\n\n    public void assertSendThrowsException() {\n      String topic = createTopicAndCreateSchemas();\n      try {\n        StepVerifier.create(\n            messagesService.sendMessage(targetCluster, topic, msgToSend)\n        ).expectError().verify();\n      } finally {\n        deleteTopic(topic);\n      }\n    }\n\n    @SneakyThrows\n    public void doAssert(Consumer<TopicMessageDTO> msgAssert) {\n      String topic = createTopicAndCreateSchemas();\n      try {\n        messagesService.sendMessage(targetCluster, topic, msgToSend).block();\n        TopicMessageDTO polled = messagesService.loadMessages(\n                targetCluster,\n                topic,\n                new ConsumerPosition(\n                    SeekTypeDTO.BEGINNING,\n                    topic,\n                    Map.of(new TopicPartition(topic, 0), 0L)\n                ),\n                null,\n                null,\n                1,\n                SeekDirectionDTO.FORWARD,\n                msgToSend.getKeySerde().get(),\n                msgToSend.getValueSerde().get()\n            ).filter(e -> e.getType().equals(TopicMessageEventDTO.TypeEnum.MESSAGE))\n            .map(TopicMessageEventDTO::getMessage)\n            .blockLast(Duration.ofSeconds(5000));\n\n        assertThat(polled).isNotNull();\n        assertThat(polled.getPartition()).isEqualTo(0);\n        assertThat(polled.getOffset()).isNotNull();\n        msgAssert.accept(polled);\n      } finally {\n        deleteTopic(topic);\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/TopicsServicePaginationTest.java",
    "content": "package com.provectus.kafka.ui.service;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.anyList;\nimport static org.mockito.ArgumentMatchers.isA;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport com.provectus.kafka.ui.controller.TopicsController;\nimport com.provectus.kafka.ui.mapper.ClusterMapper;\nimport com.provectus.kafka.ui.mapper.ClusterMapperImpl;\nimport com.provectus.kafka.ui.model.InternalLogDirStats;\nimport com.provectus.kafka.ui.model.InternalPartitionsOffsets;\nimport com.provectus.kafka.ui.model.InternalTopic;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.Metrics;\nimport com.provectus.kafka.ui.model.SortOrderDTO;\nimport com.provectus.kafka.ui.model.TopicColumnsToSortDTO;\nimport com.provectus.kafka.ui.model.TopicDTO;\nimport com.provectus.kafka.ui.service.analyze.TopicAnalysisService;\nimport com.provectus.kafka.ui.service.audit.AuditService;\nimport com.provectus.kafka.ui.service.rbac.AccessControlService;\nimport com.provectus.kafka.ui.util.AccessControlServiceMock;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.UUID;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport org.apache.kafka.clients.admin.TopicDescription;\nimport org.apache.kafka.common.TopicPartitionInfo;\nimport org.junit.jupiter.api.Test;\nimport reactor.core.publisher.Mono;\n\nclass TopicsServicePaginationTest {\n\n  private static final String LOCAL_KAFKA_CLUSTER_NAME = \"local\";\n\n  private final TopicsService topicsService = mock(TopicsService.class);\n  private final ClustersStorage clustersStorage = mock(ClustersStorage.class);\n  private final ClusterMapper clusterMapper = new ClusterMapperImpl();\n  private final AccessControlService accessControlService = new AccessControlServiceMock().getMock();\n\n  private final TopicsController topicsController =\n      new TopicsController(topicsService, mock(TopicAnalysisService.class), clusterMapper);\n\n  private void init(Map<String, InternalTopic> topicsInCache) {\n\n    when(clustersStorage.getClusterByName(isA(String.class)))\n        .thenReturn(Optional.of(buildKafkaCluster(LOCAL_KAFKA_CLUSTER_NAME)));\n    when(topicsService.getTopicsForPagination(isA(KafkaCluster.class)))\n        .thenReturn(Mono.just(new ArrayList<>(topicsInCache.values())));\n    when(topicsService.loadTopics(isA(KafkaCluster.class), anyList()))\n        .thenAnswer(a -> {\n          List<String> lst = a.getArgument(1);\n          return Mono.just(lst.stream().map(topicsInCache::get).collect(Collectors.toList()));\n        });\n    topicsController.setAccessControlService(accessControlService);\n    topicsController.setAuditService(mock(AuditService.class));\n    topicsController.setClustersStorage(clustersStorage);\n  }\n\n  @Test\n  public void shouldListFirst25Topics() {\n    init(\n        IntStream.rangeClosed(1, 100).boxed()\n            .map(Objects::toString)\n            .map(name -> new TopicDescription(name, false, List.of()))\n            .map(topicDescription -> InternalTopic.from(topicDescription, List.of(), null,\n                Metrics.empty(), InternalLogDirStats.empty(), \"_\"))\n            .collect(Collectors.toMap(InternalTopic::getName, Function.identity()))\n    );\n\n    var topics = topicsController\n        .getTopics(LOCAL_KAFKA_CLUSTER_NAME, null, null, null, null,\n            null, null, null).block();\n\n    assertThat(topics.getBody().getPageCount()).isEqualTo(4);\n    assertThat(topics.getBody().getTopics()).hasSize(25);\n    assertThat(topics.getBody().getTopics())\n        .isSortedAccordingTo(Comparator.comparing(TopicDTO::getName));\n  }\n\n  private KafkaCluster buildKafkaCluster(String clusterName) {\n    return KafkaCluster.builder()\n        .name(clusterName)\n        .build();\n  }\n\n  @Test\n  public void shouldListFirst25TopicsSortedByNameDescendingOrder() {\n    var internalTopics = IntStream.rangeClosed(1, 100).boxed()\n        .map(Objects::toString)\n        .map(name -> new TopicDescription(name, false, List.of()))\n        .map(topicDescription -> InternalTopic.from(topicDescription, List.of(), null,\n            Metrics.empty(), InternalLogDirStats.empty(), \"_\"))\n        .collect(Collectors.toMap(InternalTopic::getName, Function.identity()));\n    init(internalTopics);\n\n    var topics = topicsController\n        .getTopics(LOCAL_KAFKA_CLUSTER_NAME, null, null, null, null,\n            TopicColumnsToSortDTO.NAME, SortOrderDTO.DESC, null).block();\n\n    assertThat(topics.getBody().getPageCount()).isEqualTo(4);\n    assertThat(topics.getBody().getTopics()).hasSize(25);\n    assertThat(topics.getBody().getTopics()).isSortedAccordingTo(Comparator.comparing(TopicDTO::getName).reversed());\n    assertThat(topics.getBody().getTopics()).containsExactlyElementsOf(\n        internalTopics.values().stream()\n            .map(clusterMapper::toTopic)\n            .sorted(Comparator.comparing(TopicDTO::getName).reversed())\n            .limit(25)\n            .collect(Collectors.toList())\n    );\n  }\n\n  @Test\n  public void shouldCalculateCorrectPageCountForNonDivisiblePageSize() {\n    init(\n        IntStream.rangeClosed(1, 100).boxed()\n            .map(Objects::toString)\n            .map(name -> new TopicDescription(name, false, List.of()))\n            .map(topicDescription -> InternalTopic.from(topicDescription, List.of(), null,\n                Metrics.empty(), InternalLogDirStats.empty(), \"_\"))\n            .collect(Collectors.toMap(InternalTopic::getName, Function.identity()))\n    );\n\n    var topics = topicsController\n        .getTopics(LOCAL_KAFKA_CLUSTER_NAME, 4, 33, null, null, null, null, null).block();\n\n    assertThat(topics.getBody().getPageCount()).isEqualTo(4);\n    assertThat(topics.getBody().getTopics()).hasSize(1);\n    assertThat(topics.getBody().getTopics().get(0).getName()).isEqualTo(\"99\");\n  }\n\n  @Test\n  public void shouldCorrectlyHandleNonPositivePageNumberAndPageSize() {\n    init(\n        IntStream.rangeClosed(1, 100).boxed()\n            .map(Objects::toString)\n            .map(name -> new TopicDescription(name, false, List.of()))\n            .map(topicDescription -> InternalTopic.from(topicDescription, List.of(), null,\n                Metrics.empty(), InternalLogDirStats.empty(), \"_\"))\n            .collect(Collectors.toMap(InternalTopic::getName, Function.identity()))\n    );\n\n    var topics = topicsController\n        .getTopics(LOCAL_KAFKA_CLUSTER_NAME, 0, -1, null, null, null, null, null).block();\n\n    assertThat(topics.getBody().getPageCount()).isEqualTo(4);\n    assertThat(topics.getBody().getTopics()).hasSize(25);\n    assertThat(topics.getBody().getTopics()).isSortedAccordingTo(Comparator.comparing(TopicDTO::getName));\n  }\n\n  @Test\n  public void shouldListBotInternalAndNonInternalTopics() {\n    init(\n        IntStream.rangeClosed(1, 100).boxed()\n            .map(Objects::toString)\n            .map(name -> new TopicDescription(name, Integer.parseInt(name) % 10 == 0, List.of()))\n            .map(topicDescription -> InternalTopic.from(topicDescription, List.of(), null,\n                Metrics.empty(), InternalLogDirStats.empty(), \"_\"))\n            .collect(Collectors.toMap(InternalTopic::getName, Function.identity()))\n    );\n\n    var topics = topicsController\n        .getTopics(LOCAL_KAFKA_CLUSTER_NAME, 0, -1, true, null,\n            null, null, null).block();\n\n    assertThat(topics.getBody().getPageCount()).isEqualTo(4);\n    assertThat(topics.getBody().getTopics()).hasSize(25);\n    assertThat(topics.getBody().getTopics()).isSortedAccordingTo(Comparator.comparing(TopicDTO::getName));\n  }\n\n  @Test\n  public void shouldListOnlyNonInternalTopics() {\n\n    init(\n        IntStream.rangeClosed(1, 100).boxed()\n            .map(Objects::toString)\n            .map(name -> new TopicDescription(name, Integer.parseInt(name) % 5 == 0, List.of()))\n            .map(topicDescription -> InternalTopic.from(topicDescription, List.of(), null,\n                Metrics.empty(), InternalLogDirStats.empty(), \"_\"))\n            .collect(Collectors.toMap(InternalTopic::getName, Function.identity()))\n    );\n\n    var topics = topicsController\n        .getTopics(LOCAL_KAFKA_CLUSTER_NAME, 4, -1, false, null,\n            null, null, null).block();\n\n    assertThat(topics.getBody().getPageCount()).isEqualTo(4);\n    assertThat(topics.getBody().getTopics()).hasSize(5);\n    assertThat(topics.getBody().getTopics()).isSortedAccordingTo(Comparator.comparing(TopicDTO::getName));\n  }\n\n  @Test\n  public void shouldListOnlyTopicsContainingOne() {\n\n    init(\n        IntStream.rangeClosed(1, 100).boxed()\n            .map(Objects::toString)\n            .map(name -> new TopicDescription(name, false, List.of()))\n            .map(topicDescription -> InternalTopic.from(topicDescription, List.of(), null,\n                Metrics.empty(), InternalLogDirStats.empty(), \"_\"))\n            .collect(Collectors.toMap(InternalTopic::getName, Function.identity()))\n    );\n\n    var topics = topicsController\n        .getTopics(LOCAL_KAFKA_CLUSTER_NAME, null, null, null, \"1\",\n            null, null, null).block();\n\n    assertThat(topics.getBody().getPageCount()).isEqualTo(1);\n    assertThat(topics.getBody().getTopics()).hasSize(20);\n    assertThat(topics.getBody().getTopics()).isSortedAccordingTo(Comparator.comparing(TopicDTO::getName));\n  }\n\n  @Test\n  public void shouldListTopicsOrderedByPartitionsCount() {\n    Map<String, InternalTopic> internalTopics = IntStream.rangeClosed(1, 100).boxed()\n        .map(i -> new TopicDescription(UUID.randomUUID().toString(), false,\n            IntStream.range(0, i)\n                .mapToObj(p ->\n                    new TopicPartitionInfo(p, null, List.of(), List.of()))\n                .collect(Collectors.toList())))\n        .map(topicDescription -> InternalTopic.from(topicDescription, List.of(), InternalPartitionsOffsets.empty(),\n            Metrics.empty(), InternalLogDirStats.empty(), \"_\"))\n        .collect(Collectors.toMap(InternalTopic::getName, Function.identity()));\n\n    init(internalTopics);\n\n    var topicsSortedAsc = topicsController\n        .getTopics(LOCAL_KAFKA_CLUSTER_NAME, null, null, null,\n            null, TopicColumnsToSortDTO.TOTAL_PARTITIONS, null, null).block();\n\n    assertThat(topicsSortedAsc.getBody().getPageCount()).isEqualTo(4);\n    assertThat(topicsSortedAsc.getBody().getTopics()).hasSize(25);\n    assertThat(topicsSortedAsc.getBody().getTopics()).containsExactlyElementsOf(\n        internalTopics.values().stream()\n            .map(clusterMapper::toTopic)\n            .sorted(Comparator.comparing(TopicDTO::getPartitionCount))\n            .limit(25)\n            .collect(Collectors.toList())\n    );\n\n    var topicsSortedDesc = topicsController\n        .getTopics(LOCAL_KAFKA_CLUSTER_NAME, null, null, null,\n            null, TopicColumnsToSortDTO.TOTAL_PARTITIONS, SortOrderDTO.DESC, null).block();\n\n    assertThat(topicsSortedDesc.getBody().getPageCount()).isEqualTo(4);\n    assertThat(topicsSortedDesc.getBody().getTopics()).hasSize(25);\n    assertThat(topicsSortedDesc.getBody().getTopics()).containsExactlyElementsOf(\n        internalTopics.values().stream()\n            .map(clusterMapper::toTopic)\n            .sorted(Comparator.comparing(TopicDTO::getPartitionCount).reversed())\n            .limit(25)\n            .collect(Collectors.toList())\n    );\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/acl/AclCsvTest.java",
    "content": "package com.provectus.kafka.ui.service.acl;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport java.util.Collection;\nimport java.util.List;\nimport org.apache.kafka.common.acl.AccessControlEntry;\nimport org.apache.kafka.common.acl.AclBinding;\nimport org.apache.kafka.common.acl.AclOperation;\nimport org.apache.kafka.common.acl.AclPermissionType;\nimport org.apache.kafka.common.resource.PatternType;\nimport org.apache.kafka.common.resource.ResourcePattern;\nimport org.apache.kafka.common.resource.ResourceType;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.ValueSource;\n\nclass AclCsvTest {\n\n  private static final List<AclBinding> TEST_BINDINGS = List.of(\n      new AclBinding(\n          new ResourcePattern(ResourceType.TOPIC, \"*\", PatternType.LITERAL),\n          new AccessControlEntry(\"User:test1\", \"*\", AclOperation.READ, AclPermissionType.ALLOW)),\n      new AclBinding(\n          new ResourcePattern(ResourceType.GROUP, \"group1\", PatternType.PREFIXED),\n          new AccessControlEntry(\"User:test2\", \"localhost\", AclOperation.DESCRIBE, AclPermissionType.DENY))\n  );\n\n  @ParameterizedTest\n  @ValueSource(strings = {\n      \"Principal,ResourceType, PatternType, ResourceName,Operation,PermissionType,Host\\n\"\n          + \"User:test1,TOPIC,LITERAL,*,READ,ALLOW,*\\n\"\n          + \"User:test2,GROUP,PREFIXED,group1,DESCRIBE,DENY,localhost\",\n\n      //without header\n      \"User:test1,TOPIC,LITERAL,*,READ,ALLOW,*\\n\"\n          + \"\\n\"\n          + \"User:test2,GROUP,PREFIXED,group1,DESCRIBE,DENY,localhost\"\n          + \"\\n\"\n  })\n  void parsesValidInputCsv(String csvString) {\n    Collection<AclBinding> parsed = AclCsv.parseCsv(csvString);\n    assertThat(parsed).containsExactlyInAnyOrderElementsOf(TEST_BINDINGS);\n  }\n\n  @ParameterizedTest\n  @ValueSource(strings = {\n      // columns > 7\n      \"User:test1,TOPIC,LITERAL,*,READ,ALLOW,*,1,2,3,4\",\n      // columns < 7\n      \"User:test1,TOPIC,LITERAL,*\",\n      // enum values are illegal\n      \"User:test1,ILLEGAL,LITERAL,*,READ,ALLOW,*\",\n      \"User:test1,TOPIC,LITERAL,*,READ,ILLEGAL,*\"\n  })\n  void throwsExceptionForInvalidInputCsv(String csvString) {\n    assertThatThrownBy(() -> AclCsv.parseCsv(csvString))\n        .isInstanceOf(ValidationException.class);\n  }\n\n  @Test\n  void transformAndParseUseSameFormat() {\n    String csv = AclCsv.transformToCsvString(TEST_BINDINGS);\n    Collection<AclBinding> parsedBindings = AclCsv.parseCsv(csv);\n    assertThat(parsedBindings).containsExactlyInAnyOrderElementsOf(TEST_BINDINGS);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/acl/AclsServiceTest.java",
    "content": "package com.provectus.kafka.ui.service.acl;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport com.provectus.kafka.ui.model.CreateConsumerAclDTO;\nimport com.provectus.kafka.ui.model.CreateProducerAclDTO;\nimport com.provectus.kafka.ui.model.CreateStreamAppAclDTO;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.service.AdminClientService;\nimport com.provectus.kafka.ui.service.ReactiveAdminClient;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.UUID;\nimport org.apache.kafka.common.acl.AccessControlEntry;\nimport org.apache.kafka.common.acl.AclBinding;\nimport org.apache.kafka.common.acl.AclOperation;\nimport org.apache.kafka.common.acl.AclPermissionType;\nimport org.apache.kafka.common.resource.PatternType;\nimport org.apache.kafka.common.resource.Resource;\nimport org.apache.kafka.common.resource.ResourcePattern;\nimport org.apache.kafka.common.resource.ResourcePatternFilter;\nimport org.apache.kafka.common.resource.ResourceType;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.ArgumentCaptor;\nimport reactor.core.publisher.Mono;\n\nclass AclsServiceTest {\n\n  private static final KafkaCluster CLUSTER = KafkaCluster.builder().build();\n\n  private final ReactiveAdminClient adminClientMock = mock(ReactiveAdminClient.class);\n  private final AdminClientService adminClientService = mock(AdminClientService.class);\n\n  private final AclsService aclsService = new AclsService(adminClientService);\n\n  @BeforeEach\n  void initMocks() {\n    when(adminClientService.get(CLUSTER)).thenReturn(Mono.just(adminClientMock));\n  }\n\n  @Test\n  void testSyncAclWithAclCsv() {\n    var existingBinding1 = new AclBinding(\n        new ResourcePattern(ResourceType.TOPIC, \"*\", PatternType.LITERAL),\n        new AccessControlEntry(\"User:test1\", \"*\", AclOperation.READ, AclPermissionType.ALLOW));\n\n    var existingBinding2 = new AclBinding(\n        new ResourcePattern(ResourceType.GROUP, \"group1\", PatternType.PREFIXED),\n        new AccessControlEntry(\"User:test2\", \"localhost\", AclOperation.DESCRIBE, AclPermissionType.DENY));\n\n    var newBindingToBeAdded = new AclBinding(\n        new ResourcePattern(ResourceType.GROUP, \"groupNew\", PatternType.PREFIXED),\n        new AccessControlEntry(\"User:test3\", \"localhost\", AclOperation.DESCRIBE, AclPermissionType.DENY));\n\n    when(adminClientMock.listAcls(ResourcePatternFilter.ANY))\n        .thenReturn(Mono.just(List.of(existingBinding1, existingBinding2)));\n\n    ArgumentCaptor<Collection<AclBinding>> createdCaptor = ArgumentCaptor.forClass(Collection.class);\n    when(adminClientMock.createAcls(createdCaptor.capture()))\n        .thenReturn(Mono.empty());\n\n    ArgumentCaptor<Collection<AclBinding>> deletedCaptor = ArgumentCaptor.forClass(Collection.class);\n    when(adminClientMock.deleteAcls(deletedCaptor.capture()))\n        .thenReturn(Mono.empty());\n\n    aclsService.syncAclWithAclCsv(\n        CLUSTER,\n        \"Principal,ResourceType, PatternType, ResourceName,Operation,PermissionType,Host\\n\"\n            + \"User:test1,TOPIC,LITERAL,*,READ,ALLOW,*\\n\"\n            + \"User:test3,GROUP,PREFIXED,groupNew,DESCRIBE,DENY,localhost\"\n    ).block();\n\n    Collection<AclBinding> createdBindings = createdCaptor.getValue();\n    assertThat(createdBindings)\n        .hasSize(1)\n        .contains(newBindingToBeAdded);\n\n    Collection<AclBinding> deletedBindings = deletedCaptor.getValue();\n    assertThat(deletedBindings)\n        .hasSize(1)\n        .contains(existingBinding2);\n  }\n\n\n  @Test\n  void createsConsumerDependantAcls() {\n    ArgumentCaptor<Collection<AclBinding>> createdCaptor = ArgumentCaptor.forClass(Collection.class);\n    when(adminClientMock.createAcls(createdCaptor.capture()))\n        .thenReturn(Mono.empty());\n\n    var principal = UUID.randomUUID().toString();\n    var host = UUID.randomUUID().toString();\n\n    aclsService.createConsumerAcl(\n        CLUSTER,\n        new CreateConsumerAclDTO()\n            .principal(principal)\n            .host(host)\n            .consumerGroups(List.of(\"cg1\", \"cg2\"))\n            .topics(List.of(\"t1\", \"t2\"))\n    ).block();\n\n    //Read, Describe on topics, Read on consumerGroups\n    Collection<AclBinding> createdBindings = createdCaptor.getValue();\n    assertThat(createdBindings)\n        .hasSize(6)\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.TOPIC, \"t1\", PatternType.LITERAL),\n            new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW)))\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.TOPIC, \"t1\", PatternType.LITERAL),\n            new AccessControlEntry(principal, host, AclOperation.DESCRIBE, AclPermissionType.ALLOW)))\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.TOPIC, \"t2\", PatternType.LITERAL),\n            new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW)))\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.TOPIC, \"t2\", PatternType.LITERAL),\n            new AccessControlEntry(principal, host, AclOperation.DESCRIBE, AclPermissionType.ALLOW)))\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.GROUP, \"cg1\", PatternType.LITERAL),\n            new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW)))\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.GROUP, \"cg2\", PatternType.LITERAL),\n            new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW)));\n  }\n\n  @Test\n  void createsConsumerDependantAclsWhenTopicsAndGroupsSpecifiedByPrefix() {\n    ArgumentCaptor<Collection<AclBinding>> createdCaptor = ArgumentCaptor.forClass(Collection.class);\n    when(adminClientMock.createAcls(createdCaptor.capture()))\n        .thenReturn(Mono.empty());\n\n    var principal = UUID.randomUUID().toString();\n    var host = UUID.randomUUID().toString();\n\n    aclsService.createConsumerAcl(\n        CLUSTER,\n        new CreateConsumerAclDTO()\n            .principal(principal)\n            .host(host)\n            .consumerGroupsPrefix(\"cgPref\")\n            .topicsPrefix(\"topicPref\")\n    ).block();\n\n    //Read, Describe on topics, Read on consumerGroups\n    Collection<AclBinding> createdBindings = createdCaptor.getValue();\n    assertThat(createdBindings)\n        .hasSize(3)\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.TOPIC, \"topicPref\", PatternType.PREFIXED),\n            new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW)))\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.TOPIC, \"topicPref\", PatternType.PREFIXED),\n            new AccessControlEntry(principal, host, AclOperation.DESCRIBE, AclPermissionType.ALLOW)))\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.GROUP, \"cgPref\", PatternType.PREFIXED),\n            new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW)));\n  }\n\n  @Test\n  void createsProducerDependantAcls() {\n    ArgumentCaptor<Collection<AclBinding>> createdCaptor = ArgumentCaptor.forClass(Collection.class);\n    when(adminClientMock.createAcls(createdCaptor.capture()))\n        .thenReturn(Mono.empty());\n\n    var principal = UUID.randomUUID().toString();\n    var host = UUID.randomUUID().toString();\n\n    aclsService.createProducerAcl(\n        CLUSTER,\n        new CreateProducerAclDTO()\n            .principal(principal)\n            .host(host)\n            .topics(List.of(\"t1\"))\n            .idempotent(true)\n            .transactionalId(\"txId1\")\n    ).block();\n\n    //Write, Describe, Create permission on topics, Write, Describe on transactionalIds\n    //IDEMPOTENT_WRITE on cluster if idempotent is enabled (true)\n    Collection<AclBinding> createdBindings = createdCaptor.getValue();\n    assertThat(createdBindings)\n        .hasSize(6)\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.TOPIC, \"t1\", PatternType.LITERAL),\n            new AccessControlEntry(principal, host, AclOperation.WRITE, AclPermissionType.ALLOW)))\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.TOPIC, \"t1\", PatternType.LITERAL),\n            new AccessControlEntry(principal, host, AclOperation.DESCRIBE, AclPermissionType.ALLOW)))\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.TOPIC, \"t1\", PatternType.LITERAL),\n            new AccessControlEntry(principal, host, AclOperation.CREATE, AclPermissionType.ALLOW)))\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.TRANSACTIONAL_ID, \"txId1\", PatternType.LITERAL),\n            new AccessControlEntry(principal, host, AclOperation.WRITE, AclPermissionType.ALLOW)))\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.TRANSACTIONAL_ID, \"txId1\", PatternType.LITERAL),\n            new AccessControlEntry(principal, host, AclOperation.DESCRIBE, AclPermissionType.ALLOW)))\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.CLUSTER, Resource.CLUSTER_NAME, PatternType.LITERAL),\n            new AccessControlEntry(principal, host, AclOperation.IDEMPOTENT_WRITE, AclPermissionType.ALLOW)));\n  }\n\n\n  @Test\n  void createsProducerDependantAclsWhenTopicsAndTxIdSpecifiedByPrefix() {\n    ArgumentCaptor<Collection<AclBinding>> createdCaptor = ArgumentCaptor.forClass(Collection.class);\n    when(adminClientMock.createAcls(createdCaptor.capture()))\n        .thenReturn(Mono.empty());\n\n    var principal = UUID.randomUUID().toString();\n    var host = UUID.randomUUID().toString();\n\n    aclsService.createProducerAcl(\n        CLUSTER,\n        new CreateProducerAclDTO()\n            .principal(principal)\n            .host(host)\n            .topicsPrefix(\"topicPref\")\n            .transactionsIdPrefix(\"txIdPref\")\n            .idempotent(false)\n    ).block();\n\n    //Write, Describe, Create permission on topics, Write, Describe on transactionalIds\n    //IDEMPOTENT_WRITE on cluster if idempotent is enabled (false)\n    Collection<AclBinding> createdBindings = createdCaptor.getValue();\n    assertThat(createdBindings)\n        .hasSize(5)\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.TOPIC, \"topicPref\", PatternType.PREFIXED),\n            new AccessControlEntry(principal, host, AclOperation.WRITE, AclPermissionType.ALLOW)))\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.TOPIC, \"topicPref\", PatternType.PREFIXED),\n            new AccessControlEntry(principal, host, AclOperation.DESCRIBE, AclPermissionType.ALLOW)))\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.TOPIC, \"topicPref\", PatternType.PREFIXED),\n            new AccessControlEntry(principal, host, AclOperation.CREATE, AclPermissionType.ALLOW)))\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.TRANSACTIONAL_ID, \"txIdPref\", PatternType.PREFIXED),\n            new AccessControlEntry(principal, host, AclOperation.WRITE, AclPermissionType.ALLOW)))\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.TRANSACTIONAL_ID, \"txIdPref\", PatternType.PREFIXED),\n            new AccessControlEntry(principal, host, AclOperation.DESCRIBE, AclPermissionType.ALLOW)));\n  }\n\n\n  @Test\n  void createsStreamAppDependantAcls() {\n    ArgumentCaptor<Collection<AclBinding>> createdCaptor = ArgumentCaptor.forClass(Collection.class);\n    when(adminClientMock.createAcls(createdCaptor.capture()))\n        .thenReturn(Mono.empty());\n\n    var principal = UUID.randomUUID().toString();\n    var host = UUID.randomUUID().toString();\n\n    aclsService.createStreamAppAcl(\n        CLUSTER,\n        new CreateStreamAppAclDTO()\n            .principal(principal)\n            .host(host)\n            .inputTopics(List.of(\"t1\"))\n            .outputTopics(List.of(\"t2\", \"t3\"))\n            .applicationId(\"appId1\")\n    ).block();\n\n    // Read on input topics, Write on output topics\n    // ALL on applicationId-prefixed Groups and Topics\n    Collection<AclBinding> createdBindings = createdCaptor.getValue();\n    assertThat(createdBindings)\n        .hasSize(5)\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.TOPIC, \"t1\", PatternType.LITERAL),\n            new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW)))\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.TOPIC, \"t2\", PatternType.LITERAL),\n            new AccessControlEntry(principal, host, AclOperation.WRITE, AclPermissionType.ALLOW)))\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.TOPIC, \"t3\", PatternType.LITERAL),\n            new AccessControlEntry(principal, host, AclOperation.WRITE, AclPermissionType.ALLOW)))\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.GROUP, \"appId1\", PatternType.PREFIXED),\n            new AccessControlEntry(principal, host, AclOperation.ALL, AclPermissionType.ALLOW)))\n        .contains(new AclBinding(\n            new ResourcePattern(ResourceType.TOPIC, \"appId1\", PatternType.PREFIXED),\n            new AccessControlEntry(principal, host, AclOperation.ALL, AclPermissionType.ALLOW)));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisServiceTest.java",
    "content": "package com.provectus.kafka.ui.service.analyze;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.provectus.kafka.ui.AbstractIntegrationTest;\nimport com.provectus.kafka.ui.producer.KafkaTestProducer;\nimport com.provectus.kafka.ui.service.ClustersStorage;\nimport java.time.Duration;\nimport java.util.UUID;\nimport org.apache.commons.lang3.RandomStringUtils;\nimport org.apache.kafka.clients.admin.NewTopic;\nimport org.apache.kafka.clients.producer.ProducerRecord;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.testcontainers.shaded.org.awaitility.Awaitility;\n\n\nclass TopicAnalysisServiceTest extends AbstractIntegrationTest {\n\n  @Autowired\n  private ClustersStorage clustersStorage;\n\n  @Autowired\n  private TopicAnalysisService topicAnalysisService;\n\n  @Test\n  void savesResultWhenAnalysisIsCompleted() {\n    String topic = \"analyze_test_\" + UUID.randomUUID();\n    createTopic(new NewTopic(topic, 2, (short) 1));\n    fillTopic(topic, 1_000);\n\n    var cluster = clustersStorage.getClusterByName(LOCAL).get();\n    topicAnalysisService.analyze(cluster, topic).block();\n\n    Awaitility.await()\n        .atMost(Duration.ofSeconds(20))\n        .untilAsserted(() -> {\n          assertThat(topicAnalysisService.getTopicAnalysis(cluster, topic))\n              .hasValueSatisfying(state -> {\n                assertThat(state.getProgress()).isNull();\n                assertThat(state.getResult()).isNotNull();\n                var completedAnalyze = state.getResult();\n                assertThat(completedAnalyze.getTotalStats().getTotalMsgs()).isEqualTo(1_000);\n                assertThat(completedAnalyze.getPartitionStats().size()).isEqualTo(2);\n              });\n        });\n  }\n\n  private void fillTopic(String topic, int cnt) {\n    try (var producer = KafkaTestProducer.forKafka(kafka)) {\n      for (int i = 0; i < cnt; i++) {\n        producer.send(\n            new ProducerRecord<>(\n                topic,\n                RandomStringUtils.randomAlphabetic(5),\n                RandomStringUtils.randomAlphabetic(10)));\n      }\n    }\n  }\n\n\n}"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/audit/AuditIntegrationTest.java",
    "content": "package com.provectus.kafka.ui.service.audit;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport com.provectus.kafka.ui.AbstractIntegrationTest;\nimport com.provectus.kafka.ui.model.TopicCreationDTO;\nimport com.provectus.kafka.ui.model.rbac.Resource;\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Properties;\nimport java.util.UUID;\nimport org.apache.kafka.clients.consumer.ConsumerConfig;\nimport org.apache.kafka.clients.consumer.KafkaConsumer;\nimport org.apache.kafka.common.serialization.BytesDeserializer;\nimport org.apache.kafka.common.serialization.StringDeserializer;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.testcontainers.shaded.org.awaitility.Awaitility;\n\npublic class AuditIntegrationTest extends AbstractIntegrationTest {\n\n  @Autowired\n  private WebTestClient webTestClient;\n\n  @Test\n  void auditRecordWrittenIntoKafkaWhenNewTopicCreated() {\n    String newTopicName = \"test_audit_\" + UUID.randomUUID();\n\n    webTestClient.post()\n        .uri(\"/api/clusters/{clusterName}/topics\", LOCAL)\n        .bodyValue(\n            new TopicCreationDTO()\n                .replicationFactor(1)\n                .partitions(1)\n                .name(newTopicName)\n        )\n        .exchange()\n        .expectStatus()\n        .isOk();\n\n    try (var consumer = createConsumer()) {\n      var jsonMapper = new JsonMapper();\n      consumer.subscribe(List.of(\"__kui-audit-log\"));\n      Awaitility.await()\n          .pollInSameThread()\n          .atMost(Duration.ofSeconds(15))\n          .untilAsserted(() -> {\n            var polled = consumer.poll(Duration.ofSeconds(1));\n            assertThat(polled).anySatisfy(kafkaRecord -> {\n              try {\n                AuditRecord record = jsonMapper.readValue(kafkaRecord.value(), AuditRecord.class);\n                assertThat(record.operation()).isEqualTo(\"createTopic\");\n                assertThat(record.resources()).map(AuditRecord.AuditResource::type).contains(Resource.TOPIC);\n                assertThat(record.result().success()).isTrue();\n                assertThat(record.timestamp()).isNotBlank();\n                assertThat(record.clusterName()).isEqualTo(LOCAL);\n                assertThat(record.operationParams())\n                    .isEqualTo(Map.of(\n                        \"name\", newTopicName,\n                        \"partitions\", 1,\n                        \"replicationFactor\", 1,\n                        \"configs\", Map.of()\n                    ));\n              } catch (JsonProcessingException e) {\n                Assertions.fail();\n              }\n            });\n          });\n    }\n  }\n\n  private KafkaConsumer<?, String> createConsumer() {\n    Properties props = new Properties();\n    props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());\n    props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, BytesDeserializer.class);\n    props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);\n    props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, \"earliest\");\n    props.put(ConsumerConfig.GROUP_ID_CONFIG, AuditIntegrationTest.class.getName());\n    return new KafkaConsumer<>(props);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/audit/AuditServiceTest.java",
    "content": "package com.provectus.kafka.ui.service.audit;\n\nimport static com.provectus.kafka.ui.service.audit.AuditService.createAuditWriter;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.anyInt;\nimport static org.mockito.Mockito.anyMap;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.rbac.AccessContext;\nimport com.provectus.kafka.ui.service.ReactiveAdminClient;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.Supplier;\nimport org.apache.kafka.clients.producer.KafkaProducer;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport reactor.core.publisher.Mono;\nimport reactor.core.publisher.Signal;\n\nclass AuditServiceTest {\n\n  @Test\n  void isAuditTopicChecksIfAuditIsEnabledForCluster() {\n    Map<String, AuditWriter> writers = Map.of(\n        \"c1\", new AuditWriter(\"с1\", true, \"c1topic\", null, null),\n        \"c2\", new AuditWriter(\"c2\", false, \"c2topic\", mock(KafkaProducer.class), null)\n    );\n\n    var auditService = new AuditService(writers);\n    assertThat(auditService.isAuditTopic(KafkaCluster.builder().name(\"notExist\").build(), \"some\"))\n        .isFalse();\n    assertThat(auditService.isAuditTopic(KafkaCluster.builder().name(\"c1\").build(), \"c1topic\"))\n        .isFalse();\n    assertThat(auditService.isAuditTopic(KafkaCluster.builder().name(\"c2\").build(), \"c2topic\"))\n        .isTrue();\n  }\n\n  @Test\n  void auditCallsWriterMethodDependingOnSignal() {\n    var auditWriter = mock(AuditWriter.class);\n    var auditService = new AuditService(Map.of(\"test\", auditWriter));\n\n    var cxt = AccessContext.builder().cluster(\"test\").build();\n\n    auditService.audit(cxt, Signal.complete());\n    verify(auditWriter).write(any(), any(), eq(null));\n\n    var th = new Exception(\"testError\");\n    auditService.audit(cxt, Signal.error(th));\n    verify(auditWriter).write(any(), any(), eq(th));\n  }\n\n  @Nested\n  class CreateAuditWriter {\n\n    private final ReactiveAdminClient adminClientMock = mock(ReactiveAdminClient.class);\n    private final Supplier<KafkaProducer<byte[], byte[]>> producerSupplierMock = mock(Supplier.class);\n\n    private final ClustersProperties.Cluster clustersProperties = new ClustersProperties.Cluster();\n\n    private final KafkaCluster cluster = KafkaCluster\n        .builder()\n        .name(\"test\")\n        .originalProperties(clustersProperties)\n        .build();\n\n\n    @BeforeEach\n    void init() {\n      when(producerSupplierMock.get())\n          .thenReturn(mock(KafkaProducer.class));\n    }\n\n    @Test\n    void logOnlyAlterOpsByDefault() {\n      var auditProps = new ClustersProperties.AuditProperties();\n      auditProps.setConsoleAuditEnabled(true);\n      clustersProperties.setAudit(auditProps);\n\n      var maybeWriter = createAuditWriter(cluster, () -> adminClientMock, producerSupplierMock);\n      assertThat(maybeWriter)\n          .hasValueSatisfying(w -> assertThat(w.logAlterOperationsOnly()).isTrue());\n    }\n\n    @Test\n    void noWriterIfNoAuditPropsSet() {\n      var maybeWriter = createAuditWriter(cluster, () -> adminClientMock, producerSupplierMock);\n      assertThat(maybeWriter).isEmpty();\n    }\n\n    @Test\n    void setsLoggerIfConsoleLoggingEnabled() {\n      var auditProps = new ClustersProperties.AuditProperties();\n      auditProps.setConsoleAuditEnabled(true);\n      clustersProperties.setAudit(auditProps);\n\n      var maybeWriter = createAuditWriter(cluster, () -> adminClientMock, producerSupplierMock);\n      assertThat(maybeWriter).isPresent();\n\n      var writer = maybeWriter.get();\n      assertThat(writer.consoleLogger()).isNotNull();\n    }\n\n    @Nested\n    class WhenTopicAuditEnabled {\n\n      @BeforeEach\n      void setTopicWriteProperties() {\n        var auditProps = new ClustersProperties.AuditProperties();\n        auditProps.setTopicAuditEnabled(true);\n        auditProps.setTopic(\"test_audit_topic\");\n        auditProps.setAuditTopicsPartitions(3);\n        auditProps.setAuditTopicProperties(Map.of(\"p1\", \"v1\"));\n        clustersProperties.setAudit(auditProps);\n      }\n\n      @Test\n      void createsProducerIfTopicExists() {\n        when(adminClientMock.listTopics(true))\n            .thenReturn(Mono.just(Set.of(\"test_audit_topic\")));\n\n        var maybeWriter = createAuditWriter(cluster, () -> adminClientMock, producerSupplierMock);\n        assertThat(maybeWriter).isPresent();\n\n        //checking there was no topic creation request\n        verify(adminClientMock, times(0))\n            .createTopic(any(), anyInt(), anyInt(), anyMap());\n\n        var writer = maybeWriter.get();\n        assertThat(writer.producer()).isNotNull();\n        assertThat(writer.targetTopic()).isEqualTo(\"test_audit_topic\");\n      }\n\n      @Test\n      void createsProducerAndTopicIfItIsNotExist() {\n        when(adminClientMock.listTopics(true))\n            .thenReturn(Mono.just(Set.of()));\n\n        when(adminClientMock.createTopic(eq(\"test_audit_topic\"), eq(3), eq(null), anyMap()))\n            .thenReturn(Mono.empty());\n\n        var maybeWriter = createAuditWriter(cluster, () -> adminClientMock, producerSupplierMock);\n        assertThat(maybeWriter).isPresent();\n\n        //verifying topic created\n        verify(adminClientMock).createTopic(eq(\"test_audit_topic\"), eq(3), eq(null), anyMap());\n\n        var writer = maybeWriter.get();\n        assertThat(writer.producer()).isNotNull();\n        assertThat(writer.targetTopic()).isEqualTo(\"test_audit_topic\");\n      }\n\n    }\n  }\n\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/audit/AuditWriterTest.java",
    "content": "package com.provectus.kafka.ui.service.audit;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoInteractions;\n\nimport com.provectus.kafka.ui.config.auth.AuthenticatedUser;\nimport com.provectus.kafka.ui.model.rbac.AccessContext;\nimport com.provectus.kafka.ui.model.rbac.AccessContext.AccessContextBuilder;\nimport com.provectus.kafka.ui.model.rbac.permission.AclAction;\nimport com.provectus.kafka.ui.model.rbac.permission.ClusterConfigAction;\nimport com.provectus.kafka.ui.model.rbac.permission.ConnectAction;\nimport com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction;\nimport com.provectus.kafka.ui.model.rbac.permission.SchemaAction;\nimport com.provectus.kafka.ui.model.rbac.permission.TopicAction;\nimport java.util.List;\nimport java.util.function.UnaryOperator;\nimport java.util.stream.Stream;\nimport org.apache.kafka.clients.producer.KafkaProducer;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.mockito.Mockito;\nimport org.slf4j.Logger;\n\nclass AuditWriterTest {\n\n  final KafkaProducer<byte[], byte[]> producerMock = Mockito.mock(KafkaProducer.class);\n  final Logger loggerMock = Mockito.mock(Logger.class);\n  final AuthenticatedUser user = new AuthenticatedUser(\"someone\", List.of());\n\n  @Nested\n  class AlterOperationsOnlyWriter {\n\n    final AuditWriter alterOnlyWriter = new AuditWriter(\"test\", true, \"test-topic\", producerMock, loggerMock);\n\n    @ParameterizedTest\n    @MethodSource\n    void onlyLogsWhenAlterOperationIsPresentForOneOfResources(AccessContext ctxWithAlterOperation) {\n      alterOnlyWriter.write(ctxWithAlterOperation, user, null);\n      verify(producerMock).send(any(), any());\n      verify(loggerMock).info(any());\n    }\n\n    static Stream<AccessContext> onlyLogsWhenAlterOperationIsPresentForOneOfResources() {\n      Stream<UnaryOperator<AccessContextBuilder>> topicEditActions =\n          TopicAction.ALTER_ACTIONS.stream().map(a -> c -> c.topic(\"test\").topicActions(a));\n      Stream<UnaryOperator<AccessContextBuilder>> clusterConfigEditActions =\n          ClusterConfigAction.ALTER_ACTIONS.stream().map(a -> c -> c.clusterConfigActions(a));\n      Stream<UnaryOperator<AccessContextBuilder>> aclEditActions =\n          AclAction.ALTER_ACTIONS.stream().map(a -> c -> c.aclActions(a));\n      Stream<UnaryOperator<AccessContextBuilder>> cgEditActions =\n          ConsumerGroupAction.ALTER_ACTIONS.stream().map(a -> c -> c.consumerGroup(\"cg\").consumerGroupActions(a));\n      Stream<UnaryOperator<AccessContextBuilder>> schemaEditActions =\n          SchemaAction.ALTER_ACTIONS.stream().map(a -> c -> c.schema(\"sc\").schemaActions(a));\n      Stream<UnaryOperator<AccessContextBuilder>> connEditActions =\n          ConnectAction.ALTER_ACTIONS.stream().map(a -> c -> c.connect(\"conn\").connectActions(a));\n      return Stream.of(\n              topicEditActions, clusterConfigEditActions, aclEditActions,\n              cgEditActions, connEditActions, schemaEditActions\n          )\n          .flatMap(c -> c)\n          .map(setter -> setter.apply(AccessContext.builder().cluster(\"test\").operationName(\"test\")).build());\n    }\n\n    @ParameterizedTest\n    @MethodSource\n    void doesNothingIfNoResourceHasAlterAction(AccessContext readOnlyCxt) {\n      alterOnlyWriter.write(readOnlyCxt, user, null);\n      verifyNoInteractions(producerMock);\n      verifyNoInteractions(loggerMock);\n    }\n\n    static Stream<AccessContext> doesNothingIfNoResourceHasAlterAction() {\n      return Stream.<UnaryOperator<AccessContextBuilder>>of(\n          c -> c.topic(\"test\").topicActions(TopicAction.VIEW),\n          c -> c.clusterConfigActions(ClusterConfigAction.VIEW),\n          c -> c.aclActions(AclAction.VIEW),\n          c -> c.consumerGroup(\"cg\").consumerGroupActions(ConsumerGroupAction.VIEW),\n          c -> c.schema(\"sc\").schemaActions(SchemaAction.VIEW),\n          c -> c.connect(\"conn\").connectActions(ConnectAction.VIEW)\n      ).map(setter -> setter.apply(AccessContext.builder().cluster(\"test\").operationName(\"test\")).build());\n    }\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/ConnectorsExporterTest.java",
    "content": "package com.provectus.kafka.ui.service.integration.odd;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport com.provectus.kafka.ui.connect.model.ConnectorTopics;\nimport com.provectus.kafka.ui.model.ConnectDTO;\nimport com.provectus.kafka.ui.model.ConnectorDTO;\nimport com.provectus.kafka.ui.model.ConnectorTypeDTO;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.service.KafkaConnectService;\nimport java.util.List;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\nimport org.opendatadiscovery.client.model.DataEntity;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\n\nclass ConnectorsExporterTest {\n\n  private static final KafkaCluster CLUSTER = KafkaCluster.builder()\n      .name(\"test cluster\")\n      .bootstrapServers(\"localhost:9092\")\n      .build();\n\n  private final KafkaConnectService kafkaConnectService = mock(KafkaConnectService.class);\n  private final ConnectorsExporter exporter = new ConnectorsExporter(kafkaConnectService);\n\n  @Test\n  void exportsConnectorsAsDataTransformers() {\n    ConnectDTO connect = new ConnectDTO();\n    connect.setName(\"testConnect\");\n    connect.setAddress(\"http://kconnect:8083\");\n\n    ConnectorDTO sinkConnector = new ConnectorDTO();\n    sinkConnector.setName(\"testSink\");\n    sinkConnector.setType(ConnectorTypeDTO.SINK);\n    sinkConnector.setConnect(connect.getName());\n    sinkConnector.setConfig(\n        Map.of(\n            \"connector.class\", \"FileStreamSink\",\n            \"file\", \"filePathHere\",\n            \"topic\", \"inputTopic\"\n        )\n    );\n\n    ConnectorDTO sourceConnector = new ConnectorDTO();\n    sourceConnector.setName(\"testSource\");\n    sourceConnector.setConnect(connect.getName());\n    sourceConnector.setType(ConnectorTypeDTO.SOURCE);\n    sourceConnector.setConfig(\n        Map.of(\n            \"connector.class\", \"FileStreamSource\",\n            \"file\", \"filePathHere\",\n            \"topic\", \"outputTopic\"\n        )\n    );\n\n    when(kafkaConnectService.getConnects(CLUSTER))\n        .thenReturn(Flux.just(connect));\n\n    when(kafkaConnectService.getConnectorNamesWithErrorsSuppress(CLUSTER, connect.getName()))\n        .thenReturn(Flux.just(sinkConnector.getName(), sourceConnector.getName()));\n\n    when(kafkaConnectService.getConnector(CLUSTER, connect.getName(), sinkConnector.getName()))\n        .thenReturn(Mono.just(sinkConnector));\n\n    when(kafkaConnectService.getConnector(CLUSTER, connect.getName(), sourceConnector.getName()))\n        .thenReturn(Mono.just(sourceConnector));\n\n    when(kafkaConnectService.getConnectorTopics(CLUSTER, connect.getName(), sourceConnector.getName()))\n        .thenReturn(Mono.just(new ConnectorTopics().topics(List.of(\"outputTopic\"))));\n\n    when(kafkaConnectService.getConnectorTopics(CLUSTER, connect.getName(), sinkConnector.getName()))\n        .thenReturn(Mono.just(new ConnectorTopics().topics(List.of(\"inputTopic\"))));\n\n    StepVerifier.create(exporter.export(CLUSTER))\n        .assertNext(dataEntityList -> {\n          assertThat(dataEntityList.getDataSourceOddrn())\n              .isEqualTo(\"//kafkaconnect/host/kconnect:8083\");\n\n          assertThat(dataEntityList.getItems())\n              .hasSize(2);\n\n          assertThat(dataEntityList.getItems())\n              .filteredOn(DataEntity::getOddrn, \"//kafkaconnect/host/kconnect:8083/connectors/testSink\")\n              .singleElement()\n              .satisfies(sink -> {\n                assertThat(sink.getMetadata().get(0).getMetadata())\n                    .containsOnlyKeys(\"type\", \"connector.class\", \"file\", \"topic\");\n                assertThat(sink.getDataTransformer().getInputs()).contains(\n                    \"//kafka/cluster/localhost:9092/topics/inputTopic\");\n              });\n\n          assertThat(dataEntityList.getItems())\n              .filteredOn(DataEntity::getOddrn, \"//kafkaconnect/host/kconnect:8083/connectors/testSource\")\n              .singleElement()\n              .satisfies(source -> {\n                assertThat(source.getMetadata().get(0).getMetadata())\n                    .containsOnlyKeys(\"type\", \"connector.class\", \"file\", \"topic\");\n                assertThat(source.getDataTransformer().getOutputs()).contains(\n                    \"//kafka/cluster/localhost:9092/topics/outputTopic\");\n              });\n\n        })\n        .verifyComplete();\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/SchemaReferencesResolverTest.java",
    "content": "package com.provectus.kafka.ui.service.integration.odd;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport com.google.common.collect.ImmutableMap;\nimport com.provectus.kafka.ui.sr.api.KafkaSrClientApi;\nimport com.provectus.kafka.ui.sr.model.SchemaReference;\nimport com.provectus.kafka.ui.sr.model.SchemaSubject;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\n\nclass SchemaReferencesResolverTest {\n\n  private final KafkaSrClientApi srClientMock = mock(KafkaSrClientApi.class);\n\n  private final SchemaReferencesResolver schemaReferencesResolver = new SchemaReferencesResolver(srClientMock);\n\n  @Test\n  void resolvesRefsUsingSrClient() {\n    mockSrCall(\"sub1\", 1,\n        new SchemaSubject()\n            .schema(\"schema1\"));\n\n    mockSrCall(\"sub2\", 1,\n        new SchemaSubject()\n            .schema(\"schema2\")\n            .references(\n                List.of(\n                    new SchemaReference().name(\"ref2_1\").subject(\"sub2_1\").version(2),\n                    new SchemaReference().name(\"ref2_2\").subject(\"sub1\").version(1))));\n\n    mockSrCall(\"sub2_1\", 2,\n        new SchemaSubject()\n            .schema(\"schema2_1\")\n            .references(\n                List.of(\n                    new SchemaReference().name(\"ref2_1_1\").subject(\"sub2_1_1\").version(3),\n                    new SchemaReference().name(\"ref1\").subject(\"should_not_be_called\").version(1)\n                ))\n    );\n\n    mockSrCall(\"sub2_1_1\", 3,\n        new SchemaSubject()\n            .schema(\"schema2_1_1\"));\n\n    var resolvedRefsMono = schemaReferencesResolver.resolve(\n        List.of(\n            new SchemaReference().name(\"ref1\").subject(\"sub1\").version(1),\n            new SchemaReference().name(\"ref2\").subject(\"sub2\").version(1)));\n\n    StepVerifier.create(resolvedRefsMono)\n        .assertNext(refs ->\n            assertThat(refs)\n                .containsExactlyEntriesOf(\n                    // checking map should be ordered\n                    ImmutableMap.<String, String>builder()\n                        .put(\"ref1\", \"schema1\")\n                        .put(\"ref2_1_1\", \"schema2_1_1\")\n                        .put(\"ref2_1\", \"schema2_1\")\n                        .put(\"ref2_2\", \"schema1\")\n                        .put(\"ref2\", \"schema2\")\n                        .build()))\n        .verifyComplete();\n  }\n\n  @Test\n  void returnsEmptyMapOnEmptyInputs() {\n    StepVerifier.create(schemaReferencesResolver.resolve(null))\n        .assertNext(map -> assertThat(map).isEmpty())\n        .verifyComplete();\n\n    StepVerifier.create(schemaReferencesResolver.resolve(List.of()))\n        .assertNext(map -> assertThat(map).isEmpty())\n        .verifyComplete();\n  }\n\n  private void mockSrCall(String subject, int version, SchemaSubject subjectToReturn) {\n    when(srClientMock.getSubjectVersion(subject, version + \"\", true))\n        .thenReturn(Mono.just(subjectToReturn));\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/TopicsExporterTest.java",
    "content": "package com.provectus.kafka.ui.service.integration.odd;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.anyBoolean;\nimport static org.mockito.ArgumentMatchers.anyString;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.Statistics;\nimport com.provectus.kafka.ui.service.StatisticsCache;\nimport com.provectus.kafka.ui.sr.api.KafkaSrClientApi;\nimport com.provectus.kafka.ui.sr.model.SchemaSubject;\nimport com.provectus.kafka.ui.sr.model.SchemaType;\nimport com.provectus.kafka.ui.util.ReactiveFailover;\nimport java.util.List;\nimport java.util.Map;\nimport org.apache.kafka.clients.admin.ConfigEntry;\nimport org.apache.kafka.clients.admin.TopicDescription;\nimport org.apache.kafka.common.Node;\nimport org.apache.kafka.common.TopicPartitionInfo;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.opendatadiscovery.client.model.DataEntity;\nimport org.opendatadiscovery.client.model.DataEntityType;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.web.reactive.function.client.WebClientResponseException;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\n\nclass TopicsExporterTest {\n\n  private final KafkaSrClientApi schemaRegistryClientMock = mock(KafkaSrClientApi.class);\n\n  private final KafkaCluster cluster = KafkaCluster.builder()\n      .name(\"testCluster\")\n      .bootstrapServers(\"localhost:9092,localhost:19092\")\n      .schemaRegistryClient(ReactiveFailover.createNoop(schemaRegistryClientMock))\n      .build();\n\n  private Statistics stats;\n\n  private TopicsExporter topicsExporter;\n\n  @BeforeEach\n  void init() {\n    var statisticsCacheMock = mock(StatisticsCache.class);\n    when(statisticsCacheMock.get(cluster)).thenAnswer(invocationOnMock -> stats);\n\n    topicsExporter = new TopicsExporter(\n        topic -> !topic.startsWith(\"_\"),\n        statisticsCacheMock\n    );\n  }\n\n  @Test\n  void doesNotExportTopicsWhichDontFitFiltrationRule() {\n    when(schemaRegistryClientMock.getSubjectVersion(anyString(), anyString(), anyBoolean()))\n        .thenReturn(Mono.error(WebClientResponseException.create(404, \"NF\", new HttpHeaders(), null, null, null)));\n    stats = Statistics.empty()\n        .toBuilder()\n        .topicDescriptions(\n            Map.of(\n                \"_hidden\", new TopicDescription(\"_hidden\", false, List.of(\n                    new TopicPartitionInfo(0, null, List.of(), List.of())\n                )),\n                \"visible\", new TopicDescription(\"visible\", false, List.of(\n                    new TopicPartitionInfo(0, null, List.of(), List.of())\n                ))\n            )\n        )\n        .build();\n\n    StepVerifier.create(topicsExporter.export(cluster))\n        .assertNext(entityList -> {\n          assertThat(entityList.getDataSourceOddrn())\n              .isNotEmpty();\n\n          assertThat(entityList.getItems())\n              .hasSize(1)\n              .allSatisfy(e -> e.getOddrn().contains(\"visible\"));\n        })\n        .verifyComplete();\n  }\n\n  @Test\n  void doesExportTopicData() {\n    when(schemaRegistryClientMock.getSubjectVersion(\"testTopic-value\", \"latest\", false))\n        .thenReturn(Mono.just(\n            new SchemaSubject()\n                .schema(\"\\\"string\\\"\")\n                .schemaType(SchemaType.AVRO)\n        ));\n\n    when(schemaRegistryClientMock.getSubjectVersion(\"testTopic-key\", \"latest\", false))\n        .thenReturn(Mono.just(\n            new SchemaSubject()\n                .schema(\"\\\"int\\\"\")\n                .schemaType(SchemaType.AVRO)\n        ));\n\n    stats = Statistics.empty()\n        .toBuilder()\n        .topicDescriptions(\n            Map.of(\n                \"testTopic\",\n                new TopicDescription(\n                    \"testTopic\",\n                    false,\n                    List.of(\n                        new TopicPartitionInfo(\n                            0,\n                            null,\n                            List.of(\n                                new Node(1, \"host1\", 9092),\n                                new Node(2, \"host2\", 9092)\n                            ),\n                            List.of())\n                    ))\n            )\n        )\n        .topicConfigs(\n            Map.of(\n                \"testTopic\", List.of(\n                    new ConfigEntry(\n                        \"custom.config\",\n                        \"100500\",\n                        ConfigEntry.ConfigSource.DYNAMIC_TOPIC_CONFIG,\n                        false,\n                        false,\n                        List.of(),\n                        ConfigEntry.ConfigType.INT,\n                        null\n                    )\n                )\n            )\n        )\n        .build();\n\n    StepVerifier.create(topicsExporter.export(cluster))\n        .assertNext(entityList -> {\n          assertThat(entityList.getItems())\n              .hasSize(1);\n\n          DataEntity topicEntity = entityList.getItems().get(0);\n          assertThat(topicEntity.getName()).isNotEmpty();\n          assertThat(topicEntity.getOddrn())\n              .isEqualTo(\"//kafka/cluster/localhost:19092,localhost:9092/topics/testTopic\");\n          assertThat(topicEntity.getType()).isEqualTo(DataEntityType.KAFKA_TOPIC);\n          assertThat(topicEntity.getMetadata())\n              .hasSize(1)\n              .singleElement()\n              .satisfies(e ->\n                  assertThat(e.getMetadata())\n                      .containsExactlyInAnyOrderEntriesOf(\n                          Map.of(\n                              \"partitions\", 1,\n                              \"replication_factor\", 2,\n                              \"custom.config\", \"100500\")));\n\n          assertThat(topicEntity.getDataset()).isNotNull();\n          assertThat(topicEntity.getDataset().getFieldList())\n              .hasSize(4); // 2 field for key, 2 for value\n        })\n        .verifyComplete();\n  }\n\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/AvroExtractorTest.java",
    "content": "package com.provectus.kafka.ui.service.integration.odd.schema;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport io.confluent.kafka.schemaregistry.avro.AvroSchema;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport org.opendatadiscovery.client.model.DataSetField;\nimport org.opendatadiscovery.client.model.DataSetFieldType;\nimport org.opendatadiscovery.oddrn.model.KafkaPath;\n\nclass AvroExtractorTest {\n\n  @ParameterizedTest\n  @ValueSource(booleans = {true, false})\n  void test(boolean isKey) {\n    var list = AvroExtractor.extract(\n        new AvroSchema(\"\"\"\n                {\n                    \"type\": \"record\",\n                    \"name\": \"Message\",\n                    \"namespace\": \"com.provectus.kafka\",\n                    \"fields\":\n                    [\n                        {\n                            \"name\": \"f1\",\n                            \"type\":\n                            {\n                                \"type\": \"array\",\n                                \"items\":\n                                {\n                                    \"type\": \"record\",\n                                    \"name\": \"ArrElement\",\n                                    \"fields\":\n                                    [\n                                        {\n                                            \"name\": \"longmap\",\n                                            \"type\":\n                                            {\n                                                \"type\": \"map\",\n                                                \"values\": \"long\"\n                                            }\n                                        }\n                                    ]\n                                }\n                            }\n                        },\n                        {\n                            \"name\": \"f2\",\n                            \"type\":\n                            {\n                                \"type\": \"record\",\n                                \"name\": \"InnerMessage\",\n                                \"fields\":\n                                [\n                                    {\n                                        \"name\": \"text\",\n                                        \"doc\": \"string field here\",\n                                        \"type\": \"string\"\n                                    },\n                                    {\n                                        \"name\": \"innerMsgRef\",\n                                        \"type\": \"InnerMessage\"\n                                    },\n                                    {\n                                        \"name\": \"nullable_union\",\n                                        \"type\":\n                                        [\n                                            \"null\",\n                                            \"string\",\n                                            \"int\"\n                                        ],\n                                        \"default\": null\n                                    },\n                                    {\n                                        \"name\": \"order_enum\",\n                                        \"type\":\n                                        {\n                                            \"type\": \"enum\",\n                                            \"name\": \"Suit\",\n                                            \"symbols\":\n                                            [\n                                                \"SPADES\",\n                                                \"HEARTS\"\n                                            ]\n                                        }\n                                    },\n                                    {\n                                        \"name\": \"str_list\",\n                                        \"type\":\n                                        {\n                                            \"type\": \"array\",\n                                            \"items\": \"string\"\n                                        }\n                                    }\n                                ]\n                            }\n                        }\n                    ]\n                }\n                \"\"\"),\n\n        KafkaPath.builder()\n            .cluster(\"localhost:9092\")\n            .topic(\"someTopic\")\n            .build(),\n        isKey\n    );\n\n    String baseOddrn = \"//kafka/cluster/localhost:9092/topics/someTopic/columns/\" + (isKey ? \"key\" : \"value\");\n\n    assertThat(list).contains(\n        DataSetFieldsExtractors.rootField(\n            KafkaPath.builder().cluster(\"localhost:9092\").topic(\"someTopic\").build(),\n            isKey\n        ),\n        new DataSetField()\n            .name(\"f1\")\n            .parentFieldOddrn(baseOddrn)\n            .oddrn(baseOddrn + \"/f1\")\n            .type(\n                new DataSetFieldType()\n                    .type(DataSetFieldType.TypeEnum.LIST)\n                    .logicalType(\"array\")\n                    .isNullable(false)\n            ),\n        new DataSetField()\n            .name(\"ArrElement\")\n            .parentFieldOddrn(baseOddrn + \"/f1\")\n            .oddrn(baseOddrn + \"/f1/items/ArrElement\")\n            .type(\n                new DataSetFieldType()\n                    .type(DataSetFieldType.TypeEnum.STRUCT)\n                    .logicalType(\"com.provectus.kafka.ArrElement\")\n                    .isNullable(false)\n            ),\n        new DataSetField()\n            .name(\"longmap\")\n            .parentFieldOddrn(baseOddrn + \"/f1/items/ArrElement\")\n            .oddrn(baseOddrn + \"/f1/items/ArrElement/fields/longmap\")\n            .type(\n                new DataSetFieldType()\n                    .type(DataSetFieldType.TypeEnum.MAP)\n                    .logicalType(\"map\")\n                    .isNullable(false)\n            ),\n        new DataSetField()\n            .name(\"key\")\n            .parentFieldOddrn(baseOddrn + \"/f1/items/ArrElement/fields/longmap\")\n            .oddrn(baseOddrn + \"/f1/items/ArrElement/fields/longmap/key\")\n            .type(\n                new DataSetFieldType()\n                    .type(DataSetFieldType.TypeEnum.STRING)\n                    .logicalType(\"string\")\n                    .isNullable(false)\n            ),\n        new DataSetField()\n            .name(\"value\")\n            .parentFieldOddrn(baseOddrn + \"/f1/items/ArrElement/fields/longmap\")\n            .oddrn(baseOddrn + \"/f1/items/ArrElement/fields/longmap/value\")\n            .type(\n                new DataSetFieldType()\n                    .type(DataSetFieldType.TypeEnum.INTEGER)\n                    .logicalType(\"long\")\n                    .isNullable(false)\n            ),\n        new DataSetField()\n            .name(\"f2\")\n            .parentFieldOddrn(baseOddrn)\n            .oddrn(baseOddrn + \"/f2\")\n            .type(\n                new DataSetFieldType()\n                    .type(DataSetFieldType.TypeEnum.STRUCT)\n                    .logicalType(\"com.provectus.kafka.InnerMessage\")\n                    .isNullable(false)\n            ),\n        new DataSetField()\n            .name(\"text\")\n            .parentFieldOddrn(baseOddrn + \"/f2\")\n            .oddrn(baseOddrn + \"/f2/fields/text\")\n            .description(\"string field here\")\n            .type(\n                new DataSetFieldType()\n                    .type(DataSetFieldType.TypeEnum.STRING)\n                    .logicalType(\"string\")\n                    .isNullable(false)\n            ),\n        new DataSetField()\n            .name(\"innerMsgRef\")\n            .parentFieldOddrn(baseOddrn + \"/f2\")\n            .oddrn(baseOddrn + \"/f2/fields/innerMsgRef\")\n            .type(\n                new DataSetFieldType()\n                    .type(DataSetFieldType.TypeEnum.STRUCT)\n                    .logicalType(\"com.provectus.kafka.InnerMessage\")\n                    .isNullable(false)\n            ),\n        new DataSetField()\n            .name(\"nullable_union\")\n            .parentFieldOddrn(baseOddrn + \"/f2\")\n            .oddrn(baseOddrn + \"/f2/fields/nullable_union\")\n            .type(\n                new DataSetFieldType()\n                    .type(DataSetFieldType.TypeEnum.UNION)\n                    .logicalType(\"union\")\n                    .isNullable(true)\n            ),\n        new DataSetField()\n            .name(\"string\")\n            .parentFieldOddrn(baseOddrn + \"/f2/fields/nullable_union\")\n            .oddrn(baseOddrn + \"/f2/fields/nullable_union/values/string\")\n            .type(\n                new DataSetFieldType()\n                    .type(DataSetFieldType.TypeEnum.STRING)\n                    .logicalType(\"string\")\n                    .isNullable(true)\n            ),\n        new DataSetField()\n            .name(\"int\")\n            .parentFieldOddrn(baseOddrn + \"/f2/fields/nullable_union\")\n            .oddrn(baseOddrn + \"/f2/fields/nullable_union/values/int\")\n            .type(\n                new DataSetFieldType()\n                    .type(DataSetFieldType.TypeEnum.INTEGER)\n                    .logicalType(\"int\")\n                    .isNullable(true)\n            ),\n        new DataSetField()\n            .name(\"int\")\n            .parentFieldOddrn(baseOddrn + \"/f2/fields/nullable_union\")\n            .oddrn(baseOddrn + \"/f2/fields/nullable_union/values/int\")\n            .type(\n                new DataSetFieldType()\n                    .type(DataSetFieldType.TypeEnum.INTEGER)\n                    .logicalType(\"int\")\n                    .isNullable(true)\n            ),\n        new DataSetField()\n            .name(\"order_enum\")\n            .parentFieldOddrn(baseOddrn + \"/f2\")\n            .oddrn(baseOddrn + \"/f2/fields/order_enum\")\n            .type(\n                new DataSetFieldType()\n                    .type(DataSetFieldType.TypeEnum.STRING)\n                    .logicalType(\"enum\")\n                    .isNullable(false)\n            ),\n        new DataSetField()\n            .name(\"str_list\")\n            .parentFieldOddrn(baseOddrn + \"/f2\")\n            .oddrn(baseOddrn + \"/f2/fields/str_list\")\n            .type(\n                new DataSetFieldType()\n                    .type(DataSetFieldType.TypeEnum.LIST)\n                    .logicalType(\"array\")\n                    .isNullable(false)\n            ),\n        new DataSetField()\n            .name(\"string\")\n            .parentFieldOddrn(baseOddrn + \"/f2/fields/str_list\")\n            .oddrn(baseOddrn + \"/f2/fields/str_list/items/string\")\n            .type(\n                new DataSetFieldType()\n                    .type(DataSetFieldType.TypeEnum.STRING)\n                    .logicalType(\"string\")\n                    .isNullable(false)\n            )\n    );\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/JsonSchemaExtractorTest.java",
    "content": "package com.provectus.kafka.ui.service.integration.odd.schema;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport io.confluent.kafka.schemaregistry.json.JsonSchema;\nimport java.net.URI;\nimport java.util.List;\nimport java.util.Map;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport org.opendatadiscovery.client.model.DataSetField;\nimport org.opendatadiscovery.client.model.DataSetFieldType;\nimport org.opendatadiscovery.client.model.MetadataExtension;\nimport org.opendatadiscovery.oddrn.model.KafkaPath;\n\nclass JsonSchemaExtractorTest {\n\n  @ParameterizedTest\n  @ValueSource(booleans = {true, false})\n  void test(boolean isKey) {\n    String jsonSchema = \"\"\"\n        {\n            \"$id\": \"http://example.com/test.TestMsg\",\n            \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n            \"type\": \"object\",\n            \"required\": [ \"int32_field\" ],\n            \"properties\":\n            {\n                \"int32_field\": { \"type\": \"integer\", \"title\": \"field title\" },\n                \"lst_s_field\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"field descr\" },\n                \"untyped_struct_field\": { \"type\": \"object\", \"properties\": {} },\n                \"union_field\": { \"type\": [ \"number\", \"object\", \"null\" ] },\n                \"struct_field\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"bool_field\": { \"type\": \"boolean\" }\n                    }\n                }\n            }\n        }\n        \"\"\";\n    var fields = JsonSchemaExtractor.extract(\n        new JsonSchema(jsonSchema),\n        KafkaPath.builder()\n            .cluster(\"localhost:9092\")\n            .topic(\"someTopic\")\n            .build(),\n        isKey\n    );\n\n    String baseOddrn = \"//kafka/cluster/localhost:9092/topics/someTopic/columns/\" + (isKey ? \"key\" : \"value\");\n\n    assertThat(fields).contains(\n        DataSetFieldsExtractors.rootField(\n            KafkaPath.builder().cluster(\"localhost:9092\").topic(\"someTopic\").build(),\n            isKey\n        ),\n        new DataSetField()\n            .name(\"int32_field\")\n            .parentFieldOddrn(baseOddrn)\n            .oddrn(baseOddrn + \"/int32_field\")\n            .description(\"field title\")\n            .type(new DataSetFieldType()\n                .type(DataSetFieldType.TypeEnum.NUMBER)\n                .logicalType(\"Number\")\n                .isNullable(false)),\n        new DataSetField()\n            .name(\"lst_s_field\")\n            .parentFieldOddrn(baseOddrn)\n            .oddrn(baseOddrn + \"/lst_s_field\")\n            .description(\"field descr\")\n            .type(new DataSetFieldType()\n                .type(DataSetFieldType.TypeEnum.LIST)\n                .logicalType(\"array\")\n                .isNullable(true)),\n        new DataSetField()\n            .name(\"String\")\n            .parentFieldOddrn(baseOddrn + \"/lst_s_field\")\n            .oddrn(baseOddrn + \"/lst_s_field/items/String\")\n            .type(new DataSetFieldType()\n                .type(DataSetFieldType.TypeEnum.STRING)\n                .logicalType(\"String\")\n                .isNullable(false)),\n        new DataSetField()\n            .name(\"untyped_struct_field\")\n            .parentFieldOddrn(baseOddrn)\n            .oddrn(baseOddrn + \"/untyped_struct_field\")\n            .type(new DataSetFieldType()\n                .type(DataSetFieldType.TypeEnum.STRUCT)\n                .logicalType(\"Object\")\n                .isNullable(true)),\n        new DataSetField()\n            .name(\"union_field\")\n            .parentFieldOddrn(baseOddrn)\n            .oddrn(baseOddrn + \"/union_field/anyOf\")\n            .metadata(List.of(new MetadataExtension()\n                .schemaUrl(URI.create(\"wontbeused.oops\"))\n                .metadata(Map.of(\"criterion\", \"anyOf\"))))\n            .type(new DataSetFieldType()\n                .type(DataSetFieldType.TypeEnum.UNION)\n                .logicalType(\"anyOf\")\n                .isNullable(true)),\n        new DataSetField()\n            .name(\"Number\")\n            .parentFieldOddrn(baseOddrn + \"/union_field/anyOf\")\n            .oddrn(baseOddrn + \"/union_field/anyOf/values/Number\")\n            .type(new DataSetFieldType()\n                .type(DataSetFieldType.TypeEnum.NUMBER)\n                .logicalType(\"Number\")\n                .isNullable(true)),\n        new DataSetField()\n            .name(\"Object\")\n            .parentFieldOddrn(baseOddrn + \"/union_field/anyOf\")\n            .oddrn(baseOddrn + \"/union_field/anyOf/values/Object\")\n            .type(new DataSetFieldType()\n                .type(DataSetFieldType.TypeEnum.STRUCT)\n                .logicalType(\"Object\")\n                .isNullable(true)),\n        new DataSetField()\n            .name(\"Null\")\n            .parentFieldOddrn(baseOddrn + \"/union_field/anyOf\")\n            .oddrn(baseOddrn + \"/union_field/anyOf/values/Null\")\n            .type(new DataSetFieldType()\n                .type(DataSetFieldType.TypeEnum.UNKNOWN)\n                .logicalType(\"Null\")\n                .isNullable(true)),\n        new DataSetField()\n            .name(\"struct_field\")\n            .parentFieldOddrn(baseOddrn)\n            .oddrn(baseOddrn + \"/struct_field\")\n            .type(new DataSetFieldType()\n                .type(DataSetFieldType.TypeEnum.STRUCT)\n                .logicalType(\"Object\")\n                .isNullable(true)),\n        new DataSetField()\n            .name(\"bool_field\")\n            .parentFieldOddrn(baseOddrn + \"/struct_field\")\n            .oddrn(baseOddrn + \"/struct_field/fields/bool_field\")\n            .type(new DataSetFieldType()\n                .type(DataSetFieldType.TypeEnum.BOOLEAN)\n                .logicalType(\"Boolean\")\n                .isNullable(true))\n    );\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/schema/ProtoExtractorTest.java",
    "content": "package com.provectus.kafka.ui.service.integration.odd.schema;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport org.opendatadiscovery.client.model.DataSetField;\nimport org.opendatadiscovery.client.model.DataSetFieldType;\nimport org.opendatadiscovery.oddrn.model.KafkaPath;\n\nclass ProtoExtractorTest {\n\n  @ParameterizedTest\n  @ValueSource(booleans = {true, false})\n  void test(boolean isKey) {\n    String protoSchema = \"\"\"\n        syntax = \"proto3\";\n        package test;\n\n        import \"google/protobuf/timestamp.proto\";\n        import \"google/protobuf/duration.proto\";\n        import \"google/protobuf/struct.proto\";\n        import \"google/protobuf/wrappers.proto\";\n\n        message TestMsg {\n            map<string, int32> mapField = 100;\n            int32 int32_field = 2;\n            bool bool_field = 3;\n            SampleEnum enum_field = 4;\n\n            enum SampleEnum {\n                ENUM_V1 = 0;\n                ENUM_V2 = 1;\n            }\n\n            google.protobuf.Timestamp ts_field = 5;\n            google.protobuf.Duration duration_field = 8;\n\n            oneof some_oneof1 {\n                google.protobuf.Value one_of_v1 = 9;\n                google.protobuf.Value one_of_v2 = 10;\n            }\n            // wrapper field:\n            google.protobuf.Int64Value int64_w_field = 11;\n\n            //embedded msg\n            EmbeddedMsg emb = 19;\n\n            message EmbeddedMsg {\n                int32 emb_f1 = 1;\n                TestMsg outer_ref = 2;\n            }\n        }\"\"\";\n\n    var list = ProtoExtractor.extract(\n        new ProtobufSchema(protoSchema),\n        KafkaPath.builder()\n            .cluster(\"localhost:9092\")\n            .topic(\"someTopic\")\n            .build(),\n        isKey\n    );\n\n    String baseOddrn = \"//kafka/cluster/localhost:9092/topics/someTopic/columns/\" + (isKey ? \"key\" : \"value\");\n\n    assertThat(list)\n        .contains(\n            DataSetFieldsExtractors.rootField(\n                KafkaPath.builder().cluster(\"localhost:9092\").topic(\"someTopic\").build(),\n                isKey\n            ),\n            new DataSetField()\n                .name(\"mapField\")\n                .parentFieldOddrn(baseOddrn)\n                .oddrn(baseOddrn + \"/mapField\")\n                .type(\n                    new DataSetFieldType()\n                        .type(DataSetFieldType.TypeEnum.LIST)\n                        .logicalType(\"repeated\")\n                        .isNullable(true)\n                ),\n            new DataSetField()\n                .name(\"int32_field\")\n                .parentFieldOddrn(baseOddrn)\n                .oddrn(baseOddrn + \"/int32_field\")\n                .type(\n                    new DataSetFieldType()\n                        .type(DataSetFieldType.TypeEnum.INTEGER)\n                        .logicalType(\"int32\")\n                        .isNullable(true)\n                ),\n            new DataSetField()\n                .name(\"enum_field\")\n                .parentFieldOddrn(baseOddrn)\n                .oddrn(baseOddrn + \"/enum_field\")\n                .type(\n                    new DataSetFieldType()\n                        .type(DataSetFieldType.TypeEnum.STRING)\n                        .logicalType(\"enum\")\n                        .isNullable(true)\n                ),\n            new DataSetField()\n                .name(\"ts_field\")\n                .parentFieldOddrn(baseOddrn)\n                .oddrn(baseOddrn + \"/ts_field\")\n                .type(\n                    new DataSetFieldType()\n                        .type(DataSetFieldType.TypeEnum.DATETIME)\n                        .logicalType(\"google.protobuf.Timestamp\")\n                        .isNullable(true)\n                ),\n            new DataSetField()\n                .name(\"duration_field\")\n                .parentFieldOddrn(baseOddrn)\n                .oddrn(baseOddrn + \"/duration_field\")\n                .type(\n                    new DataSetFieldType()\n                        .type(DataSetFieldType.TypeEnum.DURATION)\n                        .logicalType(\"google.protobuf.Duration\")\n                        .isNullable(true)\n                ),\n            new DataSetField()\n                .name(\"one_of_v1\")\n                .parentFieldOddrn(baseOddrn)\n                .oddrn(baseOddrn + \"/one_of_v1\")\n                .type(\n                    new DataSetFieldType()\n                        .type(DataSetFieldType.TypeEnum.UNKNOWN)\n                        .logicalType(\"google.protobuf.Value\")\n                        .isNullable(true)\n                ),\n            new DataSetField()\n                .name(\"one_of_v2\")\n                .parentFieldOddrn(baseOddrn)\n                .oddrn(baseOddrn + \"/one_of_v2\")\n                .type(\n                    new DataSetFieldType()\n                        .type(DataSetFieldType.TypeEnum.UNKNOWN)\n                        .logicalType(\"google.protobuf.Value\")\n                        .isNullable(true)\n                ),\n            new DataSetField()\n                .name(\"int64_w_field\")\n                .parentFieldOddrn(baseOddrn)\n                .oddrn(baseOddrn + \"/int64_w_field\")\n                .type(\n                    new DataSetFieldType()\n                        .type(DataSetFieldType.TypeEnum.INTEGER)\n                        .logicalType(\"google.protobuf.Int64Value\")\n                        .isNullable(true)\n                ),\n            new DataSetField()\n                .name(\"emb\")\n                .parentFieldOddrn(baseOddrn)\n                .oddrn(baseOddrn + \"/emb\")\n                .type(\n                    new DataSetFieldType()\n                        .type(DataSetFieldType.TypeEnum.STRUCT)\n                        .logicalType(\"test.TestMsg.EmbeddedMsg\")\n                        .isNullable(true)\n                ),\n            new DataSetField()\n                .name(\"emb_f1\")\n                .parentFieldOddrn(baseOddrn + \"/emb\")\n                .oddrn(baseOddrn + \"/emb/fields/emb_f1\")\n                .type(\n                    new DataSetFieldType()\n                        .type(DataSetFieldType.TypeEnum.INTEGER)\n                        .logicalType(\"int32\")\n                        .isNullable(true)\n                ),\n            new DataSetField()\n                .name(\"outer_ref\")\n                .parentFieldOddrn(baseOddrn + \"/emb\")\n                .oddrn(baseOddrn + \"/emb/fields/outer_ref\")\n                .type(\n                    new DataSetFieldType()\n                        .type(DataSetFieldType.TypeEnum.STRUCT)\n                        .logicalType(\"test.TestMsg\")\n                        .isNullable(true)\n                )\n        );\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlApiClientTest.java",
    "content": "package com.provectus.kafka.ui.service.ksql;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.fasterxml.jackson.databind.node.DecimalNode;\nimport com.fasterxml.jackson.databind.node.IntNode;\nimport com.fasterxml.jackson.databind.node.JsonNodeFactory;\nimport com.fasterxml.jackson.databind.node.TextNode;\nimport com.provectus.kafka.ui.AbstractIntegrationTest;\nimport java.math.BigDecimal;\nimport java.time.Duration;\nimport java.util.Map;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.testcontainers.shaded.org.awaitility.Awaitility;\nimport reactor.test.StepVerifier;\n\nclass KsqlApiClientTest extends AbstractIntegrationTest {\n\n  @BeforeAll\n  static void startContainer() {\n    KSQL_DB.start();\n  }\n\n  @AfterAll\n  static void stopContainer() {\n    KSQL_DB.stop();\n  }\n\n  // Tutorial is here: https://ksqldb.io/quickstart.html\n  @Test\n  void ksqTutorialQueriesWork() {\n    var client = ksqlClient();\n    execCommandSync(client,\n        \"CREATE STREAM riderLocations (profileId VARCHAR, latitude DOUBLE, longitude DOUBLE) \"\n            + \"WITH (kafka_topic='locations', value_format='json', partitions=1);\",\n        \"CREATE TABLE currentLocation AS \"\n            + \"  SELECT profileId, \"\n            + \"         LATEST_BY_OFFSET(latitude) AS la, \"\n            + \"         LATEST_BY_OFFSET(longitude) AS lo \"\n            + \"  FROM riderlocations \"\n            + \"  GROUP BY profileId \"\n            + \"  EMIT CHANGES;\",\n        \"CREATE TABLE ridersNearMountainView AS \"\n            + \"  SELECT ROUND(GEO_DISTANCE(la, lo, 37.4133, -122.1162), -1) AS distanceInMiles, \"\n            + \"         COLLECT_LIST(profileId) AS riders, \"\n            + \"         COUNT(*) AS count \"\n            + \"  FROM currentLocation \"\n            + \"  GROUP BY ROUND(GEO_DISTANCE(la, lo, 37.4133, -122.1162), -1);\",\n        \"INSERT INTO riderLocations (profileId, latitude, longitude) VALUES ('c2309eec', 37.7877, -122.4205); \",\n        \"INSERT INTO riderLocations (profileId, latitude, longitude) VALUES ('18f4ea86', 37.3903, -122.0643); \",\n        \"INSERT INTO riderLocations (profileId, latitude, longitude) VALUES ('4ab5cbad', 37.3952, -122.0813); \",\n        \"INSERT INTO riderLocations (profileId, latitude, longitude) VALUES ('8b6eae59', 37.3944, -122.0813); \",\n        \"INSERT INTO riderLocations (profileId, latitude, longitude) VALUES ('4a7c7b41', 37.4049, -122.0822); \",\n        \"INSERT INTO riderLocations (profileId, latitude, longitude) VALUES ('4ddad000', 37.7857, -122.4011);\"\n    );\n\n    Awaitility.await()\n        .pollDelay(Duration.ofSeconds(1))\n        .atMost(Duration.ofSeconds(20))\n        .untilAsserted(() -> assertLastKsqTutorialQueryResult(client));\n  }\n\n  private void assertLastKsqTutorialQueryResult(KsqlApiClient client) {\n    // expected results:\n    //{\"header\":\"Schema\",\"columnNames\":[...],\"values\":null}\n    //{\"header\":\"Row\",\"columnNames\":null,\"values\":[[0,[\"4ab5cbad\",\"8b6eae59\",\"4a7c7b41\"],3]]}\n    //{\"header\":\"Row\",\"columnNames\":null,\"values\":[[10.0,[\"18f4ea86\"],1]]}\n    StepVerifier.create(\n            client.execute(\n                \"SELECT * from ridersNearMountainView WHERE distanceInMiles <= 10;\",\n                Map.of()\n            )\n        )\n        .assertNext(header -> {\n          assertThat(header.getHeader()).isEqualTo(\"Schema\");\n          assertThat(header.getColumnNames()).hasSize(3);\n          assertThat(header.getValues()).isNull();\n        })\n        .assertNext(row -> {\n          var distance = (DecimalNode) row.getValues().get(0).get(0);\n          var riders = (ArrayNode) row.getValues().get(0).get(1);\n          var count = (IntNode) row.getValues().get(0).get(2);\n\n          assertThat(distance).isEqualTo(new DecimalNode(new BigDecimal(0)));\n          assertThat(riders).isEqualTo(new ArrayNode(JsonNodeFactory.instance)\n              .add(new TextNode(\"4ab5cbad\"))\n              .add(new TextNode(\"8b6eae59\"))\n              .add(new TextNode(\"4a7c7b41\")));\n          assertThat(count).isEqualTo(new IntNode(3));\n        })\n        .assertNext(row -> {\n          var distance = (DecimalNode) row.getValues().get(0).get(0);\n          var riders = (ArrayNode) row.getValues().get(0).get(1);\n          var count = (IntNode) row.getValues().get(0).get(2);\n\n          assertThat(distance).isEqualTo(new DecimalNode(new BigDecimal(10)));\n          assertThat(riders).isEqualTo(new ArrayNode(JsonNodeFactory.instance)\n              .add(new TextNode(\"18f4ea86\")));\n          assertThat(count).isEqualTo(new IntNode(1));\n        })\n        .verifyComplete();\n  }\n\n  private void execCommandSync(KsqlApiClient client, String... ksqls) {\n    for (String ksql : ksqls) {\n      client.execute(ksql, Map.of()).collectList().block();\n    }\n  }\n\n  private KsqlApiClient ksqlClient() {\n    return new KsqlApiClient(KSQL_DB.url(), null, null, null, null);\n  }\n\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/KsqlServiceV2Test.java",
    "content": "package com.provectus.kafka.ui.service.ksql;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.provectus.kafka.ui.AbstractIntegrationTest;\nimport com.provectus.kafka.ui.model.KafkaCluster;\nimport com.provectus.kafka.ui.model.KsqlStreamDescriptionDTO;\nimport com.provectus.kafka.ui.model.KsqlTableDescriptionDTO;\nimport com.provectus.kafka.ui.util.ReactiveFailover;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CopyOnWriteArraySet;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nclass KsqlServiceV2Test extends AbstractIntegrationTest {\n\n  private static final Set<String> STREAMS_TO_DELETE = new CopyOnWriteArraySet<>();\n  private static final Set<String> TABLES_TO_DELETE = new CopyOnWriteArraySet<>();\n\n  @BeforeAll\n  static void init() {\n    KSQL_DB.start();\n  }\n\n  @AfterAll\n  static void cleanup() {\n    TABLES_TO_DELETE.forEach(t ->\n        ksqlClient().execute(String.format(\"DROP TABLE IF EXISTS %s DELETE TOPIC;\", t), Map.of())\n            .blockLast());\n\n    STREAMS_TO_DELETE.forEach(s ->\n        ksqlClient().execute(String.format(\"DROP STREAM IF EXISTS %s DELETE TOPIC;\", s), Map.of())\n            .blockLast());\n\n    KSQL_DB.stop();\n  }\n\n  private final KsqlServiceV2 ksqlService = new KsqlServiceV2();\n\n  @Test\n  void listStreamsReturnsAllKsqlStreams() {\n    var streamName = \"stream_\" + System.currentTimeMillis();\n    STREAMS_TO_DELETE.add(streamName);\n\n    ksqlClient()\n        .execute(\n            String.format(\"CREATE STREAM %s ( \"\n                + \"  c1 BIGINT KEY, \"\n                + \"  c2 VARCHAR \"\n                + \" ) WITH ( \"\n                + \"  KAFKA_TOPIC = '%s_topic', \"\n                + \"  PARTITIONS = 1, \"\n                + \"  VALUE_FORMAT = 'JSON' \"\n                + \" );\", streamName, streamName),\n            Map.of())\n        .blockLast();\n\n    var streams = ksqlService.listStreams(cluster()).collectList().block();\n    assertThat(streams).contains(\n        new KsqlStreamDescriptionDTO()\n            .name(streamName.toUpperCase())\n            .topic(streamName + \"_topic\")\n            .keyFormat(\"KAFKA\")\n            .valueFormat(\"JSON\")\n    );\n  }\n\n  @Test\n  void listTablesReturnsAllKsqlTables() {\n    var tableName = \"table_\" + System.currentTimeMillis();\n    TABLES_TO_DELETE.add(tableName);\n\n    ksqlClient()\n        .execute(\n            String.format(\"CREATE TABLE %s ( \"\n                + \"   c1 BIGINT PRIMARY KEY, \"\n                + \"   c2 VARCHAR \"\n                + \" ) WITH ( \"\n                + \"  KAFKA_TOPIC = '%s_topic', \"\n                + \"  PARTITIONS = 1, \"\n                + \"  VALUE_FORMAT = 'JSON' \"\n                + \" );\", tableName, tableName),\n            Map.of())\n        .blockLast();\n\n    var tables = ksqlService.listTables(cluster()).collectList().block();\n    assertThat(tables).contains(\n        new KsqlTableDescriptionDTO()\n            .name(tableName.toUpperCase())\n            .topic(tableName + \"_topic\")\n            .keyFormat(\"KAFKA\")\n            .valueFormat(\"JSON\")\n            .isWindowed(false)\n    );\n  }\n\n  private static KafkaCluster cluster() {\n    return KafkaCluster.builder()\n        .ksqlClient(ReactiveFailover.create(\n            List.of(ksqlClient()), th -> true, \"\", ReactiveFailover.DEFAULT_RETRY_GRACE_PERIOD_MS))\n        .build();\n  }\n\n  private static KsqlApiClient ksqlClient() {\n    return new KsqlApiClient(KSQL_DB.url(), null, null, null, null);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/response/ResponseParserTest.java",
    "content": "package com.provectus.kafka.ui.service.ksql.response;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.jupiter.api.Test;\n\nclass ResponseParserTest {\n\n  @Test\n  void parsesSelectHeaderIntoColumnNames() {\n    assertThat(ResponseParser.parseSelectHeadersString(\"`inQuotes` INT, notInQuotes INT\"))\n        .containsExactly(\"`inQuotes` INT\", \"notInQuotes INT\");\n\n    assertThat(ResponseParser.parseSelectHeadersString(\"`name with comma,` INT, name2 STRING\"))\n        .containsExactly(\"`name with comma,` INT\", \"name2 STRING\");\n\n    assertThat(ResponseParser.parseSelectHeadersString(\n        \"`topLvl` INT, `struct` STRUCT<`nested1` STRING, anotherName STRUCT<nested2 INT>>\"))\n        .containsExactly(\n            \"`topLvl` INT\",\n            \"`struct` STRUCT<`nested1` STRING, anotherName STRUCT<nested2 INT>>\"\n        );\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/DataMaskingTest.java",
    "content": "package com.provectus.kafka.ui.service.masking;\n\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.reset;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoInteractions;\n\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport com.fasterxml.jackson.databind.node.ContainerNode;\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.serde.api.Serde;\nimport com.provectus.kafka.ui.service.masking.policies.MaskingPolicy;\nimport java.util.List;\nimport java.util.regex.Pattern;\nimport lombok.SneakyThrows;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.ValueSource;\n\nclass DataMaskingTest {\n\n  private static final String TOPIC = \"test_topic\";\n\n  private DataMasking masking;\n\n  private MaskingPolicy policy1;\n  private MaskingPolicy policy2;\n  private MaskingPolicy policy3;\n\n  @BeforeEach\n  void init() {\n    policy1 = spy(createMaskPolicy());\n    policy2 = spy(createMaskPolicy());\n    policy3 = spy(createMaskPolicy());\n\n    masking = new DataMasking(\n        List.of(\n            new DataMasking.Mask(Pattern.compile(TOPIC), null, policy1),\n            new DataMasking.Mask(null, Pattern.compile(TOPIC), policy2),\n            new DataMasking.Mask(null, Pattern.compile(TOPIC + \"|otherTopic\"), policy3)));\n  }\n\n  private MaskingPolicy createMaskPolicy() {\n    var props = new ClustersProperties.Masking();\n    props.setType(ClustersProperties.Masking.Type.REMOVE);\n    return MaskingPolicy.create(props);\n  }\n\n  @ParameterizedTest\n  @ValueSource(strings = {\n      \"{\\\"some\\\": \\\"json\\\"}\",\n      \"[ {\\\"json\\\": \\\"array\\\"} ]\"\n  })\n  @SneakyThrows\n  void appliesMasksToJsonContainerArgsBasedOnTopicPatterns(String jsonObjOrArr) {\n    var parsedJson = (ContainerNode<?>) new JsonMapper().readTree(jsonObjOrArr);\n\n    masking.getMaskingFunction(TOPIC, Serde.Target.KEY).apply(jsonObjOrArr);\n    verify(policy1).applyToJsonContainer(eq(parsedJson));\n    verifyNoInteractions(policy2, policy3);\n\n    reset(policy1, policy2, policy3);\n\n    masking.getMaskingFunction(TOPIC, Serde.Target.VALUE).apply(jsonObjOrArr);\n    verify(policy2).applyToJsonContainer(eq(parsedJson));\n    verify(policy3).applyToJsonContainer(eq(policy2.applyToJsonContainer(parsedJson)));\n    verifyNoInteractions(policy1);\n  }\n\n  @ParameterizedTest\n  @ValueSource(strings = {\n      \"non json str\",\n      \"234\",\n      \"null\"\n  })\n  void appliesFirstFoundMaskToStringArgsBasedOnTopicPatterns(String nonJsonObjOrArrString) {\n    masking.getMaskingFunction(TOPIC, Serde.Target.KEY).apply(nonJsonObjOrArrString);\n    verify(policy1).applyToString(eq(nonJsonObjOrArrString));\n    verifyNoInteractions(policy2, policy3);\n\n    reset(policy1, policy2, policy3);\n\n    masking.getMaskingFunction(TOPIC, Serde.Target.VALUE).apply(nonJsonObjOrArrString);\n    verify(policy2).applyToString(eq(nonJsonObjOrArrString));\n    verifyNoInteractions(policy1, policy3);\n  }\n\n}"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/FieldsSelectorTest.java",
    "content": "package com.provectus.kafka.ui.service.masking.policies;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport com.provectus.kafka.ui.exception.ValidationException;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\n\nclass FieldsSelectorTest {\n\n  @Test\n  void selectsFieldsDueToProvidedPattern() {\n    var properties = new ClustersProperties.Masking();\n    properties.setFieldsNamePattern(\"f1|f2\");\n\n    var selector = FieldsSelector.create(properties);\n    assertThat(selector.shouldBeMasked(\"f1\")).isTrue();\n    assertThat(selector.shouldBeMasked(\"f2\")).isTrue();\n    assertThat(selector.shouldBeMasked(\"doesNotMatchPattern\")).isFalse();\n  }\n\n  @Test\n  void selectsFieldsDueToProvidedFieldNames() {\n    var properties = new ClustersProperties.Masking();\n    properties.setFields(List.of(\"f1\", \"f2\"));\n\n    var selector = FieldsSelector.create(properties);\n    assertThat(selector.shouldBeMasked(\"f1\")).isTrue();\n    assertThat(selector.shouldBeMasked(\"f2\")).isTrue();\n    assertThat(selector.shouldBeMasked(\"notInAList\")).isFalse();\n  }\n\n  @Test\n  void selectAllFieldsIfNoPatternAndNoNamesProvided() {\n    var properties = new ClustersProperties.Masking();\n\n    var selector = FieldsSelector.create(properties);\n    assertThat(selector.shouldBeMasked(\"anyPropertyName\")).isTrue();\n  }\n\n  @Test\n  void throwsExceptionIfBothFieldListAndPatternProvided() {\n    var properties = new ClustersProperties.Masking();\n    properties.setFieldsNamePattern(\"f1|f2\");\n    properties.setFields(List.of(\"f3\", \"f4\"));\n\n    assertThatThrownBy(() -> FieldsSelector.create(properties))\n        .isInstanceOf(ValidationException.class);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/MaskTest.java",
    "content": "package com.provectus.kafka.ui.service.masking.policies;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport com.fasterxml.jackson.databind.node.ContainerNode;\nimport java.util.List;\nimport java.util.stream.Stream;\nimport lombok.SneakyThrows;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.CsvSource;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nclass MaskTest {\n\n  private static final FieldsSelector FIELDS_SELECTOR = fieldName -> List.of(\"id\", \"name\").contains(fieldName);\n  private static final List<String> PATTERN = List.of(\"X\", \"x\", \"n\", \"-\");\n\n  @ParameterizedTest\n  @MethodSource\n  void testApplyToJsonContainer(FieldsSelector selector, ContainerNode<?> original, ContainerNode<?> expected) {\n    Mask policy = new Mask(selector, PATTERN);\n    assertThat(policy.applyToJsonContainer(original)).isEqualTo(expected);\n  }\n\n  private static Stream<Arguments> testApplyToJsonContainer() {\n    return Stream.of(\n        Arguments.of(\n            FIELDS_SELECTOR,\n            parse(\"{ \\\"id\\\": 123, \\\"name\\\": { \\\"first\\\": \\\"James\\\", \\\"surname\\\": \\\"Bond777!\\\"}}\"),\n            parse(\"{ \\\"id\\\": \\\"nnn\\\", \\\"name\\\": { \\\"first\\\": \\\"Xxxxx\\\", \\\"surname\\\": \\\"Xxxxnnn-\\\"}}\")\n        ),\n        Arguments.of(\n            FIELDS_SELECTOR,\n            parse(\"[{ \\\"id\\\": 123, \\\"f2\\\": 234}, { \\\"name\\\": \\\"1.2\\\", \\\"f2\\\": 345} ]\"),\n            parse(\"[{ \\\"id\\\": \\\"nnn\\\", \\\"f2\\\": 234}, { \\\"name\\\": \\\"n-n\\\", \\\"f2\\\": 345} ]\")\n        ),\n        Arguments.of(\n            FIELDS_SELECTOR,\n            parse(\"{ \\\"outer\\\": { \\\"f1\\\": \\\"James\\\", \\\"name\\\": \\\"Bond777!\\\"}}\"),\n            parse(\"{ \\\"outer\\\": { \\\"f1\\\": \\\"James\\\", \\\"name\\\": \\\"Xxxxnnn-\\\"}}\")\n        ),\n        Arguments.of(\n            (FieldsSelector) (fieldName -> true),\n            parse(\"{ \\\"outer\\\": { \\\"f1\\\": \\\"James\\\", \\\"name\\\": \\\"Bond777!\\\"}}\"),\n            parse(\"{ \\\"outer\\\": { \\\"f1\\\": \\\"Xxxxx\\\", \\\"name\\\": \\\"Xxxxnnn-\\\"}}\")\n        )\n    );\n  }\n\n  @ParameterizedTest\n  @CsvSource({\n      \"Some string?!1, Xxxx xxxxxx--n\",\n      \"1.24343, n-nnnnn\",\n      \"null, xxxx\"\n  })\n  void testApplyToString(String original, String expected) {\n    Mask policy = new Mask(fieldName -> true, PATTERN);\n    assertThat(policy.applyToString(original)).isEqualTo(expected);\n  }\n\n  @SneakyThrows\n  private static JsonNode parse(String str) {\n    return new JsonMapper().readTree(str);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/RemoveTest.java",
    "content": "package com.provectus.kafka.ui.service.masking.policies;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport com.fasterxml.jackson.databind.node.ContainerNode;\nimport java.util.List;\nimport java.util.stream.Stream;\nimport lombok.SneakyThrows;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.CsvSource;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nclass RemoveTest {\n\n  private static final FieldsSelector FIELDS_SELECTOR = fieldName -> List.of(\"id\", \"name\").contains(fieldName);\n\n  @ParameterizedTest\n  @MethodSource\n  void testApplyToJsonContainer(FieldsSelector fieldsSelector, ContainerNode<?> original, ContainerNode<?>  expected) {\n    var policy = new Remove(fieldsSelector);\n    assertThat(policy.applyToJsonContainer(original)).isEqualTo(expected);\n  }\n\n  private static Stream<Arguments> testApplyToJsonContainer() {\n    return Stream.of(\n        Arguments.of(\n            FIELDS_SELECTOR,\n            parse(\"{ \\\"id\\\": 123, \\\"name\\\": { \\\"first\\\": \\\"James\\\", \\\"surname\\\": \\\"Bond777!\\\"}}\"),\n            parse(\"{}\")\n        ),\n        Arguments.of(\n            FIELDS_SELECTOR,\n            parse(\"[{ \\\"id\\\": 123, \\\"f2\\\": 234}, { \\\"name\\\": \\\"1.2\\\", \\\"f2\\\": 345} ]\"),\n            parse(\"[{ \\\"f2\\\": 234}, { \\\"f2\\\": 345} ]\")\n        ),\n        Arguments.of(\n            FIELDS_SELECTOR,\n            parse(\"{ \\\"outer\\\": { \\\"f1\\\": \\\"James\\\", \\\"name\\\": \\\"Bond777!\\\"}}\"),\n            parse(\"{ \\\"outer\\\": { \\\"f1\\\": \\\"James\\\"}}\")\n        ),\n        Arguments.of(\n            (FieldsSelector) (fieldName -> true),\n            parse(\"{ \\\"outer\\\": { \\\"f1\\\": \\\"v1\\\", \\\"f2\\\": \\\"v2\\\", \\\"inner\\\" : {\\\"if1\\\": \\\"iv1\\\"}}}\"),\n            parse(\"{}\")\n        ),\n        Arguments.of(\n            (FieldsSelector) (fieldName -> true),\n            parse(\"[{ \\\"f1\\\": 123}, { \\\"f2\\\": \\\"1.2\\\"} ]\"),\n            parse(\"[{}, {}]\")\n        )\n    );\n  }\n\n  @SneakyThrows\n  private static JsonNode parse(String str) {\n    return new JsonMapper().readTree(str);\n  }\n\n  @ParameterizedTest\n  @CsvSource({\n      \"Some string?!1, null\",\n      \"1.24343, null\",\n      \"null, null\"\n  })\n  void testApplyToString(String original, String expected) {\n    var policy = new Remove(fieldName -> true);\n    assertThat(policy.applyToString(original)).isEqualTo(expected);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/ReplaceTest.java",
    "content": "package com.provectus.kafka.ui.service.masking.policies;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport com.fasterxml.jackson.databind.node.ContainerNode;\nimport java.util.List;\nimport java.util.stream.Stream;\nimport lombok.SneakyThrows;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.CsvSource;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nclass ReplaceTest {\n\n  private static final FieldsSelector FIELDS_SELECTOR = fieldName -> List.of(\"id\", \"name\").contains(fieldName);\n  private static final String REPLACEMENT_STRING = \"***\";\n\n  @ParameterizedTest\n  @MethodSource\n  void testApplyToJsonContainer(FieldsSelector fieldsSelector, ContainerNode<?> original, ContainerNode<?>  expected) {\n    var policy = new Replace(fieldsSelector, REPLACEMENT_STRING);\n    assertThat(policy.applyToJsonContainer(original)).isEqualTo(expected);\n  }\n\n  private static Stream<Arguments> testApplyToJsonContainer() {\n    return Stream.of(\n        Arguments.of(\n            FIELDS_SELECTOR,\n            parse(\"{ \\\"id\\\": 123, \\\"name\\\": { \\\"first\\\": \\\"James\\\", \\\"surname\\\": \\\"Bond777!\\\"}}\"),\n            parse(\"{ \\\"id\\\": \\\"***\\\", \\\"name\\\": { \\\"first\\\": \\\"***\\\", \\\"surname\\\": \\\"***\\\"}}\")\n        ),\n        Arguments.of(\n            FIELDS_SELECTOR,\n            parse(\"[{ \\\"id\\\": 123, \\\"f2\\\": 234}, { \\\"name\\\": \\\"1.2\\\", \\\"f2\\\": 345} ]\"),\n            parse(\"[{ \\\"id\\\": \\\"***\\\", \\\"f2\\\": 234}, { \\\"name\\\": \\\"***\\\", \\\"f2\\\": 345} ]\")\n        ),\n        Arguments.of(\n            FIELDS_SELECTOR,\n            parse(\"{ \\\"outer\\\": { \\\"f1\\\": \\\"James\\\", \\\"name\\\": \\\"Bond777!\\\"}}\"),\n            parse(\"{ \\\"outer\\\": { \\\"f1\\\": \\\"James\\\", \\\"name\\\": \\\"***\\\"}}\")\n        ),\n        Arguments.of(\n            (FieldsSelector) (fieldName -> true),\n            parse(\"{ \\\"outer\\\": { \\\"f1\\\": \\\"v1\\\", \\\"f2\\\": \\\"v2\\\", \\\"inner\\\" : {\\\"if1\\\": \\\"iv1\\\"}}}\"),\n            parse(\"{ \\\"outer\\\": { \\\"f1\\\": \\\"***\\\", \\\"f2\\\": \\\"***\\\", \\\"inner\\\" : {\\\"if1\\\": \\\"***\\\"}}}}\")\n        )\n    );\n  }\n\n  @SneakyThrows\n  private static JsonNode parse(String str) {\n    return new JsonMapper().readTree(str);\n  }\n\n  @ParameterizedTest\n  @CsvSource({\n      \"Some string?!1, ***\",\n      \"1.24343, ***\",\n      \"null, ***\"\n  })\n  void testApplyToString(String original, String expected) {\n    var policy = new Replace(fieldName -> true, REPLACEMENT_STRING);\n    assertThat(policy.applyToString(original)).isEqualTo(expected);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/metrics/JmxMetricsFormatterTest.java",
    "content": "package com.provectus.kafka.ui.service.metrics;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.math.BigDecimal;\nimport java.util.List;\nimport java.util.Map;\nimport javax.management.MBeanAttributeInfo;\nimport javax.management.ObjectName;\nimport org.assertj.core.data.Offset;\nimport org.junit.jupiter.api.Test;\n\nclass JmxMetricsFormatterTest {\n\n  /**\n   * Original format is <a href=\"https://github.com/prometheus/jmx_exporter#default-format\">here</a>.\n   */\n  @Test\n  void convertsJmxMetricsAccordingToJmxExporterFormat() throws Exception {\n    List<RawMetric> metrics = JmxMetricsFormatter.constructMetricsList(\n        new ObjectName(\n            \"kafka.server:type=Some.BrokerTopic-Metrics,name=BytesOutPer-Sec,topic=test,some-lbl=123\"),\n        new MBeanAttributeInfo[] {\n            createMbeanInfo(\"FifteenMinuteRate\"),\n            createMbeanInfo(\"Mean\"),\n            createMbeanInfo(\"Calls-count\"),\n            createMbeanInfo(\"SkipValue\"),\n        },\n        new Object[] {\n            123.0,\n            100.0,\n            10L,\n            \"string values not supported\"\n        }\n    );\n\n    assertThat(metrics).hasSize(3);\n\n    assertMetricsEqual(\n        RawMetric.create(\n            \"kafka_server_Some_BrokerTopic_Metrics_FifteenMinuteRate\",\n            Map.of(\"name\", \"BytesOutPer-Sec\", \"topic\", \"test\",  \"some_lbl\", \"123\"),\n            BigDecimal.valueOf(123.0)\n        ),\n        metrics.get(0)\n    );\n\n    assertMetricsEqual(\n        RawMetric.create(\n            \"kafka_server_Some_BrokerTopic_Metrics_Mean\",\n            Map.of(\"name\", \"BytesOutPer-Sec\", \"topic\", \"test\", \"some_lbl\", \"123\"),\n            BigDecimal.valueOf(100.0)\n        ),\n        metrics.get(1)\n    );\n\n    assertMetricsEqual(\n        RawMetric.create(\n            \"kafka_server_Some_BrokerTopic_Metrics_Calls_count\",\n            Map.of(\"name\", \"BytesOutPer-Sec\", \"topic\", \"test\", \"some_lbl\", \"123\"),\n            BigDecimal.valueOf(10)\n        ),\n        metrics.get(2)\n    );\n  }\n\n  private static MBeanAttributeInfo createMbeanInfo(String name) {\n    return new MBeanAttributeInfo(name, \"sometype-notused\", null, true, true, false, null);\n  }\n\n  private void assertMetricsEqual(RawMetric expected, RawMetric actual) {\n    assertThat(actual.name()).isEqualTo(expected.name());\n    assertThat(actual.labels()).isEqualTo(expected.labels());\n    assertThat(actual.value()).isCloseTo(expected.value(), Offset.offset(new BigDecimal(\"0.001\")));\n  }\n\n}"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/metrics/PrometheusEndpointMetricsParserTest.java",
    "content": "package com.provectus.kafka.ui.service.metrics;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.Map;\nimport java.util.Optional;\nimport org.junit.jupiter.api.Test;\n\nclass PrometheusEndpointMetricsParserTest {\n\n  @Test\n  void test() {\n    String metricsString =\n        \"kafka_server_BrokerTopicMetrics_FifteenMinuteRate\"\n            + \"{name=\\\"BytesOutPerSec\\\",topic=\\\"__confluent.support.metrics\\\",} 123.1234\";\n\n    Optional<RawMetric> parsedOpt = PrometheusEndpointMetricsParser.parse(metricsString);\n\n    assertThat(parsedOpt).hasValueSatisfying(metric -> {\n      assertThat(metric.name()).isEqualTo(\"kafka_server_BrokerTopicMetrics_FifteenMinuteRate\");\n      assertThat(metric.value()).isEqualTo(\"123.1234\");\n      assertThat(metric.labels()).containsExactlyEntriesOf(\n          Map.of(\n              \"name\", \"BytesOutPerSec\",\n              \"topic\", \"__confluent.support.metrics\"\n          ));\n    });\n  }\n\n}"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/metrics/PrometheusMetricsRetrieverTest.java",
    "content": "package com.provectus.kafka.ui.service.metrics;\n\nimport com.provectus.kafka.ui.model.MetricsConfig;\nimport java.io.IOException;\nimport java.math.BigDecimal;\nimport java.util.List;\nimport java.util.Map;\nimport okhttp3.mockwebserver.MockResponse;\nimport okhttp3.mockwebserver.MockWebServer;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.web.reactive.function.client.WebClient;\nimport reactor.test.StepVerifier;\n\nclass PrometheusMetricsRetrieverTest {\n\n  private final PrometheusMetricsRetriever retriever = new PrometheusMetricsRetriever();\n\n  private final MockWebServer mockWebServer = new MockWebServer();\n\n  @BeforeEach\n  void startMockServer() throws IOException {\n    mockWebServer.start();\n  }\n\n  @AfterEach\n  void stopMockServer() throws IOException {\n    mockWebServer.close();\n  }\n\n  @Test\n  void callsMetricsEndpointAndConvertsResponceToRawMetric() {\n    var url = mockWebServer.url(\"/metrics\");\n    mockWebServer.enqueue(prepareResponse());\n\n    MetricsConfig metricsConfig = prepareMetricsConfig(url.port(), null, null);\n\n    StepVerifier.create(retriever.retrieve(WebClient.create(), url.host(), metricsConfig))\n        .expectNextSequence(expectedRawMetrics())\n        // third metric should not be present, since it has \"NaN\" value\n        .verifyComplete();\n  }\n\n  @Test\n  void callsSecureMetricsEndpointAndConvertsResponceToRawMetric() {\n    var url = mockWebServer.url(\"/metrics\");\n    mockWebServer.enqueue(prepareResponse());\n\n\n    MetricsConfig metricsConfig = prepareMetricsConfig(url.port(), \"username\", \"password\");\n\n    StepVerifier.create(retriever.retrieve(WebClient.create(), url.host(), metricsConfig))\n        .expectNextSequence(expectedRawMetrics())\n        // third metric should not be present, since it has \"NaN\" value\n        .verifyComplete();\n  }\n\n  MockResponse prepareResponse() {\n    // body copied from real jmx exporter\n    return new MockResponse().setBody(\n        \"# HELP kafka_server_KafkaRequestHandlerPool_FifteenMinuteRate Attribute exposed for management \\n\"\n            + \"# TYPE kafka_server_KafkaRequestHandlerPool_FifteenMinuteRate untyped\\n\"\n            + \"kafka_server_KafkaRequestHandlerPool_FifteenMinuteRate{name=\\\"RequestHandlerAvgIdlePercent\\\",} 0.898\\n\"\n            + \"# HELP kafka_server_socket_server_metrics_request_size_avg The average size of requests sent. \\n\"\n            + \"# TYPE kafka_server_socket_server_metrics_request_size_avg untyped\\n\"\n            + \"kafka_server_socket_server_metrics_request_size_avg{listener=\\\"PLAIN\\\",networkProcessor=\\\"1\\\",} 101.1\\n\"\n            + \"kafka_server_socket_server_metrics_request_size_avg{listener=\\\"PLAIN2\\\",networkProcessor=\\\"5\\\",} NaN\"\n    );\n  }\n\n  MetricsConfig prepareMetricsConfig(Integer port, String username, String password) {\n    return MetricsConfig.builder()\n        .ssl(false)\n        .port(port)\n        .type(MetricsConfig.PROMETHEUS_METRICS_TYPE)\n        .username(username)\n        .password(password)\n        .build();\n  }\n\n  List<RawMetric> expectedRawMetrics() {\n\n    var firstMetric = RawMetric.create(\n        \"kafka_server_KafkaRequestHandlerPool_FifteenMinuteRate\",\n        Map.of(\"name\", \"RequestHandlerAvgIdlePercent\"),\n        new BigDecimal(\"0.898\")\n    );\n\n    var secondMetric = RawMetric.create(\n        \"kafka_server_socket_server_metrics_request_size_avg\",\n        Map.of(\"listener\", \"PLAIN\", \"networkProcessor\", \"1\"),\n        new BigDecimal(\"101.1\")\n    );\n    return List.of(firstMetric, secondMetric);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/metrics/WellKnownMetricsTest.java",
    "content": "package com.provectus.kafka.ui.service.metrics;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.provectus.kafka.ui.model.Metrics;\nimport java.math.BigDecimal;\nimport java.util.Arrays;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.apache.kafka.common.Node;\nimport org.junit.jupiter.api.Test;\n\nclass WellKnownMetricsTest {\n\n  private final WellKnownMetrics wellKnownMetrics = new WellKnownMetrics();\n\n  @Test\n  void bytesIoTopicMetricsPopulated() {\n    populateWith(\n        new Node(0, \"host\", 123),\n        \"kafka_server_BrokerTopicMetrics_FifteenMinuteRate{name=\\\"BytesInPerSec\\\",topic=\\\"test-topic\\\",} 1.0\",\n        \"kafka_server_BrokerTopicMetrics_FifteenMinuteRate{name=\\\"BytesOutPerSec\\\",topic=\\\"test-topic\\\",} 2.0\",\n        \"kafka_server_brokertopicmetrics_fifteenminuterate{name=\\\"bytesinpersec\\\",topic=\\\"test-topic\\\",} 1.0\",\n        \"kafka_server_brokertopicmetrics_fifteenminuterate{name=\\\"bytesoutpersec\\\",topic=\\\"test-topic\\\",} 2.0\",\n        \"some_unknown_prefix_brokertopicmetrics_fifteenminuterate{name=\\\"bytesinpersec\\\",topic=\\\"test-topic\\\",} 1.0\",\n        \"some_unknown_prefix_brokertopicmetrics_fifteenminuterate{name=\\\"bytesoutpersec\\\",topic=\\\"test-topic\\\",} 2.0\"\n    );\n    assertThat(wellKnownMetrics.bytesInFifteenMinuteRate)\n        .containsEntry(\"test-topic\", new BigDecimal(\"3.0\"));\n    assertThat(wellKnownMetrics.bytesOutFifteenMinuteRate)\n        .containsEntry(\"test-topic\", new BigDecimal(\"6.0\"));\n  }\n\n  @Test\n  void bytesIoBrokerMetricsPopulated() {\n    populateWith(\n        new Node(1, \"host1\", 123),\n        \"kafka_server_BrokerTopicMetrics_FifteenMinuteRate{name=\\\"BytesInPerSec\\\",} 1.0\",\n        \"kafka_server_BrokerTopicMetrics_FifteenMinuteRate{name=\\\"BytesOutPerSec\\\",} 2.0\"\n    );\n    populateWith(\n        new Node(2, \"host2\", 345),\n        \"some_unknown_prefix_brokertopicmetrics_fifteenminuterate{name=\\\"bytesinpersec\\\",} 10.0\",\n        \"some_unknown_prefix_brokertopicmetrics_fifteenminuterate{name=\\\"bytesoutpersec\\\",} 20.0\"\n    );\n\n    assertThat(wellKnownMetrics.brokerBytesInFifteenMinuteRate)\n        .hasSize(2)\n        .containsEntry(1, new BigDecimal(\"1.0\"))\n        .containsEntry(2, new BigDecimal(\"10.0\"));\n\n    assertThat(wellKnownMetrics.brokerBytesOutFifteenMinuteRate)\n        .hasSize(2)\n        .containsEntry(1, new BigDecimal(\"2.0\"))\n        .containsEntry(2, new BigDecimal(\"20.0\"));\n  }\n\n  @Test\n  void appliesInnerStateToMetricsBuilder() {\n    //filling per topic io rates\n    wellKnownMetrics.bytesInFifteenMinuteRate.put(\"topic\", new BigDecimal(1));\n    wellKnownMetrics.bytesOutFifteenMinuteRate.put(\"topic\", new BigDecimal(2));\n\n    //filling per broker io rates\n    wellKnownMetrics.brokerBytesInFifteenMinuteRate.put(1, new BigDecimal(1));\n    wellKnownMetrics.brokerBytesOutFifteenMinuteRate.put(1, new BigDecimal(2));\n    wellKnownMetrics.brokerBytesInFifteenMinuteRate.put(2, new BigDecimal(10));\n    wellKnownMetrics.brokerBytesOutFifteenMinuteRate.put(2, new BigDecimal(20));\n\n    Metrics.MetricsBuilder builder = Metrics.builder();\n    wellKnownMetrics.apply(builder);\n    var metrics = builder.build();\n\n    // checking per topic io rates\n    assertThat(metrics.getTopicBytesInPerSec()).containsExactlyEntriesOf(wellKnownMetrics.bytesInFifteenMinuteRate);\n    assertThat(metrics.getTopicBytesOutPerSec()).containsExactlyEntriesOf(wellKnownMetrics.bytesOutFifteenMinuteRate);\n\n    // checking per broker io rates\n    assertThat(metrics.getBrokerBytesInPerSec()).containsExactlyInAnyOrderEntriesOf(\n        Map.of(1, new BigDecimal(1), 2, new BigDecimal(10)));\n    assertThat(metrics.getBrokerBytesOutPerSec()).containsExactlyInAnyOrderEntriesOf(\n        Map.of(1, new BigDecimal(2), 2, new BigDecimal(20)));\n  }\n\n  private void populateWith(Node n, String... prometheusMetric) {\n    Arrays.stream(prometheusMetric)\n        .map(PrometheusEndpointMetricsParser::parse)\n        .filter(Optional::isPresent)\n        .map(Optional::get)\n        .forEach(m -> wellKnownMetrics.populate(n, m));\n  }\n\n}"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/AccessControlServiceMock.java",
    "content": "package com.provectus.kafka.ui.util;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyString;\nimport static org.mockito.Mockito.when;\n\nimport com.provectus.kafka.ui.service.rbac.AccessControlService;\nimport org.mockito.Mockito;\nimport reactor.core.publisher.Mono;\n\npublic class AccessControlServiceMock {\n\n  public AccessControlService getMock() {\n    AccessControlService mock = Mockito.mock(AccessControlService.class);\n\n    when(mock.validateAccess(any())).thenReturn(Mono.empty());\n    when(mock.isSchemaAccessible(anyString(), anyString())).thenReturn(Mono.just(true));\n\n    when(mock.filterViewableTopics(any(), any())).then(invocation -> Mono.just(invocation.getArgument(0)));\n\n    return mock;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/DynamicConfigOperationsTest.java",
    "content": "package com.provectus.kafka.ui.util;\n\nimport static com.provectus.kafka.ui.util.DynamicConfigOperations.DYNAMIC_CONFIG_ENABLED_ENV_PROPERTY;\nimport static com.provectus.kafka.ui.util.DynamicConfigOperations.DYNAMIC_CONFIG_PATH_ENV_PROPERTY;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport com.provectus.kafka.ui.config.ClustersProperties;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport javax.annotation.Nullable;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.io.TempDir;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.CsvSource;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.springframework.core.env.ConfigurableEnvironment;\nimport org.springframework.core.env.MapPropertySource;\nimport org.springframework.core.env.MutablePropertySources;\nimport org.springframework.core.env.PropertySource;\n\nclass DynamicConfigOperationsTest {\n\n  private static final String SAMPLE_YAML_CONFIG = \"\"\"\n       kafka:\n        clusters:\n          - name: test\n            bootstrapServers: localhost:9092\n      \"\"\";\n\n  private final ConfigurableApplicationContext ctxMock = mock(ConfigurableApplicationContext.class);\n  private final ConfigurableEnvironment envMock = mock(ConfigurableEnvironment.class);\n\n  private final DynamicConfigOperations ops = new DynamicConfigOperations(ctxMock);\n\n  @TempDir\n  private Path tmpDir;\n\n  @BeforeEach\n  void initMocks() {\n    when(ctxMock.getEnvironment()).thenReturn(envMock);\n  }\n\n  @Test\n  void initializerAddsDynamicPropertySourceIfAllEnvVarsAreSet() throws Exception {\n    Path propsFilePath = tmpDir.resolve(\"props.yaml\");\n    Files.writeString(propsFilePath, SAMPLE_YAML_CONFIG, StandardOpenOption.CREATE);\n\n    MutablePropertySources propertySources = new MutablePropertySources();\n    propertySources.addFirst(new MapPropertySource(\"test\", Map.of(\"testK\", \"testV\")));\n\n    when(envMock.getPropertySources()).thenReturn(propertySources);\n    mockEnvWithVars(Map.of(\n        DYNAMIC_CONFIG_ENABLED_ENV_PROPERTY, \"true\",\n        DYNAMIC_CONFIG_PATH_ENV_PROPERTY, propsFilePath.toString()\n    ));\n\n    DynamicConfigOperations.dynamicConfigPropertiesInitializer().initialize(ctxMock);\n\n    assertThat(propertySources.size()).isEqualTo(2);\n    assertThat(propertySources.stream())\n        .element(0)\n        .extracting(PropertySource::getName)\n        .isEqualTo(\"dynamicProperties\");\n  }\n\n  @ParameterizedTest\n  @CsvSource({\n      \"false, /tmp/conf.yaml\",\n      \"true, \",\n      \", /tmp/conf.yaml\",\n      \",\",\n      \"true, /tmp/conf.yaml\", //vars set, but file doesn't exist\n  })\n  void initializerDoNothingIfAnyOfEnvVarsNotSet(@Nullable String enabledVar, @Nullable String pathVar) {\n    var vars = new HashMap<String, Object>(); // using HashMap to keep null values\n    vars.put(DYNAMIC_CONFIG_ENABLED_ENV_PROPERTY, enabledVar);\n    vars.put(DYNAMIC_CONFIG_PATH_ENV_PROPERTY, pathVar);\n    mockEnvWithVars(vars);\n\n    DynamicConfigOperations.dynamicConfigPropertiesInitializer().initialize(ctxMock);\n    verify(envMock, times(0)).getPropertySources();\n  }\n\n  @ParameterizedTest\n  @ValueSource(booleans = {true, false})\n  void persistRewritesOrCreateConfigFile(boolean exists) throws Exception {\n    Path propsFilePath = tmpDir.resolve(\"props.yaml\");\n    if (exists) {\n      Files.writeString(propsFilePath, SAMPLE_YAML_CONFIG, StandardOpenOption.CREATE);\n    }\n\n    mockEnvWithVars(Map.of(\n        DYNAMIC_CONFIG_ENABLED_ENV_PROPERTY, \"true\",\n        DYNAMIC_CONFIG_PATH_ENV_PROPERTY, propsFilePath.toString()\n    ));\n\n    var overrideProps = new ClustersProperties();\n    var cluster = new ClustersProperties.Cluster();\n    cluster.setName(\"newName\");\n    overrideProps.setClusters(List.of(cluster));\n\n    ops.persist(\n        DynamicConfigOperations.PropertiesStructure.builder()\n            .kafka(overrideProps)\n            .build()\n    );\n\n    assertThat(ops.loadDynamicPropertySource())\n        .get()\n        .extracting(ps -> ps.getProperty(\"kafka.clusters[0].name\"))\n        .isEqualTo(\"newName\");\n  }\n\n  private void mockEnvWithVars(Map<String, Object> envVars) {\n    envVars.forEach((k, v) -> when(envMock.getProperty(k)).thenReturn((String) v));\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/GithubReleaseInfoTest.java",
    "content": "package com.provectus.kafka.ui.util;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.IOException;\nimport java.time.Duration;\nimport okhttp3.mockwebserver.MockResponse;\nimport okhttp3.mockwebserver.MockWebServer;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport reactor.test.StepVerifier;\n\nclass GithubReleaseInfoTest {\n\n  private final MockWebServer mockWebServer = new MockWebServer();\n\n  @BeforeEach\n  void startMockServer() throws IOException {\n    mockWebServer.start();\n  }\n\n  @AfterEach\n  void stopMockServer() throws IOException {\n    mockWebServer.close();\n  }\n\n  @Test\n  void test() {\n    mockWebServer.enqueue(new MockResponse()\n        .addHeader(\"content-type: application/json\")\n        .setBody(\"\"\"\n            {\n              \"published_at\": \"2023-03-09T16:11:31Z\",\n              \"tag_name\": \"v0.6.0\",\n              \"html_url\": \"https://github.com/provectus/kafka-ui/releases/tag/v0.6.0\",\n              \"some_unused_prop\": \"ololo\"\n            }\n            \"\"\"));\n    var url = mockWebServer.url(\"repos/provectus/kafka-ui/releases/latest\").toString();\n\n    var infoHolder = new GithubReleaseInfo(url);\n    infoHolder.refresh().block();\n\n    var i = infoHolder.get();\n    assertThat(i.html_url())\n        .isEqualTo(\"https://github.com/provectus/kafka-ui/releases/tag/v0.6.0\");\n    assertThat(i.published_at())\n        .isEqualTo(\"2023-03-09T16:11:31Z\");\n    assertThat(i.tag_name())\n        .isEqualTo(\"v0.6.0\");\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/PollingThrottlerTest.java",
    "content": "package com.provectus.kafka.ui.util;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.data.Percentage.withPercentage;\n\nimport com.google.common.base.Stopwatch;\nimport com.google.common.util.concurrent.RateLimiter;\nimport com.provectus.kafka.ui.emitter.PollingThrottler;\nimport java.util.concurrent.ThreadLocalRandom;\nimport java.util.concurrent.TimeUnit;\nimport org.junit.jupiter.api.Test;\n\nclass PollingThrottlerTest {\n\n  @Test\n  void testTrafficThrottled() {\n    var throttler = new PollingThrottler(\"test\", RateLimiter.create(1000));\n    long polledBytes = 0;\n    var stopwatch = Stopwatch.createStarted();\n    while (stopwatch.elapsed(TimeUnit.SECONDS) < 1) {\n      int newPolled = ThreadLocalRandom.current().nextInt(10);\n      throttler.throttleAfterPoll(newPolled);\n      polledBytes += newPolled;\n    }\n    assertThat(polledBytes).isCloseTo(1000, withPercentage(3.0));\n  }\n\n  @Test\n  void noopThrottlerDoNotLimitPolling() {\n    var noopThrottler = PollingThrottler.noop();\n    var stopwatch = Stopwatch.createStarted();\n    // emulating that we polled 1GB\n    for (int i = 0; i < 1024; i++) {\n      noopThrottler.throttleAfterPoll(1024 * 1024);\n    }\n    // checking that were are able to \"poll\" 1GB in less than a second\n    assertThat(stopwatch.elapsed().getSeconds()).isLessThan(1);\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/ReactiveFailoverTest.java",
    "content": "package com.provectus.kafka.ui.util;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.google.common.base.Preconditions;\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.function.Consumer;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\nimport java.util.stream.Stream;\nimport org.junit.jupiter.api.Test;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\n\nclass ReactiveFailoverTest {\n\n  private static final String NO_AVAILABLE_PUBLISHERS_MSG = \"no active publishers!\";\n  private static final Predicate<Throwable> FAILING_EXCEPTION_FILTER = th -> th.getMessage().contains(\"fail!\");\n  private static final Supplier<Throwable> FAILING_EXCEPTION_SUPPLIER = () -> new IllegalStateException(\"fail!\");\n  private static final Duration RETRY_PERIOD = Duration.ofMillis(300);\n\n  private final List<Publisher> publishers = Stream.generate(Publisher::new).limit(3).toList();\n\n  private final ReactiveFailover<Publisher> failover = ReactiveFailover.create(\n      publishers,\n      FAILING_EXCEPTION_FILTER,\n      NO_AVAILABLE_PUBLISHERS_MSG,\n      RETRY_PERIOD\n  );\n\n  @Test\n  void testMonoFailoverCycle() throws InterruptedException {\n    // starting with first publisher:\n    // 0 -> ok : ok\n    monoCheck(\n        Map.of(\n            0, okMono()\n        ),\n        List.of(0),\n        step -> step.expectNextCount(1).verifyComplete()\n    );\n\n    // 0 -> fail, 1 -> ok : ok\n    monoCheck(\n        Map.of(\n            0, failingMono(),\n            1, okMono()\n        ),\n        List.of(0, 1),\n        step -> step.expectNextCount(1).verifyComplete()\n    );\n\n    // 0.failed, 1.failed, 2 -> ok : ok\n    monoCheck(\n        Map.of(\n            1, failingMono(),\n            2, okMono()\n        ),\n        List.of(1, 2),\n        step -> step.expectNextCount(1).verifyComplete()\n    );\n\n    // 0.failed, 1.failed, 2 -> fail : failing exception\n    monoCheck(\n        Map.of(\n            2, failingMono()\n        ),\n        List.of(2),\n        step -> step.verifyErrorMessage(FAILING_EXCEPTION_SUPPLIER.get().getMessage())\n    );\n\n    // 0.failed, 1.failed, 2.failed : No alive publisher exception\n    monoCheck(\n        Map.of(),\n        List.of(),\n        step -> step.verifyErrorMessage(NO_AVAILABLE_PUBLISHERS_MSG)\n    );\n\n    // resetting retry: all publishers became alive: 0.ok, 1.ok, 2.ok\n    Thread.sleep(RETRY_PERIOD.toMillis() + 1);\n\n    // starting with last errored publisher:\n    // 2 -> fail, 0 -> fail, 1 -> ok : ok\n    monoCheck(\n        Map.of(\n            2, failingMono(),\n            0, failingMono(),\n            1, okMono()\n        ),\n        List.of(2, 0, 1),\n        step -> step.expectNextCount(1).verifyComplete()\n    );\n\n    // 1 -> ok : ok\n    monoCheck(\n        Map.of(\n            1, okMono()\n        ),\n        List.of(1),\n        step -> step.expectNextCount(1).verifyComplete()\n    );\n  }\n\n  @Test\n  void testFluxFailoverCycle() throws InterruptedException {\n    // starting with first publisher:\n    // 0 -> ok : ok\n    fluxCheck(\n        Map.of(\n            0, okFlux()\n        ),\n        List.of(0),\n        step -> step.expectNextCount(1).verifyComplete()\n    );\n\n    // 0 -> fail, 1 -> ok : ok\n    fluxCheck(\n        Map.of(\n            0, failingFlux(),\n            1, okFlux()\n        ),\n        List.of(0, 1),\n        step -> step.expectNextCount(1).verifyComplete()\n    );\n\n    // 0.failed, 1.failed, 2 -> ok : ok\n    fluxCheck(\n        Map.of(\n            1, failingFlux(),\n            2, okFlux()\n        ),\n        List.of(1, 2),\n        step -> step.expectNextCount(1).verifyComplete()\n    );\n\n    // 0.failed, 1.failed, 2 -> fail : failing exception\n    fluxCheck(\n        Map.of(\n            2, failingFlux()\n        ),\n        List.of(2),\n        step -> step.verifyErrorMessage(FAILING_EXCEPTION_SUPPLIER.get().getMessage())\n    );\n\n    // 0.failed, 1.failed, 2.failed : No alive publisher exception\n    fluxCheck(\n        Map.of(),\n        List.of(),\n        step -> step.verifyErrorMessage(NO_AVAILABLE_PUBLISHERS_MSG)\n    );\n\n    // resetting retry: all publishers became alive: 0.ok, 1.ok, 2.ok\n    Thread.sleep(RETRY_PERIOD.toMillis() + 1);\n\n    // starting with last errored publisher:\n    // 2 -> fail, 0 -> fail, 1 -> ok : ok\n    fluxCheck(\n        Map.of(\n            2, failingFlux(),\n            0, failingFlux(),\n            1, okFlux()\n        ),\n        List.of(2, 0, 1),\n        step -> step.expectNextCount(1).verifyComplete()\n    );\n\n    // 1 -> ok : ok\n    fluxCheck(\n        Map.of(\n            1, okFlux()\n        ),\n        List.of(1),\n        step -> step.expectNextCount(1).verifyComplete()\n    );\n  }\n\n  private void monoCheck(Map<Integer, Mono<String>> mock,\n                         List<Integer> publishersToBeCalled, // for checking calls order\n                         Consumer<StepVerifier.Step<?>> stepVerifier) {\n    AtomicInteger calledCount = new AtomicInteger();\n    var mono = failover.mono(publisher -> {\n      int calledPublisherIdx = publishers.indexOf(publisher);\n      assertThat(calledPublisherIdx).isEqualTo(publishersToBeCalled.get(calledCount.getAndIncrement()));\n      return Preconditions.checkNotNull(\n          mock.get(calledPublisherIdx),\n          \"Mono result not set for publisher %d\", calledPublisherIdx\n      );\n    });\n    stepVerifier.accept(StepVerifier.create(mono));\n    assertThat(calledCount.get()).isEqualTo(publishersToBeCalled.size());\n  }\n\n\n  private void fluxCheck(Map<Integer, Flux<String>> mock,\n                         List<Integer> publishersToBeCalled, // for checking calls order\n                         Consumer<StepVerifier.Step<?>> stepVerifier) {\n    AtomicInteger calledCount = new AtomicInteger();\n    var flux = failover.flux(publisher -> {\n      int calledPublisherIdx = publishers.indexOf(publisher);\n      assertThat(calledPublisherIdx).isEqualTo(publishersToBeCalled.get(calledCount.getAndIncrement()));\n      return Preconditions.checkNotNull(\n          mock.get(calledPublisherIdx),\n          \"Mono result not set for publisher %d\", calledPublisherIdx\n      );\n    });\n    stepVerifier.accept(StepVerifier.create(flux));\n    assertThat(calledCount.get()).isEqualTo(publishersToBeCalled.size());\n  }\n\n  private Flux<String> okFlux() {\n    return Flux.just(\"ok\");\n  }\n\n  private Flux<String> failingFlux() {\n    return Flux.error(FAILING_EXCEPTION_SUPPLIER);\n  }\n\n  private Mono<String> okMono() {\n    return Mono.just(\"ok\");\n  }\n\n  private Mono<String> failingMono() {\n    return Mono.error(FAILING_EXCEPTION_SUPPLIER);\n  }\n\n  public static class Publisher {\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/AvroJsonSchemaConverterTest.java",
    "content": "package com.provectus.kafka.ui.util.jsonschema;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport lombok.SneakyThrows;\nimport org.apache.avro.Schema;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nclass AvroJsonSchemaConverterTest {\n\n  private AvroJsonSchemaConverter converter;\n  private URI basePath;\n\n  @BeforeEach\n  void init() throws URISyntaxException {\n    converter = new AvroJsonSchemaConverter();\n    basePath = new URI(\"http://example.com/\");\n  }\n\n  @Test\n  void avroConvertTest() {\n    String avroSchema =\n        \" {\"\n            + \"     \\\"type\\\": \\\"record\\\",\"\n            + \"     \\\"name\\\": \\\"Message\\\",\"\n            + \"     \\\"namespace\\\": \\\"com.provectus.kafka\\\",\"\n            + \"     \\\"fields\\\": [\"\n            + \"         {\"\n            + \"             \\\"name\\\": \\\"record\\\",\"\n            + \"             \\\"type\\\": {\"\n            + \"                 \\\"type\\\": \\\"record\\\",\"\n            + \"                 \\\"name\\\": \\\"InnerMessage\\\",\"\n            + \"                 \\\"fields\\\": [\"\n            + \"                     {\"\n            + \"                         \\\"name\\\": \\\"id\\\",\"\n            + \"                         \\\"type\\\": \\\"long\\\"\"\n            + \"                     },\"\n            + \"                     {\"\n            + \"                         \\\"name\\\": \\\"text\\\",\"\n            + \"                         \\\"type\\\": \\\"string\\\"\"\n            + \"                     },\"\n            + \"                     {\"\n            + \"                         \\\"name\\\": \\\"long_text\\\",\"\n            + \"                         \\\"type\\\": [\"\n            + \"                             \\\"null\\\",\"\n            + \"                             \\\"string\\\"\"\n            + \"                         ],\"\n            + \"                         \\\"default\\\": null\"\n            + \"                     },\"\n            + \"                     {\"\n            + \"                         \\\"name\\\": \\\"order\\\",\"\n            + \"                         \\\"type\\\": {\"\n            + \"                        \\\"type\\\": \\\"enum\\\",\"\n            + \"                        \\\"name\\\": \\\"Suit\\\",\"\n            + \"                        \\\"symbols\\\": [\\\"SPADES\\\",\\\"HEARTS\\\",\\\"DIAMONDS\\\",\\\"CLUBS\\\"]\"\n            + \"                         }\"\n            + \"                     },\"\n            + \"                     {\"\n            + \"                         \\\"name\\\": \\\"array\\\",\"\n            + \"                         \\\"type\\\": {\"\n            + \"                             \\\"type\\\": \\\"array\\\",\"\n            + \"                             \\\"items\\\": \\\"string\\\",\"\n            + \"                             \\\"default\\\": []\"\n            + \"                         }\"\n            + \"                     },\"\n            + \"                     {\"\n            + \"                         \\\"name\\\": \\\"map\\\",\"\n            + \"                         \\\"type\\\": {\"\n            + \"                             \\\"type\\\": \\\"map\\\",\"\n            + \"                             \\\"values\\\": \\\"long\\\",\"\n            + \"                             \\\"default\\\": {}\"\n            + \"                         }\"\n            + \"                     }\"\n            + \"                 ]\"\n            + \"             }\"\n            + \"         }\"\n            + \"     ]\"\n            + \" }\";\n\n    String expectedJsonSchema = \"{ \"\n        + \"  \\\"$id\\\" : \\\"http://example.com/Message\\\", \"\n        + \"  \\\"$schema\\\" : \\\"https://json-schema.org/draft/2020-12/schema\\\", \"\n        + \"  \\\"type\\\" : \\\"object\\\", \"\n        + \"  \\\"properties\\\" : { \"\n        + \"    \\\"record\\\" : { \\\"$ref\\\" : \\\"#/definitions/com.provectus.kafka.InnerMessage\\\" } \"\n        + \"  }, \"\n        + \"  \\\"required\\\" : [ \\\"record\\\" ], \"\n        + \"  \\\"definitions\\\" : { \"\n        + \"    \\\"com.provectus.kafka.Message\\\" : { \\\"$ref\\\" : \\\"#\\\" }, \"\n        + \"    \\\"com.provectus.kafka.InnerMessage\\\" : { \"\n        + \"      \\\"type\\\" : \\\"object\\\", \"\n        + \"      \\\"properties\\\" : { \"\n        + \"        \\\"long_text\\\" : { \"\n        + \"          \\\"oneOf\\\" : [ { \"\n        + \"            \\\"type\\\" : \\\"null\\\" \"\n        + \"          }, { \"\n        + \"            \\\"type\\\" : \\\"object\\\", \"\n        + \"            \\\"properties\\\" : { \"\n        + \"              \\\"string\\\" : { \"\n        + \"                \\\"type\\\" : \\\"string\\\" \"\n        + \"              } \"\n        + \"            } \"\n        + \"          } ] \"\n        + \"        }, \"\n        + \"        \\\"array\\\" : { \"\n        + \"          \\\"type\\\" : \\\"array\\\", \"\n        + \"          \\\"items\\\" : { \\\"type\\\" : \\\"string\\\" } \"\n        + \"        }, \"\n        + \"        \\\"id\\\" : { \\\"type\\\" : \\\"integer\\\" }, \"\n        + \"        \\\"text\\\" : { \\\"type\\\" : \\\"string\\\" }, \"\n        + \"        \\\"map\\\" : { \"\n        + \"          \\\"type\\\" : \\\"object\\\", \"\n        + \"          \\\"additionalProperties\\\" : { \\\"type\\\" : \\\"integer\\\" } \"\n        + \"        }, \"\n        + \"        \\\"order\\\" : { \"\n        + \"          \\\"enum\\\" : [ \\\"SPADES\\\", \\\"HEARTS\\\", \\\"DIAMONDS\\\", \\\"CLUBS\\\" ], \"\n        + \"          \\\"type\\\" : \\\"string\\\" \"\n        + \"        } \"\n        + \"      }, \"\n        + \"      \\\"required\\\" : [ \\\"id\\\", \\\"text\\\", \\\"order\\\", \\\"array\\\", \\\"map\\\" ] \"\n        + \"    } \"\n        + \"  } \"\n        + \"}\";\n\n    convertAndCompare(expectedJsonSchema, avroSchema);\n  }\n\n  @Test\n  void testNullableUnions()  {\n    String avroSchema =\n        \" {\"\n            + \"     \\\"type\\\": \\\"record\\\",\"\n            + \"     \\\"name\\\": \\\"Message\\\",\"\n            + \"     \\\"namespace\\\": \\\"com.provectus.kafka\\\",\"\n            + \"     \\\"fields\\\": [\"\n            + \"                     {\"\n            + \"                         \\\"name\\\": \\\"text\\\",\"\n            + \"                         \\\"type\\\": [\"\n            + \"                             \\\"null\\\",\"\n            + \"                             \\\"string\\\"\"\n            + \"                         ],\"\n            + \"                         \\\"default\\\": null\"\n            + \"                     },\"\n            + \"                     {\"\n            + \"                         \\\"name\\\": \\\"value\\\",\"\n            + \"                         \\\"type\\\": [\"\n            + \"                             \\\"null\\\",\"\n            + \"                             \\\"string\\\",\"\n            + \"                             \\\"long\\\"\"\n            + \"                         ],\"\n            + \"                         \\\"default\\\": null\"\n            + \"                     }\"\n            + \"     ]\"\n            + \" }\";\n\n    String expectedJsonSchema =\n        \"{\\\"$id\\\":\\\"http://example.com/Message\\\",\"\n        + \"\\\"$schema\\\":\\\"https://json-schema.org/draft/2020-12/schema\\\",\"\n        + \"\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"text\\\":\"\n        + \"{\\\"oneOf\\\":[{\\\"type\\\":\\\"null\\\"},{\\\"type\\\":\\\"object\\\",\"\n        + \"\\\"properties\\\":{\\\"string\\\":{\\\"type\\\":\\\"string\\\"}}}]},\\\"value\\\":\"\n        + \"{\\\"oneOf\\\":[{\\\"type\\\":\\\"null\\\"},{\\\"type\\\":\\\"object\\\",\"\n        + \"\\\"properties\\\":{\\\"string\\\":{\\\"type\\\":\\\"string\\\"},\\\"long\\\":{\\\"type\\\":\\\"integer\\\"}}}]}},\"\n        + \"\\\"definitions\\\" : { \\\"com.provectus.kafka.Message\\\" : { \\\"$ref\\\" : \\\"#\\\" }}}\";\n\n    convertAndCompare(expectedJsonSchema, avroSchema);\n  }\n\n  @Test\n  void testRecordReferences() {\n    String avroSchema =\n        \"{\\n\"\n            + \"    \\\"type\\\": \\\"record\\\", \"\n            + \"    \\\"namespace\\\": \\\"n.s\\\", \"\n            + \"    \\\"name\\\": \\\"RootMsg\\\", \"\n            + \"    \\\"fields\\\":\\n\"\n            + \"    [ \"\n            + \"        { \"\n            + \"            \\\"name\\\": \\\"inner1\\\", \"\n            + \"            \\\"type\\\": { \"\n            + \"                \\\"type\\\": \\\"record\\\", \"\n            + \"                \\\"name\\\": \\\"Inner\\\", \"\n            + \"                \\\"fields\\\": [ { \\\"name\\\": \\\"f1\\\", \\\"type\\\": \\\"double\\\" } ] \"\n            + \"            } \"\n            + \"        }, \"\n            + \"        { \"\n            + \"            \\\"name\\\": \\\"inner2\\\", \"\n            + \"            \\\"type\\\": { \"\n            + \"                \\\"type\\\": \\\"record\\\", \"\n            + \"                \\\"namespace\\\": \\\"n.s2\\\", \"\n            + \"                \\\"name\\\": \\\"Inner\\\", \"\n            + \"                \\\"fields\\\": \"\n            + \"                [ { \\\"name\\\": \\\"f1\\\", \\\"type\\\": \\\"double\\\" } ] \"\n            + \"            } \"\n            + \"        }, \"\n            + \"        { \"\n            + \"            \\\"name\\\": \\\"refField\\\", \"\n            + \"            \\\"type\\\": [ \\\"null\\\", \\\"Inner\\\", \\\"n.s2.Inner\\\", \\\"RootMsg\\\" ] \"\n            + \"        } \"\n            + \"    ] \"\n            + \"}\";\n\n    String expectedJsonSchema = \"{ \"\n        + \"  \\\"$id\\\" : \\\"http://example.com/RootMsg\\\", \"\n        + \"  \\\"$schema\\\" : \\\"https://json-schema.org/draft/2020-12/schema\\\", \"\n        + \"  \\\"type\\\" : \\\"object\\\", \"\n        + \"  \\\"properties\\\" : { \"\n        + \"    \\\"inner1\\\" : { \\\"$ref\\\" : \\\"#/definitions/n.s.Inner\\\" }, \"\n        + \"    \\\"inner2\\\" : { \\\"$ref\\\" : \\\"#/definitions/n.s2.Inner\\\" }, \"\n        + \"    \\\"refField\\\" : { \"\n        + \"      \\\"oneOf\\\" : [  \"\n        + \"      { \"\n        + \"        \\\"type\\\" : \\\"null\\\" \"\n        + \"      },  \"\n        + \"      { \"\n        + \"        \\\"type\\\" : \\\"object\\\", \"\n        + \"        \\\"properties\\\" : { \"\n        + \"          \\\"n.s.RootMsg\\\" : { \\\"$ref\\\" : \\\"#/definitions/n.s.RootMsg\\\" }, \"\n        + \"          \\\"n.s2.Inner\\\" : { \\\"$ref\\\" : \\\"#/definitions/n.s2.Inner\\\" }, \"\n        + \"          \\\"n.s.Inner\\\" : { \\\"$ref\\\" : \\\"#/definitions/n.s.Inner\\\" } \"\n        + \"        } \"\n        + \"      } ] \"\n        + \"    } \"\n        + \"  }, \"\n        + \"  \\\"required\\\" : [ \\\"inner1\\\", \\\"inner2\\\" ], \"\n        + \"  \\\"definitions\\\" : { \"\n        + \"    \\\"n.s.RootMsg\\\" : { \\\"$ref\\\" : \\\"#\\\" }, \"\n        + \"    \\\"n.s2.Inner\\\" : { \"\n        + \"      \\\"type\\\" : \\\"object\\\", \"\n        + \"      \\\"properties\\\" : { \\\"f1\\\" : { \\\"type\\\" : \\\"number\\\" } }, \"\n        + \"      \\\"required\\\" : [ \\\"f1\\\" ] \"\n        + \"    }, \"\n        + \"    \\\"n.s.Inner\\\" : { \"\n        + \"      \\\"type\\\" : \\\"object\\\", \"\n        + \"      \\\"properties\\\" : { \\\"f1\\\" : { \\\"type\\\" : \\\"number\\\" } }, \"\n        + \"      \\\"required\\\" : [ \\\"f1\\\" ] \"\n        + \"    } \"\n        + \"  } \"\n        + \"}\";\n\n    convertAndCompare(expectedJsonSchema, avroSchema);\n  }\n\n  @SneakyThrows\n  private void convertAndCompare(String expectedJsonSchema, String sourceAvroSchema) {\n    var parseAvroSchema = new Schema.Parser().parse(sourceAvroSchema);\n    var converted = converter.convert(basePath, parseAvroSchema).toJson();\n    var objectMapper = new ObjectMapper();\n    Assertions.assertEquals(\n        objectMapper.readTree(expectedJsonSchema),\n        objectMapper.readTree(converted)\n    );\n  }\n\n}"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/JsonAvroConversionTest.java",
    "content": "package com.provectus.kafka.ui.util.jsonschema;\n\nimport static com.provectus.kafka.ui.util.jsonschema.JsonAvroConversion.convertAvroToJson;\nimport static com.provectus.kafka.ui.util.jsonschema.JsonAvroConversion.convertJsonToAvro;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport com.fasterxml.jackson.databind.node.BooleanNode;\nimport com.fasterxml.jackson.databind.node.DoubleNode;\nimport com.fasterxml.jackson.databind.node.FloatNode;\nimport com.fasterxml.jackson.databind.node.IntNode;\nimport com.fasterxml.jackson.databind.node.LongNode;\nimport com.fasterxml.jackson.databind.node.TextNode;\nimport com.google.common.primitives.Longs;\nimport com.provectus.kafka.ui.exception.JsonAvroConversionException;\nimport io.confluent.kafka.schemaregistry.avro.AvroSchema;\nimport java.math.BigDecimal;\nimport java.nio.ByteBuffer;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Instant;\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.LocalTime;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport lombok.SneakyThrows;\nimport org.apache.avro.Schema;\nimport org.apache.avro.generic.GenericData;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\n\nclass JsonAvroConversionTest {\n\n  // checking conversion from json to KafkaAvroSerializer-compatible avro objects\n  @Nested\n  class FromJsonToAvro {\n\n    @Test\n    void primitiveRoot() {\n      assertThat(convertJsonToAvro(\"\\\"str\\\"\", createSchema(\"\\\"string\\\"\")))\n          .isEqualTo(\"str\");\n\n      assertThat(convertJsonToAvro(\"123\", createSchema(\"\\\"int\\\"\")))\n          .isEqualTo(123);\n\n      assertThat(convertJsonToAvro(\"123\", createSchema(\"\\\"long\\\"\")))\n          .isEqualTo(123L);\n\n      assertThat(convertJsonToAvro(\"123.123\", createSchema(\"\\\"float\\\"\")))\n          .isEqualTo(123.123F);\n\n      assertThat(convertJsonToAvro(\"12345.12345\", createSchema(\"\\\"double\\\"\")))\n          .isEqualTo(12345.12345);\n    }\n\n    @Test\n    void primitiveTypedFields() {\n      var schema = createSchema(\n          \"\"\"\n               {\n                 \"type\": \"record\",\n                 \"name\": \"TestAvroRecord\",\n                 \"fields\": [\n                   {\n                     \"name\": \"f_int\",\n                     \"type\": \"int\"\n                   },\n                   {\n                     \"name\": \"f_long\",\n                     \"type\": \"long\"\n                   },\n                   {\n                     \"name\": \"f_string\",\n                     \"type\": \"string\"\n                   },\n                   {\n                     \"name\": \"f_boolean\",\n                     \"type\": \"boolean\"\n                   },\n                   {\n                     \"name\": \"f_float\",\n                     \"type\": \"float\"\n                   },\n                   {\n                     \"name\": \"f_double\",\n                     \"type\": \"double\"\n                   },\n                   {\n                     \"name\": \"f_enum\",\n                     \"type\" : {\n                      \"type\": \"enum\",\n                      \"name\": \"Suit\",\n                      \"symbols\" : [\"SPADES\", \"HEARTS\", \"DIAMONDS\", \"CLUBS\"]\n                     }\n                   },\n                   {\n                     \"name\" : \"f_fixed\",\n                     \"type\" : { \"type\" : \"fixed\" ,\"size\" : 8, \"name\": \"long_encoded\" }\n                   },\n                   {\n                     \"name\" : \"f_bytes\",\n                     \"type\": \"bytes\"\n                   }\n                 ]\n              }\"\"\"\n      );\n\n      String jsonPayload = \"\"\"\n          {\n            \"f_int\": 123,\n            \"f_long\": 4294967294,\n            \"f_string\": \"string here\",\n            \"f_boolean\": true,\n            \"f_float\": 123.1,\n            \"f_double\": 123456.123456,\n            \"f_enum\": \"SPADES\",\n            \"f_fixed\": \"\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\u0004Ò\",\n            \"f_bytes\": \"\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\t)\"\n          }\n          \"\"\";\n\n      var converted = convertJsonToAvro(jsonPayload, schema);\n      assertThat(converted).isInstanceOf(GenericData.Record.class);\n\n      var record = (GenericData.Record) converted;\n      assertThat(record.get(\"f_int\")).isEqualTo(123);\n      assertThat(record.get(\"f_long\")).isEqualTo(4294967294L);\n      assertThat(record.get(\"f_string\")).isEqualTo(\"string here\");\n      assertThat(record.get(\"f_boolean\")).isEqualTo(true);\n      assertThat(record.get(\"f_float\")).isEqualTo(123.1f);\n      assertThat(record.get(\"f_double\")).isEqualTo(123456.123456);\n      assertThat(record.get(\"f_enum\"))\n          .isEqualTo(\n              new GenericData.EnumSymbol(\n                  schema.getField(\"f_enum\").schema(),\n                  \"SPADES\"\n              )\n          );\n      assertThat(((GenericData.Fixed) record.get(\"f_fixed\")).bytes()).isEqualTo(Longs.toByteArray(1234L));\n      assertThat(((ByteBuffer) record.get(\"f_bytes\")).array()).isEqualTo(Longs.toByteArray(2345L));\n    }\n\n    @Test\n    void unionRoot() {\n      var schema = createSchema(\"[ \\\"null\\\", \\\"string\\\", \\\"int\\\" ]\");\n\n      var converted = convertJsonToAvro(\"{\\\"string\\\":\\\"string here\\\"}\", schema);\n      assertThat(converted).isEqualTo(\"string here\");\n\n      converted = convertJsonToAvro(\"{\\\"int\\\": 123}\", schema);\n      assertThat(converted).isEqualTo(123);\n\n      converted = convertJsonToAvro(\"null\", schema);\n      assertThat(converted).isEqualTo(null);\n    }\n\n    @Test\n    void unionField() {\n      var schema = createSchema(\n          \"\"\"\n               {\n                 \"type\": \"record\",\n                 \"namespace\": \"com.test\",\n                 \"name\": \"TestAvroRecord\",\n                 \"fields\": [\n                   {\n                     \"name\": \"f_union\",\n                     \"type\": [ \"null\", \"int\", \"TestAvroRecord\"]\n                   }\n                 ]\n              }\"\"\"\n      );\n\n      String jsonPayload = \"{ \\\"f_union\\\": null }\";\n\n      var record = (GenericData.Record) convertJsonToAvro(jsonPayload, schema);\n      assertThat(record.get(\"f_union\")).isNull();\n\n      jsonPayload = \"{ \\\"f_union\\\": { \\\"int\\\": 123 } }\";\n      record = (GenericData.Record) convertJsonToAvro(jsonPayload, schema);\n      assertThat(record.get(\"f_union\")).isEqualTo(123);\n\n      //short name can be used since there is no clash with other type names\n      jsonPayload = \"{ \\\"f_union\\\": { \\\"TestAvroRecord\\\": { \\\"f_union\\\": { \\\"int\\\": 123  } } } }\";\n      record = (GenericData.Record) convertJsonToAvro(jsonPayload, schema);\n      assertThat(record.get(\"f_union\")).isInstanceOf(GenericData.Record.class);\n      var innerRec = (GenericData.Record) record.get(\"f_union\");\n      assertThat(innerRec.get(\"f_union\")).isEqualTo(123);\n\n      assertThatThrownBy(() ->\n          convertJsonToAvro(\"{ \\\"f_union\\\": { \\\"NotExistingType\\\": 123 } }\", schema)\n      ).isInstanceOf(JsonAvroConversionException.class);\n    }\n\n    @Test\n    void unionFieldWithTypeNamesClash() {\n      var schema = createSchema(\n          \"\"\"\n               {\n                 \"type\": \"record\",\n                 \"namespace\": \"com.test\",\n                 \"name\": \"TestAvroRecord\",\n                 \"fields\": [\n                   {\n                     \"name\": \"nestedClass\",\n                     \"type\": {\n                       \"type\": \"record\",\n                       \"namespace\": \"com.nested\",\n                       \"name\": \"TestAvroRecord\",\n                       \"fields\": [\n                         {\"name\" : \"inner_obj_field\", \"type\": \"int\" }\n                       ]\n                     }\n                   },\n                   {\n                     \"name\": \"f_union\",\n                     \"type\": [ \"null\", \"int\", \"com.test.TestAvroRecord\", \"com.nested.TestAvroRecord\"]\n                   }\n                 ]\n              }\"\"\"\n      );\n      //short name can't can be used since there is a clash with other type names\n      var jsonPayload = \"{ \\\"f_union\\\": { \\\"com.test.TestAvroRecord\\\": { \\\"f_union\\\": { \\\"int\\\": 123  } } } }\";\n      var record = (GenericData.Record) convertJsonToAvro(jsonPayload, schema);\n      assertThat(record.get(\"f_union\")).isInstanceOf(GenericData.Record.class);\n      var innerRec = (GenericData.Record) record.get(\"f_union\");\n      assertThat(innerRec.get(\"f_union\")).isEqualTo(123);\n\n      //short name can't can be used since there is a clash with other type names\n      jsonPayload = \"{ \\\"f_union\\\": { \\\"com.nested.TestAvroRecord\\\": { \\\"inner_obj_field\\\":  234 } } }\";\n      record = (GenericData.Record) convertJsonToAvro(jsonPayload, schema);\n      assertThat(record.get(\"f_union\")).isInstanceOf(GenericData.Record.class);\n      innerRec = (GenericData.Record) record.get(\"f_union\");\n      assertThat(innerRec.get(\"inner_obj_field\")).isEqualTo(234);\n\n      assertThatThrownBy(() ->\n          convertJsonToAvro(\"{ \\\"f_union\\\": { \\\"TestAvroRecord\\\": { \\\"inner_obj_field\\\":  234 } } }\", schema)\n      ).isInstanceOf(JsonAvroConversionException.class);\n    }\n\n    @Test\n    void mapField() {\n      var schema = createSchema(\n          \"\"\"\n               {\n                 \"type\": \"record\",\n                 \"name\": \"TestAvroRecord\",\n                 \"fields\": [\n                   {\n                     \"name\": \"long_map\",\n                     \"type\": {\n                       \"type\": \"map\",\n                       \"values\" : \"long\",\n                       \"default\": {}\n                     }\n                   },\n                   {\n                     \"name\": \"string_map\",\n                     \"type\": {\n                       \"type\": \"map\",\n                       \"values\" : \"string\",\n                       \"default\": {}\n                     }\n                   },\n                   {\n                     \"name\": \"self_ref_map\",\n                     \"type\": {\n                       \"type\": \"map\",\n                       \"values\" : \"TestAvroRecord\",\n                       \"default\": {}\n                     }\n                   }\n                 ]\n              }\"\"\"\n      );\n\n      String jsonPayload = \"\"\"\n          {\n            \"long_map\": {\n              \"k1\": 123,\n              \"k2\": 456\n            },\n            \"string_map\": {\n              \"k3\": \"s1\",\n              \"k4\": \"s2\"\n            },\n            \"self_ref_map\": {\n              \"k5\" : {\n                \"long_map\": { \"_k1\": 222 },\n                \"string_map\": { \"_k2\": \"_s1\" }\n              }\n            }\n          }\n          \"\"\";\n\n      var record = (GenericData.Record) convertJsonToAvro(jsonPayload, schema);\n      assertThat(record.get(\"long_map\"))\n          .isEqualTo(Map.of(\"k1\", 123L, \"k2\", 456L));\n      assertThat(record.get(\"string_map\"))\n          .isEqualTo(Map.of(\"k3\", \"s1\", \"k4\", \"s2\"));\n      assertThat(record.get(\"self_ref_map\"))\n          .isNotNull();\n\n      Map<String, Object> selfRefMapField = (Map<String, Object>) record.get(\"self_ref_map\");\n      assertThat(selfRefMapField)\n          .hasSize(1)\n          .hasEntrySatisfying(\"k5\", v -> {\n            assertThat(v).isInstanceOf(GenericData.Record.class);\n            var innerRec = (GenericData.Record) v;\n            assertThat(innerRec.get(\"long_map\"))\n                .isEqualTo(Map.of(\"_k1\", 222L));\n            assertThat(innerRec.get(\"string_map\"))\n                .isEqualTo(Map.of(\"_k2\", \"_s1\"));\n          });\n    }\n\n    @Test\n    void arrayField() {\n      var schema = createSchema(\n          \"\"\"\n               {\n                 \"type\": \"record\",\n                 \"name\": \"TestAvroRecord\",\n                 \"fields\": [\n                   {\n                     \"name\": \"f_array\",\n                     \"type\": {\n                        \"type\": \"array\",\n                        \"items\" : \"string\",\n                        \"default\": []\n                      }\n                   }\n                 ]\n              }\"\"\"\n      );\n\n      String jsonPayload = \"\"\"\n          {\n            \"f_array\": [ \"e1\", \"e2\" ]\n          }\n          \"\"\";\n\n      var record = (GenericData.Record) convertJsonToAvro(jsonPayload, schema);\n      assertThat(record.get(\"f_array\")).isEqualTo(List.of(\"e1\", \"e2\"));\n    }\n\n    @Test\n    void logicalTypesField() {\n      var schema = createSchema(\n          \"\"\"\n               {\n                 \"type\": \"record\",\n                 \"name\": \"TestAvroRecord\",\n                 \"fields\": [\n                   {\n                     \"name\": \"lt_date\",\n                     \"type\": { \"type\": \"int\", \"logicalType\": \"date\" }\n                   },\n                   {\n                     \"name\": \"lt_uuid\",\n                     \"type\": { \"type\": \"string\", \"logicalType\": \"uuid\" }\n                   },\n                   {\n                     \"name\": \"lt_decimal\",\n                     \"type\": { \"type\": \"bytes\", \"logicalType\": \"decimal\", \"precision\": 22, \"scale\":10 }\n                   },\n                   {\n                     \"name\": \"lt_time_millis\",\n                     \"type\": { \"type\": \"int\", \"logicalType\": \"time-millis\"}\n                   },\n                   {\n                     \"name\": \"lt_time_micros\",\n                     \"type\": { \"type\": \"long\", \"logicalType\": \"time-micros\"}\n                   },\n                   {\n                     \"name\": \"lt_timestamp_millis\",\n                     \"type\": { \"type\": \"long\", \"logicalType\": \"timestamp-millis\" }\n                   },\n                   {\n                     \"name\": \"lt_timestamp_micros\",\n                     \"type\": { \"type\": \"long\", \"logicalType\": \"timestamp-micros\" }\n                   },\n                   {\n                     \"name\": \"lt_local_timestamp_millis\",\n                     \"type\": { \"type\": \"long\", \"logicalType\": \"local-timestamp-millis\" }\n                   },\n                   {\n                     \"name\": \"lt_local_timestamp_micros\",\n                     \"type\": { \"type\": \"long\", \"logicalType\": \"local-timestamp-micros\" }\n                   }\n                 ]\n              }\"\"\"\n      );\n\n      String jsonPayload = \"\"\"\n          {\n            \"lt_date\":\"1991-08-14\",\n            \"lt_decimal\": 2.1617413862327545E11,\n            \"lt_time_millis\": \"10:15:30.001\",\n            \"lt_time_micros\": \"10:15:30.123456\",\n            \"lt_uuid\": \"a37b75ca-097c-5d46-6119-f0637922e908\",\n            \"lt_timestamp_millis\": \"2007-12-03T10:15:30.123Z\",\n            \"lt_timestamp_micros\": \"2007-12-13T10:15:30.123456Z\",\n            \"lt_local_timestamp_millis\": \"2017-12-03T10:15:30.123\",\n            \"lt_local_timestamp_micros\": \"2017-12-13T10:15:30.123456\"\n          }\n          \"\"\";\n\n      var converted = convertJsonToAvro(jsonPayload, schema);\n      assertThat(converted).isInstanceOf(GenericData.Record.class);\n\n      var record = (GenericData.Record) converted;\n\n      assertThat(record.get(\"lt_date\"))\n          .isEqualTo(LocalDate.of(1991, 8, 14));\n      assertThat(record.get(\"lt_decimal\"))\n          .isEqualTo(new BigDecimal(\"2.1617413862327545E11\"));\n      assertThat(record.get(\"lt_time_millis\"))\n          .isEqualTo(LocalTime.parse(\"10:15:30.001\"));\n      assertThat(record.get(\"lt_time_micros\"))\n          .isEqualTo(LocalTime.parse(\"10:15:30.123456\"));\n      assertThat(record.get(\"lt_timestamp_millis\"))\n          .isEqualTo(Instant.parse(\"2007-12-03T10:15:30.123Z\"));\n      assertThat(record.get(\"lt_timestamp_micros\"))\n          .isEqualTo(Instant.parse(\"2007-12-13T10:15:30.123456Z\"));\n      assertThat(record.get(\"lt_local_timestamp_millis\"))\n          .isEqualTo(LocalDateTime.parse(\"2017-12-03T10:15:30.123\"));\n      assertThat(record.get(\"lt_local_timestamp_micros\"))\n          .isEqualTo(LocalDateTime.parse(\"2017-12-13T10:15:30.123456\"));\n    }\n  }\n\n  // checking conversion of KafkaAvroDeserializer output to JsonNode\n  @Nested\n  class FromAvroToJson {\n\n    @Test\n    void primitiveRoot() {\n      assertThat(convertAvroToJson(\"str\", createSchema(\"\\\"string\\\"\")))\n          .isEqualTo(new TextNode(\"str\"));\n\n      assertThat(convertAvroToJson(123, createSchema(\"\\\"int\\\"\")))\n          .isEqualTo(new IntNode(123));\n\n      assertThat(convertAvroToJson(123L, createSchema(\"\\\"long\\\"\")))\n          .isEqualTo(new LongNode(123));\n\n      assertThat(convertAvroToJson(123.1F, createSchema(\"\\\"float\\\"\")))\n          .isEqualTo(new FloatNode(123.1F));\n\n      assertThat(convertAvroToJson(123.1, createSchema(\"\\\"double\\\"\")))\n          .isEqualTo(new DoubleNode(123.1));\n\n      assertThat(convertAvroToJson(true, createSchema(\"\\\"boolean\\\"\")))\n          .isEqualTo(BooleanNode.valueOf(true));\n\n      assertThat(convertAvroToJson(ByteBuffer.wrap(Longs.toByteArray(123L)), createSchema(\"\\\"bytes\\\"\")))\n          .isEqualTo(new TextNode(new String(Longs.toByteArray(123L), StandardCharsets.ISO_8859_1)));\n    }\n\n    @SneakyThrows\n    @Test\n    void primitiveTypedFields() {\n      var schema = createSchema(\n          \"\"\"\n               {\n                 \"type\": \"record\",\n                 \"name\": \"TestAvroRecord\",\n                 \"fields\": [\n                   {\n                     \"name\": \"f_int\",\n                     \"type\": \"int\"\n                   },\n                   {\n                     \"name\": \"f_long\",\n                     \"type\": \"long\"\n                   },\n                   {\n                     \"name\": \"f_string\",\n                     \"type\": \"string\"\n                   },\n                   {\n                     \"name\": \"f_boolean\",\n                     \"type\": \"boolean\"\n                   },\n                   {\n                     \"name\": \"f_float\",\n                     \"type\": \"float\"\n                   },\n                   {\n                     \"name\": \"f_double\",\n                     \"type\": \"double\"\n                   },\n                   {\n                     \"name\": \"f_enum\",\n                     \"type\" : {\n                      \"type\": \"enum\",\n                      \"name\": \"Suit\",\n                      \"symbols\" : [\"SPADES\", \"HEARTS\", \"DIAMONDS\", \"CLUBS\"]\n                     }\n                   },\n                   {\n                     \"name\" : \"f_fixed\",\n                     \"type\" : { \"type\" : \"fixed\" ,\"size\" : 8, \"name\": \"long_encoded\" }\n                   },\n                   {\n                     \"name\" : \"f_bytes\",\n                     \"type\": \"bytes\"\n                   }\n                 ]\n              }\"\"\"\n      );\n\n      byte[] fixedFieldValue = Longs.toByteArray(1234L);\n      byte[] bytesFieldValue = Longs.toByteArray(2345L);\n\n      GenericData.Record inputRecord = new GenericData.Record(schema);\n      inputRecord.put(\"f_int\", 123);\n      inputRecord.put(\"f_long\", 4294967294L);\n      inputRecord.put(\"f_string\", \"string here\");\n      inputRecord.put(\"f_boolean\", true);\n      inputRecord.put(\"f_float\", 123.1f);\n      inputRecord.put(\"f_double\", 123456.123456);\n      inputRecord.put(\"f_enum\", new GenericData.EnumSymbol(schema.getField(\"f_enum\").schema(), \"SPADES\"));\n      inputRecord.put(\"f_fixed\", new GenericData.Fixed(schema.getField(\"f_fixed\").schema(), fixedFieldValue));\n      inputRecord.put(\"f_bytes\", ByteBuffer.wrap(bytesFieldValue));\n\n      String expectedJson = \"\"\"\n          {\n            \"f_int\": 123,\n            \"f_long\": 4294967294,\n            \"f_string\": \"string here\",\n            \"f_boolean\": true,\n            \"f_float\": 123.1,\n            \"f_double\": 123456.123456,\n            \"f_enum\": \"SPADES\",\n            \"f_fixed\": \"\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\u0004Ò\",\n            \"f_bytes\": \"\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\t)\"\n          }\n          \"\"\";\n\n      assertJsonsEqual(expectedJson, convertAvroToJson(inputRecord, schema));\n    }\n\n    @Test\n    void logicalTypesField() {\n      var schema = createSchema(\n          \"\"\"\n               {\n                 \"type\": \"record\",\n                 \"name\": \"TestAvroRecord\",\n                 \"fields\": [\n                   {\n                     \"name\": \"lt_date\",\n                     \"type\": { \"type\": \"int\", \"logicalType\": \"date\" }\n                   },\n                   {\n                     \"name\": \"lt_uuid\",\n                     \"type\": { \"type\": \"string\", \"logicalType\": \"uuid\" }\n                   },\n                   {\n                     \"name\": \"lt_decimal\",\n                     \"type\": { \"type\": \"bytes\", \"logicalType\": \"decimal\", \"precision\": 22, \"scale\":10 }\n                   },\n                   {\n                     \"name\": \"lt_time_millis\",\n                     \"type\": { \"type\": \"int\", \"logicalType\": \"time-millis\"}\n                   },\n                   {\n                     \"name\": \"lt_time_micros\",\n                     \"type\": { \"type\": \"long\", \"logicalType\": \"time-micros\"}\n                   },\n                   {\n                     \"name\": \"lt_timestamp_millis\",\n                     \"type\": { \"type\": \"long\", \"logicalType\": \"timestamp-millis\" }\n                   },\n                   {\n                     \"name\": \"lt_timestamp_micros\",\n                     \"type\": { \"type\": \"long\", \"logicalType\": \"timestamp-micros\" }\n                   },\n                   {\n                     \"name\": \"lt_local_timestamp_millis\",\n                     \"type\": { \"type\": \"long\", \"logicalType\": \"local-timestamp-millis\" }\n                   },\n                   {\n                     \"name\": \"lt_local_timestamp_micros\",\n                     \"type\": { \"type\": \"long\", \"logicalType\": \"local-timestamp-micros\" }\n                   }\n                 ]\n              }\"\"\"\n      );\n\n      GenericData.Record inputRecord = new GenericData.Record(schema);\n      inputRecord.put(\"lt_date\", LocalDate.of(1991, 8, 14));\n      inputRecord.put(\"lt_uuid\", UUID.fromString(\"a37b75ca-097c-5d46-6119-f0637922e908\"));\n      inputRecord.put(\"lt_decimal\", new BigDecimal(\"2.16\"));\n      inputRecord.put(\"lt_time_millis\", LocalTime.parse(\"10:15:30.001\"));\n      inputRecord.put(\"lt_time_micros\", LocalTime.parse(\"10:15:30.123456\"));\n      inputRecord.put(\"lt_timestamp_millis\", Instant.parse(\"2007-12-03T10:15:30.123Z\"));\n      inputRecord.put(\"lt_timestamp_micros\", Instant.parse(\"2007-12-13T10:15:30.123456Z\"));\n      inputRecord.put(\"lt_local_timestamp_millis\", LocalDateTime.parse(\"2017-12-03T10:15:30.123\"));\n      inputRecord.put(\"lt_local_timestamp_micros\", LocalDateTime.parse(\"2017-12-13T10:15:30.123456\"));\n\n      String expectedJson = \"\"\"\n          {\n            \"lt_date\":\"1991-08-14\",\n            \"lt_uuid\": \"a37b75ca-097c-5d46-6119-f0637922e908\",\n            \"lt_decimal\": 2.16,\n            \"lt_time_millis\": \"10:15:30.001\",\n            \"lt_time_micros\": \"10:15:30.123456\",\n            \"lt_timestamp_millis\": \"2007-12-03T10:15:30.123Z\",\n            \"lt_timestamp_micros\": \"2007-12-13T10:15:30.123456Z\",\n            \"lt_local_timestamp_millis\": \"2017-12-03T10:15:30.123\",\n            \"lt_local_timestamp_micros\": \"2017-12-13T10:15:30.123456\"\n          }\n          \"\"\";\n\n      assertJsonsEqual(expectedJson, convertAvroToJson(inputRecord, schema));\n    }\n\n    @Test\n    void unionField() {\n      var schema = createSchema(\n          \"\"\"\n               {\n                 \"type\": \"record\",\n                 \"namespace\": \"com.test\",\n                 \"name\": \"TestAvroRecord\",\n                 \"fields\": [\n                   {\n                     \"name\": \"f_union\",\n                     \"type\": [ \"null\", \"int\", \"TestAvroRecord\"]\n                   }\n                 ]\n              }\"\"\"\n      );\n\n      var r = new GenericData.Record(schema);\n      r.put(\"f_union\", null);\n      assertJsonsEqual(\" {}\", convertAvroToJson(r, schema));\n\n      r = new GenericData.Record(schema);\n      r.put(\"f_union\", 123);\n      assertJsonsEqual(\" { \\\"f_union\\\" : { \\\"int\\\" : 123 } }\", convertAvroToJson(r, schema));\n\n\n      r = new GenericData.Record(schema);\n      var innerRec = new GenericData.Record(schema);\n      innerRec.put(\"f_union\", 123);\n      r.put(\"f_union\", innerRec);\n      // short type name can be set since there is NO clash with other types name\n      assertJsonsEqual(\n          \" { \\\"f_union\\\" : { \\\"TestAvroRecord\\\" : { \\\"f_union\\\" : { \\\"int\\\" : 123 } } } }\",\n          convertAvroToJson(r, schema)\n      );\n    }\n\n    @Test\n    void unionFieldWithInnerTypesNamesClash() {\n      var schema = createSchema(\n          \"\"\"\n               {\n                 \"type\": \"record\",\n                 \"namespace\": \"com.test\",\n                 \"name\": \"TestAvroRecord\",\n                 \"fields\": [\n                   {\n                     \"name\": \"nestedClass\",\n                     \"type\": {\n                       \"type\": \"record\",\n                       \"namespace\": \"com.nested\",\n                       \"name\": \"TestAvroRecord\",\n                       \"fields\": [\n                         {\"name\" : \"inner_obj_field\", \"type\": \"int\" }\n                       ]\n                     }\n                   },\n                   {\n                     \"name\": \"f_union\",\n                     \"type\": [ \"null\", \"int\", \"com.test.TestAvroRecord\", \"com.nested.TestAvroRecord\"]\n                   }\n                 ]\n              }\"\"\"\n      );\n\n      var r = new GenericData.Record(schema);\n      var innerRec = new GenericData.Record(schema);\n      innerRec.put(\"f_union\", 123);\n      r.put(\"f_union\", innerRec);\n      // full type name should be set since there is a clash with other type name\n      assertJsonsEqual(\n          \" { \\\"f_union\\\" : { \\\"com.test.TestAvroRecord\\\" : { \\\"f_union\\\" : { \\\"int\\\" : 123 } } } }\",\n          convertAvroToJson(r, schema)\n      );\n    }\n\n  }\n\n  private Schema createSchema(String schema) {\n    return new AvroSchema(schema).rawSchema();\n  }\n\n  @SneakyThrows\n  private void assertJsonsEqual(String expectedJson, JsonNode actual) {\n    var mapper = new JsonMapper();\n    assertThat(actual.toPrettyString())\n        .isEqualTo(mapper.readTree(expectedJson).toPrettyString());\n  }\n\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/ProtobufSchemaConverterTest.java",
    "content": "package com.provectus.kafka.ui.util.jsonschema;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema;\nimport java.net.URI;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\n\nclass ProtobufSchemaConverterTest {\n\n  @Test\n  void testSchemaConvert() throws Exception {\n    String protoSchema = \"\"\"\n        syntax = \"proto3\";\n        package test;\n\n        import \"google/protobuf/timestamp.proto\";\n        import \"google/protobuf/duration.proto\";\n        import \"google/protobuf/struct.proto\";\n        import \"google/protobuf/wrappers.proto\";\n\n        message TestMsg {\n            string string_field = 1;\n            int32 int32_field = 2;\n            bool bool_field = 3;\n            SampleEnum enum_field = 4;\n\n            enum SampleEnum {\n                ENUM_V1 = 0;\n                ENUM_V2 = 1;\n            }\n\n            google.protobuf.Timestamp ts_field = 5;\n            google.protobuf.Struct struct_field = 6;\n            google.protobuf.ListValue lst_v_field = 7;\n            google.protobuf.Duration duration_field = 8;\n\n            oneof some_oneof1 {\n                google.protobuf.Value v1 = 9;\n                google.protobuf.Value v2 = 10;\n            }\n            // wrapper fields:\n            google.protobuf.Int64Value int64_w_field = 11;\n            google.protobuf.Int32Value int32_w_field = 12;\n            google.protobuf.UInt64Value uint64_w_field = 13;\n            google.protobuf.UInt32Value uint32_w_field = 14;\n            google.protobuf.StringValue string_w_field = 15;\n            google.protobuf.BoolValue bool_w_field = 16;\n            google.protobuf.DoubleValue double_w_field = 17;\n            google.protobuf.FloatValue float_w_field = 18;\n\n            //embedded msg\n            EmbeddedMsg emb = 19;\n            repeated EmbeddedMsg emb_list = 20;\n\n            message EmbeddedMsg {\n                int32 emb_f1 = 1;\n                TestMsg outer_ref = 2;\n                EmbeddedMsg self_ref = 3;\n            }\n\n            map<int32, string> intToStringMap = 21;\n            map<string, EmbeddedMsg> strToObjMap  = 22;\n        }\"\"\";\n\n    String expectedJsonSchema = \"\"\"\n        {\n            \"$id\": \"http://example.com/test.TestMsg\",\n            \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n            \"type\": \"object\",\n            \"definitions\":\n            {\n                \"test.TestMsg\":\n                {\n                    \"type\": \"object\",\n                    \"properties\":\n                    {\n                        \"enum_field\": {\n                            \"enum\":\n                            [\n                                \"ENUM_V1\",\n                                \"ENUM_V2\"\n                            ],\n                            \"type\": \"string\"\n                        },\n                        \"string_w_field\": { \"type\": \"string\" },\n                        \"ts_field\": { \"type\": \"string\", \"format\": \"date-time\" },\n                        \"emb_list\": {\n                            \"type\": \"array\",\n                            \"items\": { \"$ref\": \"#/definitions/test.TestMsg.EmbeddedMsg\" }\n                        },\n                        \"float_w_field\": { \"type\": \"number\" },\n                        \"lst_v_field\": {\n                            \"type\": \"array\",\n                            \"items\": { \"type\":[ \"number\", \"string\", \"object\", \"array\", \"boolean\", \"null\" ] }\n                        },\n                        \"struct_field\": { \"type\": \"object\", \"properties\": {} },\n                        \"string_field\": { \"type\": \"string\" },\n                        \"double_w_field\": { \"type\": \"number\" },\n                        \"bool_field\": { \"type\": \"boolean\" },\n                        \"int32_w_field\": { \"type\": \"integer\", \"maximum\": 2147483647, \"minimum\": -2147483648 },\n                        \"duration_field\": { \"type\": \"string\" },\n                        \"int32_field\": { \"type\": \"integer\", \"maximum\": 2147483647, \"minimum\": -2147483648 },\n                        \"int64_w_field\": {\n                            \"type\": \"integer\",\n                            \"maximum\": 9223372036854775807, \"minimum\": -9223372036854775808\n                        },\n                        \"v1\": { \"type\": [ \"number\", \"string\", \"object\", \"array\", \"boolean\", \"null\" ] },\n                        \"emb\": { \"$ref\": \"#/definitions/test.TestMsg.EmbeddedMsg\" },\n                        \"v2\": { \"type\": [ \"number\", \"string\", \"object\", \"array\", \"boolean\", \"null\" ] },\n                        \"uint32_w_field\": { \"type\": \"integer\", \"maximum\": 4294967295, \"minimum\": 0 },\n                        \"bool_w_field\": { \"type\": \"boolean\" },\n                        \"uint64_w_field\": { \"type\": \"integer\", \"maximum\": 18446744073709551615, \"minimum\": 0 },\n                        \"strToObjMap\": { \"type\": \"object\", \"additionalProperties\": true },\n                        \"intToStringMap\": { \"type\": \"object\", \"additionalProperties\": true }\n                    }\n                },\n                \"test.TestMsg.EmbeddedMsg\": {\n                    \"type\": \"object\",\n                    \"properties\":\n                    {\n                        \"emb_f1\": { \"type\": \"integer\", \"maximum\": 2147483647, \"minimum\": -2147483648 },\n                        \"outer_ref\": { \"$ref\": \"#/definitions/test.TestMsg\" },\n                        \"self_ref\": { \"$ref\": \"#/definitions/test.TestMsg.EmbeddedMsg\" }\n                    }\n                }\n            },\n            \"$ref\": \"#/definitions/test.TestMsg\"\n        }\"\"\";\n\n    ProtobufSchemaConverter converter = new ProtobufSchemaConverter();\n    ProtobufSchema protobufSchema = new ProtobufSchema(protoSchema);\n    URI basePath = new URI(\"http://example.com/\");\n\n    JsonSchema converted = converter.convert(basePath, protobufSchema.toDescriptor());\n    assertJsonEqual(expectedJsonSchema, converted.toJson());\n  }\n\n  private void assertJsonEqual(String expected, String actual) throws Exception {\n    ObjectMapper om = new ObjectMapper();\n    Assertions.assertEquals(om.readTree(expected), om.readTree(actual));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/resources/application-test.yml",
    "content": "spring:\n  jmx:\n    enabled: true\nauth:\n  type: DISABLED"
  },
  {
    "path": "kafka-ui-api/src/test/resources/fileForUploadTest.txt",
    "content": "some content goes here\n"
  },
  {
    "path": "kafka-ui-api/src/test/resources/protobuf-serde/address-book.proto",
    "content": "syntax = \"proto3\";\npackage test;\n\noption java_multiple_files = true;\noption java_package = \"com.example.tutorial.protos\";\noption java_outer_classname = \"AddressBookProtos\";\n\nmessage Person {\n  string name = 1;\n  int32 id = 2;  // Unique ID number for this person.\n  string email = 3;\n\n  enum PhoneType {\n    MOBILE = 0;\n    HOME = 1;\n    WORK = 2;\n  }\n\n  message PhoneNumber {\n    string number = 1;\n    PhoneType type = 2;\n  }\n\n  repeated PhoneNumber phones = 4;\n\n}\n\nmessage AnotherPerson {\n    string name = 1;\n    string surname = 2;\n}\n\n// Our address book file is just one of these.\nmessage AddressBook {\n  int32 version = 1;\n  repeated Person people = 2;\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/resources/protobuf-serde/lang-description.proto",
    "content": "syntax = \"proto3\";\n\npackage test;\n\nimport \"language/language.proto\";\nimport \"google/protobuf/wrappers.proto\";\n\nmessage LanguageDescription {\n    test.lang.Language lang = 1;\n    google.protobuf.StringValue descr = 2;\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/resources/protobuf-serde/language/language.proto",
    "content": "syntax = \"proto3\";\npackage test.lang;\n\nenum Language {\n    DE = 0;\n    EN = 1;\n    ES = 2;\n    FR = 3;\n    PL = 4;\n    RU = 5;\n}\n"
  },
  {
    "path": "kafka-ui-api/src/test/resources/protobuf-serde/sensor.proto",
    "content": "syntax = \"proto3\";\npackage test;\n\nmessage Sensor {\n    string name = 1;\n    double temperature = 2;\n    int32 humidity = 3;\n\n    enum SwitchLevel {\n        CLOSED = 0;\n        OPEN = 1;\n    }\n    SwitchLevel door = 5;\n}\n"
  },
  {
    "path": "kafka-ui-contract/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n    xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>kafka-ui</artifactId>\n        <groupId>com.provectus</groupId>\n        <version>0.0.1-SNAPSHOT</version>\n    </parent>\n\n    <modelVersion>4.0.0</modelVersion>\n    <artifactId>kafka-ui-contract</artifactId>\n\n    <profiles>\n        <profile>\n            <id>generate-spring-webflux-api</id>\n            <activation>\n                <activeByDefault>true</activeByDefault>\n            </activation>\n\n            <dependencies>\n                <dependency>\n                    <groupId>org.springframework.boot</groupId>\n                    <artifactId>spring-boot-starter-webflux</artifactId>\n                </dependency>\n                <dependency>\n                    <groupId>org.springframework.boot</groupId>\n                    <artifactId>spring-boot-starter-validation</artifactId>\n                </dependency>\n                <dependency>\n                    <groupId>io.swagger.core.v3</groupId>\n                    <artifactId>swagger-integration-jakarta</artifactId>\n                    <version>2.2.8</version>\n                </dependency>\n                <dependency>\n                    <groupId>org.openapitools</groupId>\n                    <artifactId>jackson-databind-nullable</artifactId>\n                    <version>0.2.4</version>\n                </dependency>\n                <dependency>\n                    <groupId>jakarta.annotation</groupId>\n                    <artifactId>jakarta.annotation-api</artifactId>\n                    <version>2.1.1</version>\n                </dependency>\n                <dependency>\n                    <groupId>javax.annotation</groupId>\n                    <artifactId>javax.annotation-api</artifactId>\n                    <version>1.3.2</version>\n                </dependency>\n            </dependencies>\n\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.openapitools</groupId>\n                        <artifactId>openapi-generator-maven-plugin</artifactId>\n                        <version>${openapi-generator-maven-plugin.version}</version>\n                        <executions>\n                            <execution>\n                                <id>generate-kafka-ui-client</id>\n                                <goals>\n                                    <goal>generate</goal>\n                                </goals>\n                                <configuration>\n                                    <inputSpec>${project.basedir}/src/main/resources/swagger/kafka-ui-api.yaml\n                                    </inputSpec>\n                                    <output>${project.build.directory}/generated-sources/kafka-ui-client</output>\n                                    <generatorName>java</generatorName>\n                                    <generateApiTests>false</generateApiTests>\n                                    <generateModelTests>false</generateModelTests>\n                                    <configOptions>\n                                        <modelPackage>com.provectus.kafka.ui.api.model</modelPackage>\n                                        <apiPackage>com.provectus.kafka.ui.api.api</apiPackage>\n                                        <sourceFolder>kafka-ui-client</sourceFolder>\n                                        <asyncNative>true</asyncNative>\n                                        <library>webclient</library>\n                                        <useBeanValidation>true</useBeanValidation>\n                                        <dateLibrary>java8</dateLibrary>\n                                        <useJakartaEe>true</useJakartaEe>\n                                    </configOptions>\n                                </configuration>\n                            </execution>\n                            <execution>\n                                <id>generate-backend-api</id>\n                                <goals>\n                                    <goal>generate</goal>\n                                </goals>\n                                <configuration>\n                                    <inputSpec>${project.basedir}/src/main/resources/swagger/kafka-ui-api.yaml</inputSpec>\n                                    <output>${project.build.directory}/generated-sources/api</output>\n                                    <generatorName>spring</generatorName>\n                                    <modelNameSuffix>DTO</modelNameSuffix>\n                                    <configOptions>\n                                        <modelPackage>com.provectus.kafka.ui.model</modelPackage>\n                                        <apiPackage>com.provectus.kafka.ui.api</apiPackage>\n                                        <sourceFolder>kafka-ui-contract</sourceFolder>\n                                        <reactive>true</reactive>\n                                        <interfaceOnly>true</interfaceOnly>\n                                        <skipDefaultInterface>true</skipDefaultInterface>\n                                        <useBeanValidation>true</useBeanValidation>\n                                        <useTags>true</useTags>\n                                        <useSpringBoot3>true</useSpringBoot3>\n                                        <dateLibrary>java8</dateLibrary>\n                                    </configOptions>\n                                </configuration>\n                            </execution>\n                            <execution>\n                                <id>generate-connect-client</id>\n                                <goals>\n                                    <goal>generate</goal>\n                                </goals>\n                                <configuration>\n                                    <inputSpec>${project.basedir}/src/main/resources/swagger/kafka-connect-api.yaml\n                                    </inputSpec>\n                                    <output>${project.build.directory}/generated-sources/kafka-connect-client</output>\n                                    <generatorName>java</generatorName>\n                                    <generateApiTests>false</generateApiTests>\n                                    <generateModelTests>false</generateModelTests>\n                                    <configOptions>\n                                        <modelPackage>com.provectus.kafka.ui.connect.model</modelPackage>\n                                        <apiPackage>com.provectus.kafka.ui.connect.api</apiPackage>\n                                        <sourceFolder>kafka-connect-client</sourceFolder>\n                                        <asyncNative>true</asyncNative>\n                                        <library>webclient</library>\n                                        <useJakartaEe>true</useJakartaEe>\n                                        <useBeanValidation>true</useBeanValidation>\n                                        <dateLibrary>java8</dateLibrary>\n                                    </configOptions>\n                                </configuration>\n                            </execution>\n                            <execution>\n                                <id>generate-sr-client</id>\n                                <goals>\n                                    <goal>generate</goal>\n                                </goals>\n                                <configuration>\n                                    <inputSpec>${project.basedir}/src/main/resources/swagger/kafka-sr-api.yaml\n                                    </inputSpec>\n                                    <output>${project.build.directory}/generated-sources/kafka-sr-client</output>\n                                    <generatorName>java</generatorName>\n                                    <generateApiTests>false</generateApiTests>\n                                    <generateModelTests>false</generateModelTests>\n                                    <configOptions>\n                                        <modelPackage>com.provectus.kafka.ui.sr.model</modelPackage>\n                                        <apiPackage>com.provectus.kafka.ui.sr.api</apiPackage>\n                                        <sourceFolder>kafka-sr-client</sourceFolder>\n                                        <asyncNative>true</asyncNative>\n                                        <library>webclient</library>\n                                        <useJakartaEe>true</useJakartaEe>\n                                        <useBeanValidation>true</useBeanValidation>\n                                        <dateLibrary>java8</dateLibrary>\n                                    </configOptions>\n                                </configuration>\n                            </execution>\n                        </executions>\n                    </plugin>\n                    <plugin>\n                        <groupId>com.github.eirslett</groupId>\n                        <artifactId>frontend-maven-plugin</artifactId>\n                        <version>${frontend-maven-plugin.version}</version>\n                        <configuration>\n                            <workingDirectory>../kafka-ui-react-app</workingDirectory>\n                            <environmentVariables>\n                                <VITE_TAG>${project.version}</VITE_TAG>\n                            </environmentVariables>\n                        </configuration>\n                        <executions>\n                            <execution>\n                                <id>install node and pnpm</id>\n                                <goals>\n                                    <goal>install-node-and-pnpm</goal>\n                                </goals>\n                                <configuration>\n                                    <nodeVersion>${node.version}</nodeVersion>\n                                    <pnpmVersion>${pnpm.version}</pnpmVersion>\n                                </configuration>\n                            </execution>\n                            <execution>\n                                <id>pnpm install</id>\n                                <goals>\n                                    <goal>pnpm</goal>\n                                </goals>\n                                <configuration>\n                                    <arguments>install</arguments>\n                                </configuration>\n                            </execution>\n                            <execution>\n                                <id>pnpm gen:sources</id>\n                                <goals>\n                                    <goal>pnpm</goal>\n                                </goals>\n                                <configuration>\n                                    <arguments>gen:sources</arguments>\n                                </configuration>\n                            </execution>\n                        </executions>\n                    </plugin>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-clean-plugin</artifactId>\n                        <configuration>\n                            <filesets>\n                                <fileset>\n                                    <directory>${basedir}/${frontend-generated-sources-directory}</directory>\n                                </fileset>\n                            </filesets>\n                        </configuration>\n                    </plugin>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-resources-plugin</artifactId>\n                        <executions>\n                            <execution>\n                                <id>copy-resource-one</id>\n                                <phase>generate-resources</phase>\n                                <goals>\n                                    <goal>copy-resources</goal>\n                                </goals>\n\n                                <configuration>\n                                    <outputDirectory>${basedir}/${frontend-generated-sources-directory}</outputDirectory>\n                                    <resources>\n                                        <resource>\n                                            <directory>${project.build.directory}/generated-sources/frontend/</directory>\n                                            <includes>\n                                                <include>**/*.ts</include>\n                                            </includes>\n                                        </resource>\n                                    </resources>\n                                </configuration>\n                            </execution>\n                        </executions>\n\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n    </profiles>\n</project>\n"
  },
  {
    "path": "kafka-ui-contract/src/main/resources/swagger/kafka-connect-api.yaml",
    "content": "openapi: 3.0.0\ninfo:\n  description: Api Documentation\n  version: 0.1.0\n  title: Api Documentation\n  termsOfService: urn:tos\n  contact: {}\n  license:\n    name: Apache 2.0\n    url: http://www.apache.org/licenses/LICENSE-2.0\ntags:\n  - name: /connect\nservers:\n  - url: /localhost\n\npaths:\n  /connectors:\n    get:\n      tags:\n        - KafkaConnectClient\n      summary: get all connectors from Kafka Connect service\n      operationId: getConnectors\n      parameters:\n        - name: search\n          in: query\n          required: false\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: string\n    post:\n      tags:\n        - KafkaConnectClient\n      summary: create new connector\n      operationId: createConnector\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/NewConnector'\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Connector'\n        400:\n          description: Bad request\n        409:\n          description: rebalance is in progress\n        500:\n          description: Internal server error\n\n  /connectors/{connectorName}:\n    get:\n      tags:\n        - KafkaConnectClient\n      summary: get information about the connector\n      operationId: getConnector\n      parameters:\n        - name: connectorName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Connector'\n    delete:\n      tags:\n        - KafkaConnectClient\n      summary: delete connector\n      operationId: deleteConnector\n      parameters:\n        - name: connectorName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n        409:\n          description: rebalance is in progress\n\n  /connectors/{connectorName}/config:\n    get:\n      tags:\n        - KafkaConnectClient\n      summary: get connector configuration\n      operationId: getConnectorConfig\n      parameters:\n        - name: connectorName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ConnectorConfig'\n    put:\n      tags:\n        - KafkaConnectClient\n      summary: update or create connector with provided config\n      operationId: setConnectorConfig\n      parameters:\n        - name: connectorName\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/ConnectorConfig'\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Connector'\n        400:\n          description: Bad request\n        409:\n          description: rebalance is in progress\n        500:\n          description: Internal server error\n\n\n  /connectors/{connectorName}/status:\n    get:\n      tags:\n        - KafkaConnectClient\n      summary: get connector status\n      operationId: getConnectorStatus\n      parameters:\n        - name: connectorName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ConnectorStatus'\n\n  /connectors/{connectorName}/restart:\n    post:\n      tags:\n        - KafkaConnectClient\n      summary: restart the connector and its tasks\n      operationId: restartConnector\n      parameters:\n        - name: connectorName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: includeTasks\n          in: query\n          required: false\n          schema:\n            type: boolean\n            default: false\n          description: Specifies whether to restart the connector instance and task instances or just the connector instance\n        - name: onlyFailed\n          in: query\n          required: false\n          schema:\n            type: boolean\n            default: false\n          description: Specifies whether to restart just the instances with a FAILED status or all instances\n\n      responses:\n        200:\n          description: OK\n        409:\n          description: rebalance is in progress\n\n  /connectors/{connectorName}/pause:\n    put:\n      tags:\n        - KafkaConnectClient\n      summary: pause the connector\n      operationId: pauseConnector\n      parameters:\n        - name: connectorName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        202:\n          description: Accepted\n\n  /connectors/{connectorName}/resume:\n    put:\n      tags:\n        - KafkaConnectClient\n      summary: resume the connector\n      operationId: resumeConnector\n      parameters:\n        - name: connectorName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        202:\n          description: Accepted\n\n  /connectors/{connectorName}/tasks:\n    get:\n      tags:\n        - KafkaConnectClient\n      summary: get connector tasks\n      operationId: getConnectorTasks\n      parameters:\n        - name: connectorName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/ConnectorTask'\n\n  /connectors/{connectorName}/topics:\n    get:\n      tags:\n        - KafkaConnectClient\n      summary: The set of topic names the connector has been using since its creation or since the last time its set of active topics was reset\n      operationId: getConnectorTopics\n      parameters:\n        - name: connectorName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: object\n                additionalProperties:\n                  $ref: '#/components/schemas/ConnectorTopics'\n\n  /connectors/{connectorName}/tasks/{taskId}/status:\n    get:\n      tags:\n        - KafkaConnectClient\n      summary: get connector task status\n      operationId: getConnectorTaskStatus\n      parameters:\n        - name: connectorName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: taskId\n          in: path\n          required: true\n          schema:\n            type: integer\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/TaskStatus'\n\n  /connectors/{connectorName}/tasks/{taskId}/restart:\n    post:\n      tags:\n        - KafkaConnectClient\n      summary: restart connector task\n      operationId: restartConnectorTask\n      parameters:\n        - name: connectorName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: taskId\n          in: path\n          required: true\n          schema:\n            type: integer\n      responses:\n        200:\n          description: OK\n\n  /connector-plugins:\n    get:\n      tags:\n        - KafkaConnectClient\n      summary: get connector plugins\n      operationId: getConnectorPlugins\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/ConnectorPlugin'\n\n  /connector-plugins/{pluginName}/config/validate:\n    put:\n      tags:\n        - KafkaConnectClient\n      summary: validate connector plugin configuration\n      operationId: validateConnectorPluginConfig\n      parameters:\n        - name: pluginName\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/ConnectorConfig'\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ConnectorPluginConfigValidationResponse'\n\ncomponents:\n  securitySchemes:\n    basicAuth:\n      type: http\n      scheme: basic\n  schemas:\n    ConnectorConfig:\n      type: object\n      additionalProperties:\n        type: object\n\n    Task:\n      type: object\n      properties:\n        connector:\n          type: string\n        task:\n          type: integer\n\n    ConnectorTask:\n      type: object\n      properties:\n        id:\n          $ref: '#/components/schemas/Task'\n        config:\n          $ref: '#/components/schemas/ConnectorConfig'\n\n    NewConnector:\n      type: object\n      properties:\n        name:\n          type: string\n        config:\n          $ref: '#/components/schemas/ConnectorConfig'\n      required:\n        - name\n        - config\n\n    Connector:\n      allOf:\n        - $ref: '#/components/schemas/NewConnector'\n        - type: object\n          properties:\n            tasks:\n              type: array\n              items:\n                $ref: '#/components/schemas/Task'\n            type:\n              type: string\n              enum:\n                - source\n                - sink\n\n    TaskStatus:\n      type: object\n      properties:\n        id:\n          type: integer\n        state:\n          type: string\n          enum:\n            - RUNNING\n            - FAILED\n            - PAUSED\n            - RESTARTING\n            - UNASSIGNED\n        worker_id:\n          type: string\n        trace:\n          type: string\n\n    ConnectorStatus:\n      type: object\n      properties:\n        name:\n          type: string\n        connector:\n          type: object\n          properties:\n            state:\n              type: string\n              enum:\n                - RUNNING\n                - FAILED\n                - PAUSED\n                - UNASSIGNED\n            worker_id:\n              type: string\n            trace:\n              type: string\n        tasks:\n          type: array\n          items:\n            $ref: '#/components/schemas/TaskStatus'\n\n    ConnectorPlugin:\n      type: object\n      properties:\n        class:\n          type: string\n\n    ConnectorPluginConfigDefinition:\n      type: object\n      properties:\n        name:\n          type: string\n        type:\n          type: string\n          enum:\n            - BOOLEAN\n            - CLASS\n            - DOUBLE\n            - INT\n            - LIST\n            - LONG\n            - PASSWORD\n            - SHORT\n            - STRING\n        required:\n          type: boolean\n        default_value:\n          type: string\n        importance:\n          type: string\n          enum:\n            - LOW\n            - MEDIUM\n            - HIGH\n        documentation:\n          type: string\n        group:\n          type: string\n        width:\n          type: string\n          enum:\n            - SHORT\n            - MEDIUM\n            - LONG\n            - NONE\n        display_name:\n          type: string\n        dependents:\n          type: array\n          items:\n            type: string\n        order:\n          type: integer\n\n    ConnectorPluginConfigValue:\n      type: object\n      properties:\n        name:\n          type: string\n        value:\n          type: string\n        recommended_values:\n          type: array\n          items:\n            type: string\n        errors:\n          type: array\n          items:\n            type: string\n        visible:\n          type: boolean\n\n    ConnectorPluginConfig:\n      type: object\n      properties:\n        definition:\n          $ref: '#/components/schemas/ConnectorPluginConfigDefinition'\n        value:\n          $ref: '#/components/schemas/ConnectorPluginConfigValue'\n\n    ConnectorPluginConfigValidationResponse:\n      type: object\n      properties:\n        name:\n          type: string\n        error_count:\n          type: integer\n        groups:\n          type: array\n          items:\n            type: string\n        configs:\n          type: array\n          items:\n            $ref: '#/components/schemas/ConnectorPluginConfig'\n\n    ConnectorTopics:\n      type: object\n      properties:\n        topics:\n          type: array\n          items:\n            type: string\n\n\nsecurity:\n  - basicAuth: []\n\n"
  },
  {
    "path": "kafka-ui-contract/src/main/resources/swagger/kafka-sr-api.yaml",
    "content": "openapi: 3.0.0\ninfo:\n    description: Api Documentation\n    version: 0.1.0\n    title: Api Documentation\n    termsOfService: urn:tos\n    contact: {}\n    license:\n        name: Apache 2.0\n        url: http://www.apache.org/licenses/LICENSE-2.0\ntags:\n    - name: /schemaregistry\nservers:\n    - url: /localhost\n\npaths:\n    /subjects:\n        get:\n            tags:\n              - KafkaSrClient\n            summary: get all connectors from Kafka Connect service\n            operationId: getAllSubjectNames\n            parameters:\n              - name: subjectPrefix\n                in: query\n                required: false\n                schema:\n                  type: string\n              - name: deleted\n                in: query\n                schema:\n                  type: boolean\n            responses:\n                200:\n                  description: OK\n                  content:\n                      application/json:\n                          schema:\n                            #workaround for https://github.com/spring-projects/spring-framework/issues/24734\n                            type: string\n\n    /subjects/{subject}:\n        delete:\n            tags:\n                - KafkaSrClient\n            operationId: deleteAllSubjectVersions\n            parameters:\n                - name: subject\n                  in: path\n                  required: true\n                  schema:\n                    type: string\n                - name: permanent\n                  in: query\n                  schema:\n                    type: boolean\n                  required: false\n            responses:\n                200:\n                    description: OK\n                404:\n                    description: Not found\n\n    /subjects/{subject}/versions/{version}:\n        get:\n            tags:\n              - KafkaSrClient\n            operationId: getSubjectVersion\n            parameters:\n              - name: subject\n                in: path\n                required: true\n                schema:\n                  type: string\n              - name: version\n                in: path\n                required: true\n                schema:\n                  type: string\n              - name: deleted\n                in: query\n                schema:\n                  type: boolean\n            responses:\n                200:\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/SchemaSubject'\n                404:\n                    description: Not found\n                422:\n                    description: Invalid version\n        delete:\n            tags:\n                - KafkaSrClient\n            operationId: deleteSubjectVersion\n            parameters:\n                - name: subject\n                  in: path\n                  required: true\n                  schema:\n                    type: string\n                - name: permanent\n                  in: query\n                  required: false\n                  schema:\n                    type: boolean\n                    default: false\n                - name: version\n                  in: path\n                  required: true\n                  schema:\n                    type: string\n            responses:\n                200:\n                    description: OK\n                404:\n                    description: Not found\n\n    /subjects/{subject}/versions:\n        get:\n            tags:\n                - KafkaSrClient\n            operationId: getSubjectVersions\n            parameters:\n                - name: subject\n                  in: path\n                  required: true\n                  schema:\n                      type: string\n            responses:\n                200:\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                type: array\n                                items:\n                                    type: integer\n                                    format: int32\n                404:\n                    description: Not found\n        post:\n            tags:\n                - KafkaSrClient\n            operationId: registerNewSchema\n            parameters:\n                - name: subject\n                  in: path\n                  required: true\n                  schema:\n                      type: string\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/NewSubject'\n            responses:\n                200:\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/SubjectId'\n\n    /config/:\n        get:\n            tags:\n                - KafkaSrClient\n            operationId: getGlobalCompatibilityLevel\n            responses:\n                200:\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/CompatibilityConfig'\n                404:\n                    description: Not found\n        put:\n            tags:\n                - KafkaSrClient\n            operationId: updateGlobalCompatibilityLevel\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/CompatibilityLevelChange'\n            responses:\n                200:\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/CompatibilityLevelChange'\n                404:\n                    description: Not found\n\n    /config/{subject}:\n        get:\n            tags:\n                - KafkaSrClient\n            operationId: getSubjectCompatibilityLevel\n            parameters:\n                - name: subject\n                  in: path\n                  required: true\n                  schema:\n                      type: string\n                - name: defaultToGlobal\n                  in: query\n                  required: true\n                  schema:\n                      type: boolean\n            responses:\n                200:\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/CompatibilityConfig'\n                404:\n                    description: Not found\n        put:\n            tags:\n                - KafkaSrClient\n            operationId: updateSubjectCompatibilityLevel\n            parameters:\n                - name: subject\n                  in: path\n                  required: true\n                  schema:\n                      type: string\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/CompatibilityLevelChange'\n            responses:\n                200:\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/CompatibilityLevelChange'\n                404:\n                    description: Not found\n        delete:\n            tags:\n                - KafkaSrClient\n            operationId: deleteSubjectCompatibilityLevel\n            parameters:\n                - name: subject\n                  in: path\n                  required: true\n                  schema:\n                      type: string\n            responses:\n                200:\n                    description: OK\n                404:\n                    description: Not found\n\n    /compatibility/subjects/{subject}/versions/{version}:\n        post:\n            tags:\n                - KafkaSrClient\n            operationId: checkSchemaCompatibility\n            parameters:\n                - name: subject\n                  in: path\n                  required: true\n                  schema:\n                      type: string\n                - name: version\n                  in: path\n                  required: true\n                  schema:\n                      type: string\n                - name: verbose\n                  in: query\n                  description: Show reason a schema fails the compatibility test\n                  schema:\n                      type: boolean\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/NewSubject'\n            responses:\n                200:\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/CompatibilityCheckResponse'\n                404:\n                    description: Not found\n\nsecurity:\n    - basicAuth: []\n\ncomponents:\n    securitySchemes:\n        basicAuth:\n            type: http\n            scheme: basic\n    schemas:\n        SchemaSubject:\n            type: object\n            properties:\n              subject:\n                type: string\n              version:\n                type: string\n              id:\n                type: integer\n              schema:\n                type: string\n              schemaType:\n                  $ref: '#/components/schemas/SchemaType'\n              references:\n                type: array\n                items:\n                  $ref: '#/components/schemas/SchemaReference'\n            required:\n              - id\n              - subject\n              - version\n              - schema\n              - schemaType\n\n        SchemaType:\n          type: string\n          description: upon updating a schema, the type of an existing schema can't be changed\n          enum:\n            - AVRO\n            - JSON\n            - PROTOBUF\n\n        SchemaReference:\n            type: object\n            properties:\n                name:\n                    type: string\n                subject:\n                    type: string\n                version:\n                    type: integer\n            required:\n                - name\n                - subject\n                - version\n\n        SubjectId:\n            type: object\n            properties:\n                id:\n                    type: integer\n\n        NewSubject:\n            type: object\n            description: should be set for creating/updating schema subject\n            properties:\n                schema:\n                    type: string\n                schemaType:\n                    $ref: '#/components/schemas/SchemaType'\n                references:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/SchemaReference'\n            required:\n                - schema\n                - schemaType\n\n        CompatibilityConfig:\n            type: object\n            properties:\n                compatibilityLevel:\n                    $ref: '#/components/schemas/Compatibility'\n            required:\n                - compatibilityLevel\n\n        CompatibilityLevelChange:\n            type: object\n            properties:\n                compatibility:\n                    $ref: '#/components/schemas/Compatibility'\n            required:\n                - compatibility\n\n\n        Compatibility:\n            type: string\n            enum:\n                - BACKWARD\n                - BACKWARD_TRANSITIVE\n                - FORWARD\n                - FORWARD_TRANSITIVE\n                - FULL\n                - FULL_TRANSITIVE\n                - NONE\n\n\n        CompatibilityCheckResponse:\n            type: object\n            properties:\n                is_compatible:\n                    type: boolean\n"
  },
  {
    "path": "kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml",
    "content": "openapi: 3.0.0\ninfo:\n  description: Api Documentation\n  version: 0.1.0\n  title: Api Documentation\n  termsOfService: urn:tos\n  contact: { }\n  license:\n    name: Apache 2.0\n    url: https://www.apache.org/licenses/LICENSE-2.0\ntags:\n  - name: /api/clusters\n  - name: /api/clusters/connects\nservers:\n  - url: /localhost\n\npaths:\n  /api/clusters:\n    get:\n      tags:\n        - Clusters\n      summary: getClusters\n      operationId: getClusters\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/Cluster'\n\n\n  /api/clusters/{clusterName}/cache:\n    post:\n      tags:\n        - Clusters\n      summary: updateClusterInfo\n      operationId: updateClusterInfo\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Cluster'\n        404:\n          description: Not found\n\n\n  /api/clusters/{clusterName}/brokers:\n    get:\n      tags:\n        - Brokers\n      summary: getBrokers\n      operationId: getBrokers\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/Broker'\n\n  /api/clusters/{clusterName}/brokers/{id}/configs:\n    get:\n      tags:\n        - Brokers\n      summary: getBrokerConfig\n      operationId: getBrokerConfig\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: id\n          in: path\n          required: true\n          schema:\n            type: integer\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/BrokerConfig'\n        404:\n          description: Not found\n\n  /api/clusters/{clusterName}/brokers/{id}/configs/{name}:\n    put:\n      tags:\n        - Brokers\n      summary: updateBrokerConfigByName\n      operationId: updateBrokerConfigByName\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: id\n          in: path\n          required: true\n          schema:\n            type: integer\n        - name: name\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/BrokerConfigItem'\n      responses:\n        200:\n          description: OK\n\n  /api/clusters/{clusterName}/metrics:\n    get:\n      tags:\n        - Clusters\n      summary: getClusterMetrics\n      operationId: getClusterMetrics\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ClusterMetrics'\n\n  /api/clusters/{clusterName}/stats:\n    get:\n      tags:\n        - Clusters\n      summary: getClusterStats\n      operationId: getClusterStats\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ClusterStats'\n\n  /api/clusters/{clusterName}/brokers/{id}/metrics:\n    get:\n      tags:\n        - Brokers\n      summary: getBrokersMetrics\n      operationId: getBrokersMetrics\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: id\n          in: path\n          required: true\n          schema:\n            type: integer\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/BrokerMetrics'\n\n  /api/clusters/{clusterName}/brokers/logdirs:\n    get:\n      tags:\n        - Brokers\n      summary: getAllBrokersLogdirs\n      operationId: getAllBrokersLogdirs\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: broker\n          in: query\n          description: array of broker ids\n          required: false\n          schema:\n            type: array\n            items:\n              type: integer\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/BrokersLogdirs'\n\n  /api/clusters/{clusterName}/brokers/{id}/logdirs:\n    patch:\n      tags:\n        - Brokers\n      summary: updateBrokerTopicPartitionLogDir\n      operationId: updateBrokerTopicPartitionLogDir\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: id\n          in: path\n          required: true\n          schema:\n            type: integer\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/BrokerLogdirUpdate'\n      responses:\n        200:\n          description: OK\n\n  /api/clusters/{clusterName}/topics:\n    get:\n      tags:\n        - Topics\n      summary: getTopics\n      operationId: getTopics\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: page\n          in: query\n          required: false\n          schema:\n            type: integer\n        - name: perPage\n          in: query\n          required: false\n          schema:\n            type: integer\n        - name: showInternal\n          in: query\n          required: false\n          schema:\n            type: boolean\n        - name: search\n          in: query\n          required: false\n          schema:\n            type: string\n        - name: orderBy\n          in: query\n          required: false\n          schema:\n            $ref: '#/components/schemas/TopicColumnsToSort'\n        - name: sortOrder\n          in: query\n          required: false\n          schema:\n            $ref: '#/components/schemas/SortOrder'\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/TopicsResponse'\n    post:\n      tags:\n        - Topics\n      summary: createTopic\n      operationId: createTopic\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/TopicCreation'\n      responses:\n        201:\n          description: Created\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Topic'\n\n  /api/clusters/{clusterName}/topics/{topicName}/clone:\n    post:\n      tags:\n        - Topics\n      summary: cloneTopic\n      operationId: cloneTopic\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: topicName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: newTopicName\n          in: query\n          required: true\n          schema:\n            type: string\n      responses:\n        201:\n          description: Created\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Topic'\n        404:\n          description: Not found\n\n  /api/clusters/{clusterName}/topics/{topicName}/analysis:\n    get:\n      tags:\n        - Topics\n      summary: getTopicAnalysis\n      operationId: getTopicAnalysis\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: topicName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/TopicAnalysis'\n        404:\n          description: Not found\n    post:\n      tags:\n        - Topics\n      summary: analyzeTopic\n      operationId: analyzeTopic\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: topicName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: Analysis started\n        404:\n          description: Not found\n    delete:\n      tags:\n        - Topics\n      summary: cancelTopicAnalysis\n      operationId: cancelTopicAnalysis\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: topicName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: Analysis cancelled\n        404:\n          description: Not found\n\n\n  /api/clusters/{clusterName}/topics/{topicName}:\n    get:\n      tags:\n        - Topics\n      summary: getTopicDetails\n      operationId: getTopicDetails\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: topicName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/TopicDetails'\n    post:\n      tags:\n        - Topics\n      summary: recreateTopic\n      operationId: recreateTopic\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: topicName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        201:\n          description: Created\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Topic'\n        404:\n          description: Not found\n        408:\n          description: Topic recreation timeout\n    patch:\n      tags:\n        - Topics\n      summary: updateTopic\n      operationId: updateTopic\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: topicName\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/TopicUpdate'\n      responses:\n        200:\n          description: Updated\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Topic'\n    delete:\n      tags:\n        - Topics\n      summary: deleteTopic\n      operationId: deleteTopic\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: topicName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n        404:\n          description: Not found\n\n  /api/clusters/{clusterName}/topics/{topicName}/config:\n    get:\n      tags:\n        - Topics\n      summary: getTopicConfigs\n      operationId: getTopicConfigs\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: topicName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/TopicConfig'\n\n  /api/clusters/{clusterName}/topics/{topicName}/replications:\n    patch:\n      tags:\n        - Topics\n      summary: changeReplicationFactor\n      operationId: changeReplicationFactor\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: topicName\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/ReplicationFactorChange'\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ReplicationFactorChangeResponse'\n        404:\n          description: Not found\n        400:\n          description: Bad Request\n\n  /api/clusters/{clusterName}/topic/{topicName}/serdes:\n    get:\n      tags:\n        - Messages\n      summary: getSerdes\n      operationId: getSerdes\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: topicName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: use\n          in: query\n          required: true\n          schema:\n            $ref: '#/components/schemas/SerdeUsage'\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/TopicSerdeSuggestion'\n\n  /api/smartfilters/testexecutions:\n    put:\n      tags:\n        - Messages\n      summary: executeSmartFilterTest\n      operationId: executeSmartFilterTest\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/SmartFilterTestExecution'\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/SmartFilterTestExecutionResult'\n\n\n  /api/clusters/{clusterName}/topics/{topicName}/messages:\n    get:\n      tags:\n        - Messages\n      summary: getTopicMessages\n      operationId: getTopicMessages\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: topicName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: seekType\n          in: query\n          schema:\n            $ref: \"#/components/schemas/SeekType\"\n        - name: seekTo\n          in: query\n          schema:\n            type: array\n            items:\n              type: string\n          description: The format is [partition]::[offset] for specifying offsets or [partition]::[timestamp in millis] for specifying timestamps\n        - name: limit\n          in: query\n          schema:\n            type: integer\n        - name: q\n          in: query\n          schema:\n            type: string\n        - name: filterQueryType\n          in: query\n          schema:\n            $ref:  \"#/components/schemas/MessageFilterType\"\n        - name: seekDirection\n          in: query\n          schema:\n            $ref: \"#/components/schemas/SeekDirection\"\n        - name: keySerde\n          in: query\n          description: \"Serde that should be used for deserialization. Will be chosen automatically if not set.\"\n          schema:\n            type: string\n        - name: valueSerde\n          in: query\n          description: \"Serde that should be used for deserialization. Will be chosen automatically if not set.\"\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            text/event-stream:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/TopicMessageEvent'\n    delete:\n      tags:\n        - Messages\n      summary: deleteTopicMessages\n      operationId: deleteTopicMessages\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: topicName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: partitions\n          in: query\n          required: false\n          schema:\n            type: array\n            items:\n              type: integer\n      responses:\n        200:\n          description: OK\n        404:\n          description: Not found\n    post:\n      tags:\n        - Messages\n      summary: sendTopicMessages\n      operationId: sendTopicMessages\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: topicName\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/CreateTopicMessage'\n      responses:\n        200:\n          description: OK\n        404:\n          description: Not found\n\n  /api/clusters/{clusterName}/topics/{topicName}/activeproducers:\n    get:\n      tags:\n        - Topics\n      summary: get producer states for topic\n      operationId: getActiveProducerStates\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: topicName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/TopicProducerState'\n\n  /api/clusters/{clusterName}/topics/{topicName}/consumer-groups:\n    get:\n      tags:\n        - Consumer Groups\n      summary: get Consumer Groups By Topics\n      operationId: getTopicConsumerGroups\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: topicName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/ConsumerGroup'\n\n  /api/clusters/{clusterName}/consumer-groups/paged:\n    get:\n      tags:\n        - Consumer Groups\n      summary: Get consumer groups with paging support\n      operationId: getConsumerGroupsPage\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: page\n          in: query\n          required: false\n          schema:\n            type: integer\n        - name: perPage\n          in: query\n          required: false\n          schema:\n            type: integer\n        - name: search\n          in: query\n          required: false\n          schema:\n            type: string\n        - name: orderBy\n          in: query\n          required: false\n          schema:\n            $ref: '#/components/schemas/ConsumerGroupOrdering'\n        - name: sortOrder\n          in: query\n          required: false\n          schema:\n            $ref: '#/components/schemas/SortOrder'\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ConsumerGroupsPageResponse'\n\n\n  /api/clusters/{clusterName}/consumer-groups/{id}:\n    get:\n      tags:\n        - Consumer Groups\n      summary: get Consumer Group By Id\n      operationId: getConsumerGroup\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: id\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ConsumerGroupDetails'\n\n    delete:\n      tags:\n        - Consumer Groups\n      summary: Delete Consumer Group by ID\n      operationId: deleteConsumerGroup\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: id\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n\n  /api/clusters/{clusterName}/consumer-groups/{id}/offsets:\n    post:\n      tags:\n        - Consumer Groups\n      summary: resets consumer group offsets\n      operationId: resetConsumerGroupOffsets\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: id\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/ConsumerGroupOffsetsReset'\n      responses:\n        200:\n          description: OK\n\n  /api/clusters/{clusterName}/schemas:\n    post:\n      tags:\n        - Schemas\n      summary: create a new subject schema or update existing subject schema\n      operationId: createNewSchema\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/NewSchemaSubject'\n      responses:\n        200:\n          description: Ok\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/SchemaSubject'\n        400:\n          description: Bad request\n        409:\n          description: Duplicate schema\n        422:\n          description: Invalid parameters\n    get:\n      tags:\n        - Schemas\n      summary: get all schemas of latest version from Schema Registry service\n      operationId: getSchemas\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: page\n          in: query\n          required: false\n          schema:\n            type: integer\n        - name: perPage\n          in: query\n          required: false\n          schema:\n            type: integer\n        - name: search\n          in: query\n          required: false\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/SchemaSubjectsResponse'\n\n  /api/clusters/{clusterName}/schemas/{subject}:\n    delete:\n      tags:\n        - Schemas\n      summary: delete schema from Schema Registry service\n      operationId: deleteSchema\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: subject\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n        404:\n          description: Not found\n\n  /api/clusters/{clusterName}/schemas/{subject}/versions:\n    get:\n      tags:\n        - Schemas\n      summary: get all version of subject from Schema Registry service\n      operationId: getAllVersionsBySubject\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: subject\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/SchemaSubject'\n\n  /api/clusters/{clusterName}/schemas/{subject}/latest:\n    get:\n      tags:\n        - Schemas\n      summary: get the latest schema from Schema Registry service\n      operationId: getLatestSchema\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: subject\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/SchemaSubject'\n    delete:\n      tags:\n        - Schemas\n      summary: delete the latest schema from schema registry\n      operationId: deleteLatestSchema\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: subject\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n        404:\n          description: Not found\n\n\n  /api/clusters/{clusterName}/schemas/{subject}/versions/{version}:\n    get:\n      tags:\n        - Schemas\n      summary: get schema by version from Schema Registry service\n      operationId: getSchemaByVersion\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: subject\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: version\n          in: path\n          required: true\n          schema:\n            type: integer\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/SchemaSubject'\n    delete:\n      tags:\n        - Schemas\n      summary: delete schema by version from schema registry\n      operationId: deleteSchemaByVersion\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: subject\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: version\n          in: path\n          required: true\n          schema:\n            type: integer\n      responses:\n        200:\n          description: OK\n        404:\n          description: Not found\n\n  /api/clusters/{clusterName}/schemas/compatibility:\n    get:\n      tags:\n        - Schemas\n      summary: Get global schema compatibility level\n      operationId: getGlobalSchemaCompatibilityLevel\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/CompatibilityLevel'\n    put:\n      tags:\n        - Schemas\n      summary: Update compatibility level globally\n      operationId: updateGlobalSchemaCompatibilityLevel\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/CompatibilityLevel'\n      responses:\n        200:\n          description: OK\n        404:\n          description: Not Found\n\n  /api/clusters/{clusterName}/schemas/{subject}/compatibility:\n    put:\n      tags:\n        - Schemas\n      summary: Update compatibility level for specific schema.\n      operationId: updateSchemaCompatibilityLevel\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: subject\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/CompatibilityLevel'\n      responses:\n        200:\n          description: OK\n        404:\n          description: Not Found\n\n  /api/clusters/{clusterName}/schemas/{subject}/check:\n    post:\n      tags:\n        - Schemas\n      summary: Check compatibility of the schema.\n      operationId: checkSchemaCompatibility\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: subject\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/NewSchemaSubject'\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/CompatibilityCheckResponse'\n        404:\n          description: Not Found\n\n  /api/clusters/{clusterName}/connects:\n    get:\n      tags:\n        - Kafka Connect\n      summary: get all kafka connect instances\n      operationId: getConnects\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/Connect'\n\n  /api/clusters/{clusterName}/connectors:\n    get:\n      tags:\n        - Kafka Connect\n      summary: get filtered kafka connectors\n      operationId: getAllConnectors\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: search\n          in: query\n          required: false\n          schema:\n            type: string\n        - name: orderBy\n          in: query\n          required: false\n          schema:\n            $ref: '#/components/schemas/ConnectorColumnsToSort'\n        - name: sortOrder\n          in: query\n          required: false\n          schema:\n            $ref: '#/components/schemas/SortOrder'\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/FullConnectorInfo'\n\n  /api/clusters/{clusterName}/connects/{connectName}/connectors:\n    get:\n      tags:\n        - Kafka Connect\n      summary: get connectors for provided kafka connect instance\n      operationId: getConnectors\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: connectName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: string\n    post:\n      tags:\n        - Kafka Connect\n      summary: create new connector\n      operationId: createConnector\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: connectName\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/NewConnector'\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Connector'\n        409:\n          description: rebalance is in progress\n\n  /api/clusters/{clusterName}/connects/{connectName}/connectors/{connectorName}:\n    get:\n      tags:\n        - Kafka Connect\n      summary: get information about the connector\n      operationId: getConnector\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: connectName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: connectorName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Connector'\n    delete:\n      tags:\n        - Kafka Connect\n      summary: delete connector\n      operationId: deleteConnector\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: connectName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: connectorName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n        409:\n          description: rebalance is in progress\n\n  /api/clusters/{clusterName}/connects/{connectName}/connectors/{connectorName}/action/{action}:\n    post:\n      tags:\n        - Kafka Connect\n      summary: update connector state (restart, pause or resume)\n      operationId: updateConnectorState\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: connectName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: connectorName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: action\n          in: path\n          required: true\n          schema:\n            $ref: '#/components/schemas/ConnectorAction'\n      responses:\n        200:\n          description: OK\n        409:\n          description: rebalance is in progress\n\n  /api/clusters/{clusterName}/connects/{connectName}/connectors/{connectorName}/config:\n    get:\n      tags:\n        - Kafka Connect\n      summary: get connector configuration\n      operationId: getConnectorConfig\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: connectName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: connectorName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ConnectorConfig'\n    put:\n      tags:\n        - Kafka Connect\n      summary: update or create connector with provided config\n      operationId: setConnectorConfig\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: connectName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: connectorName\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/ConnectorConfig'\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Connector'\n        409:\n          description: rebalance is in progress\n\n  /api/clusters/{clusterName}/connects/{connectName}/connectors/{connectorName}/tasks:\n    get:\n      tags:\n        - Kafka Connect\n      summary: get connector tasks\n      operationId: getConnectorTasks\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: connectName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: connectorName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/Task'\n\n  /api/clusters/{clusterName}/connects/{connectName}/connectors/{connectorName}/tasks/{taskId}/action/restart:\n    post:\n      tags:\n        - Kafka Connect\n      summary: restart connector task\n      operationId: restartConnectorTask\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: connectName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: connectorName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: taskId\n          in: path\n          required: true\n          schema:\n            type: integer\n      responses:\n        200:\n          description: OK\n\n\n  /api/clusters/{clusterName}/ksql/v2:\n    post:\n      tags:\n        - Ksql\n      summary: executeKsql\n      operationId: executeKsql\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/KsqlCommandV2'\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/KsqlCommandV2Response'\n\n  /api/clusters/{clusterName}/ksql/tables:\n    get:\n      tags:\n        - Ksql\n      summary: listTables\n      operationId: listTables\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/KsqlTableDescription'\n\n  /api/clusters/{clusterName}/ksql/streams:\n    get:\n      tags:\n        - Ksql\n      summary: listStreams\n      operationId: listStreams\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/KsqlStreamDescription'\n\n  /api/clusters/{clusterName}/ksql/response:\n    get:\n      tags:\n        - Ksql\n      summary: Open SSE pipe\n      operationId: openKsqlResponsePipe\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: pipeId\n          in: query\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            text/event-stream:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/KsqlResponse'\n\n  /api/clusters/{clusterName}/connects/{connectName}/plugins:\n    get:\n      tags:\n        - Kafka Connect\n      summary: get connector plugins\n      operationId: getConnectorPlugins\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: connectName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/ConnectorPlugin'\n\n  /api/clusters/{clusterName}/connects/{connectName}/plugins/{pluginName}/config/validate:\n    put:\n      tags:\n        - Kafka Connect\n      summary: validate connector plugin configuration\n      operationId: validateConnectorPluginConfig\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: connectName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: pluginName\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/ConnectorConfig'\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ConnectorPluginConfigValidationResponse'\n\n  /api/clusters/{clusterName}/topics/{topicName}/partitions:\n    patch:\n      tags:\n        - Topics\n      summary: increaseTopicPartitions\n      operationId: increaseTopicPartitions\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: topicName\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/PartitionsIncrease'\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/PartitionsIncreaseResponse'\n        404:\n          description: Not found\n\n  /api/clusters/{clusterName}/acls:\n    get:\n      tags:\n        - Acls\n      summary: listKafkaAcls\n      operationId: listAcls\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: resourceType\n          in: query\n          required: false\n          schema:\n            $ref: '#/components/schemas/KafkaAclResourceType'\n        - name: resourceName\n          in: query\n          required: false\n          schema:\n            type: string\n        - name: namePatternType\n          in: query\n          required: false\n          schema:\n            $ref: '#/components/schemas/KafkaAclNamePatternType'\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/KafkaAcl'\n\n  /api/clusters/{clusterName}/acl/csv:\n    get:\n      tags:\n        - Acls\n      summary: getAclAsCsv\n      operationId: getAclAsCsv\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: OK\n          content:\n            text/plain:\n              schema:\n                type: string\n    post:\n      tags:\n        - Acls\n      summary: syncAclsCsv\n      operationId: syncAclsCsv\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          text/plain:\n            schema:\n              type: string\n      responses:\n        200:\n          description: OK\n\n  /api/clusters/{clusterName}/acl:\n    post:\n      tags:\n        - Acls\n      summary: createAcl\n      operationId: createAcl\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/KafkaAcl'\n      responses:\n        200:\n          description: OK\n\n    delete:\n      tags:\n        - Acls\n      summary: deleteAcl\n      operationId: deleteAcl\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/KafkaAcl'\n      responses:\n        200:\n          description: OK\n        404:\n          description: Acl not found\n\n  /api/clusters/{clusterName}/acl/consumer:\n    post:\n      tags:\n        - Acls\n      summary: createConsumerAcl\n      operationId: createConsumerAcl\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/CreateConsumerAcl'\n      responses:\n        200:\n          description: OK\n\n  /api/clusters/{clusterName}/acl/producer:\n    post:\n      tags:\n        - Acls\n      summary: createProducerAcl\n      operationId: createProducerAcl\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/CreateProducerAcl'\n      responses:\n        200:\n          description: OK\n\n  /api/clusters/{clusterName}/acl/streamApp:\n    post:\n      tags:\n        - Acls\n      summary: createStreamAppAcl\n      operationId: createStreamAppAcl\n      parameters:\n        - name: clusterName\n          in: path\n          required: true\n          schema:\n            type: string\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/CreateStreamAppAcl'\n      responses:\n        200:\n          description: OK\n\n  /api/authorization:\n    get:\n      tags:\n        - Authorization\n      summary: Get user authentication related info\n      operationId: getUserAuthInfo\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/AuthenticationInfo'\n\n  /api/info:\n    get:\n      tags:\n        - ApplicationConfig\n      summary: Gets application info\n      operationId: getApplicationInfo\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApplicationInfo'\n\n  /api/config:\n    get:\n      tags:\n        - ApplicationConfig\n      summary: Gets current application configuration\n      operationId: getCurrentConfig\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApplicationConfig'\n    put:\n      tags:\n        - ApplicationConfig\n      summary: Restarts application with specified configuration\n      operationId: restartWithConfig\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/RestartRequest'\n      responses:\n        200:\n          description: OK\n\n  /api/config/validated:\n    put:\n      tags:\n        - ApplicationConfig\n      summary: Restarts application with specified configuration\n      operationId: validateConfig\n      requestBody:\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/ApplicationConfig'\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApplicationConfigValidation'\n\n\n  /api/config/relatedfiles:\n    post:\n      tags:\n        - ApplicationConfig\n      summary: Restarts application with specified configuration\n      operationId: uploadConfigRelatedFile\n      requestBody:\n        content:\n          multipart/form-data:\n            schema:\n              type: object\n              properties:\n                file:\n                  type: string\n                  format: binary\n      responses:\n        200:\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/UploadedFileInfo'\n\ncomponents:\n  schemas:\n    TopicSerdeSuggestion:\n      type: object\n      properties:\n        key:\n          type: array\n          items:\n            $ref: '#/components/schemas/SerdeDescription'\n        value:\n          type: array\n          items:\n            $ref: '#/components/schemas/SerdeDescription'\n\n    SerdeDescription:\n      type: object\n      properties:\n        name:\n          type: string\n        description:\n          type: string\n        preferred:\n          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.\"\n          type: boolean\n        schema:\n          type: string\n        additionalProperties:\n          type: object\n          additionalProperties:\n            type: object\n\n    SerdeUsage:\n      type: string\n      enum:\n        - SERIALIZE\n        - DESERIALIZE\n\n    ErrorResponse:\n      description: Error object that will be returned with 4XX and 5XX HTTP statuses\n      type: object\n      properties:\n        code:\n          type: integer\n          description: Internal error code (can be used for message formatting & localization on UI)\n        message:\n          type: string\n          description: Error message\n        timestamp:\n          type: number\n          description: Response unix timestamp in ms\n        requestId:\n          type: string\n          description: Unique server-defined request id for convenient debugging\n        fieldsErrors:\n          type: array\n          items:\n            $ref: '#/components/schemas/FieldError'\n        stackTrace:\n          type: string\n\n    FieldError:\n      type: object\n      properties:\n        fieldName:\n          type: string\n          description: Name of field that violated format\n        restrictions:\n          description: Field format violations description (ex. [\"size must be between 0 and 20\", \"must be a well-formed email address\"])\n          type: array\n          items:\n            type: string\n\n    MetricsCollectionError:\n      type: object\n      properties:\n        message:\n          type: string\n        stackTrace:\n          type: string\n\n    ApplicationInfo:\n      type: object\n      properties:\n        enabledFeatures:\n          type: array\n          items:\n            type: string\n            enum:\n              - DYNAMIC_CONFIG\n        build:\n          type: object\n          properties:\n            commitId:\n              type: string\n            version:\n              type: string\n            buildTime:\n              type: string\n            isLatestRelease:\n              type: boolean\n        latestRelease:\n          type: object\n          properties:\n            versionTag:\n              type: string\n            publishedAt:\n              type: string\n            htmlUrl:\n              type: string\n\n    Cluster:\n      type: object\n      properties:\n        name:\n          type: string\n        defaultCluster:\n          type: boolean\n        status:\n          $ref: '#/components/schemas/ServerStatus'\n        lastError:\n          $ref: '#/components/schemas/MetricsCollectionError'\n        brokerCount:\n          type: integer\n        onlinePartitionCount:\n          type: integer\n        topicCount:\n          type: integer\n        bytesInPerSec:\n          type: number\n        bytesOutPerSec:\n          type: number\n        readOnly:\n          type: boolean\n        version:\n          type: string\n        features:\n          type: array\n          items:\n            type: string\n            enum:\n              - SCHEMA_REGISTRY\n              - KAFKA_CONNECT\n              - KSQL_DB\n              - TOPIC_DELETION\n              - KAFKA_ACL_VIEW # get ACLs listing\n              - KAFKA_ACL_EDIT # create & delete ACLs\n      required:\n        - id\n        - name\n        - status\n\n    ServerStatus:\n      type: string\n      enum:\n        - online\n        - offline\n        - initializing\n\n    ClusterMetrics:\n      type: object\n      properties:\n        items:\n          type: array\n          items:\n            $ref: '#/components/schemas/Metric'\n\n    ClusterStats:\n      type: object\n      properties:\n        brokerCount:\n          type: integer\n        zooKeeperStatus:\n          type: integer\n          deprecated: true\n        activeControllers:\n          type: integer\n          description: Id of broker which is cluster's controller. null, if controller not known yet.\n        onlinePartitionCount:\n          type: integer\n        offlinePartitionCount:\n          type: integer\n        inSyncReplicasCount:\n          type: integer\n        outOfSyncReplicasCount:\n          type: integer\n        underReplicatedPartitionCount:\n          type: integer\n        diskUsage:\n          type: array\n          items:\n            $ref: '#/components/schemas/BrokerDiskUsage'\n        version:\n          type: string\n\n    BrokerDiskUsage:\n      type: object\n      properties:\n        brokerId:\n          type: integer\n        segmentSize:\n          type: integer\n          format: int64\n        segmentCount:\n          type: integer\n      required:\n        - brokerId\n\n    BrokerMetrics:\n      type: object\n      properties:\n        segmentSize:\n          type: integer\n          format: int64\n        segmentCount:\n          type: integer\n        metrics:\n          type: array\n          items:\n            $ref: '#/components/schemas/Metric'\n\n    BrokerLogdirs:\n      type: object\n      properties:\n        name:\n          type: string\n        error:\n          type: string\n        topics:\n          type: array\n          items:\n            $ref: '#/components/schemas/TopicLogdirs'\n\n    BrokersLogdirs:\n      type: object\n      properties:\n        name:\n          type: string\n        error:\n          type: string\n        topics:\n          type: array\n          items:\n            $ref: '#/components/schemas/BrokerTopicLogdirs'\n\n    TopicsResponse:\n      type: object\n      properties:\n        pageCount:\n          type: integer\n        topics:\n          type: array\n          items:\n            $ref: '#/components/schemas/Topic'\n\n    TopicColumnsToSort:\n      type: string\n      enum:\n        - NAME\n        - OUT_OF_SYNC_REPLICAS\n        - TOTAL_PARTITIONS\n        - REPLICATION_FACTOR\n        - SIZE\n\n    ConnectorColumnsToSort:\n      type: string\n      enum:\n        - NAME\n        - CONNECT\n        - TYPE\n        - STATUS\n\n    SortOrder:\n      type: string\n      enum:\n        - ASC\n        - DESC\n\n    Topic:\n      type: object\n      properties:\n        name:\n          type: string\n        internal:\n          type: boolean\n        partitionCount:\n          type: integer\n        replicationFactor:\n          type: integer\n        replicas:\n          type: integer\n        inSyncReplicas:\n          type: integer\n        segmentSize:\n          type: integer\n          format: int64\n        segmentCount:\n          type: integer\n        bytesInPerSec:\n          type: number\n        bytesOutPerSec:\n          type: number\n        underReplicatedPartitions:\n          type: integer\n        cleanUpPolicy:\n          $ref: '#/components/schemas/CleanUpPolicy'\n        partitions:\n          type: array\n          items:\n            $ref: \"#/components/schemas/Partition\"\n      required:\n        - name\n\n    TopicAnalysis:\n      type: object\n      description: \"Represents analysis state. Note: 'progress' and 'result' fields are set exclusively depending on analysis state.\"\n      properties:\n        progress:\n          $ref: '#/components/schemas/TopicAnalysisProgress'\n        result:\n          $ref: '#/components/schemas/TopicAnalysisResult'\n\n    TopicAnalysisProgress:\n      type: object\n      properties:\n        startedAt:\n          type: integer\n          format: int64\n        completenessPercent:\n          type: number\n        msgsScanned:\n          type: integer\n          format: int64\n        bytesScanned:\n          type: integer\n          format: int64\n\n    TopicAnalysisResult:\n      type: object\n      properties:\n        startedAt:\n          type: integer\n          format: int64\n        finishedAt:\n          type: integer\n          format: int64\n        error:\n          type: string\n        totalStats:\n          $ref: '#/components/schemas/TopicAnalysisStats'\n        partitionStats:\n          type: array\n          items:\n            $ref: \"#/components/schemas/TopicAnalysisStats\"\n\n    TopicAnalysisStats:\n      type: object\n      properties:\n        partition:\n          type: integer\n          format: int32\n          description: \"null if this is total stats\"\n        totalMsgs:\n          type: integer\n          format: int64\n        minOffset:\n          type: integer\n          format: int64\n        maxOffset:\n          type: integer\n          format: int64\n        minTimestamp:\n          type: integer\n          format: int64\n        maxTimestamp:\n          type: integer\n          format: int64\n        nullKeys:\n          type: integer\n          format: int64\n        nullValues:\n          type: integer\n          format: int64\n        approxUniqKeys:\n          type: integer\n          format: int64\n        approxUniqValues:\n          type: integer\n          format: int64\n        keySize:\n          $ref: \"#/components/schemas/TopicAnalysisSizeStats\"\n        valueSize:\n          $ref: \"#/components/schemas/TopicAnalysisSizeStats\"\n        hourlyMsgCounts:\n          type: array\n          items:\n            type: object\n            properties:\n              hourStart:\n                type: integer\n                format: int64\n              count:\n                type: integer\n                format: int64\n\n    TopicAnalysisSizeStats:\n      type: object\n      description: \"All sizes in bytes\"\n      properties:\n        sum:\n          type: integer\n          format: int64\n        min:\n          type: integer\n          format: int64\n        max:\n          type: integer\n          format: int64\n        avg:\n          type: integer\n          format: int64\n        prctl50:\n          type: integer\n          format: int64\n        prctl75:\n          type: integer\n          format: int64\n        prctl95:\n          type: integer\n          format: int64\n        prctl99:\n          type: integer\n          format: int64\n        prctl999:\n          type: integer\n          format: int64\n\n    Replica:\n      type: object\n      properties:\n        broker:\n          type: integer\n        leader:\n          type: boolean\n        inSync:\n          type: boolean\n\n    TopicDetails:\n      type: object\n      properties:\n        name:\n          type: string\n        internal:\n          type: boolean\n        partitions:\n          type: array\n          items:\n            $ref: \"#/components/schemas/Partition\"\n        partitionCount:\n          type: integer\n        replicationFactor:\n          type: integer\n        replicas:\n          type: integer\n        inSyncReplicas:\n          type: integer\n        bytesInPerSec:\n          type: number\n        bytesOutPerSec:\n          type: number\n        segmentSize:\n          type: integer\n          format: int64\n        segmentCount:\n          type: integer\n        underReplicatedPartitions:\n          type: integer\n        cleanUpPolicy:\n          $ref: '#/components/schemas/CleanUpPolicy'\n        keySerde:\n          type: string\n        valueSerde:\n          type: string\n      required:\n        - name\n\n    TopicConfig:\n      type: object\n      properties:\n        name:\n          type: string\n        value:\n          type: string\n        defaultValue:\n          type: string\n        source:\n          $ref: \"#/components/schemas/ConfigSource\"\n        isSensitive:\n          type: boolean\n        isReadOnly:\n          type: boolean\n        synonyms:\n          type: array\n          items:\n            $ref: \"#/components/schemas/ConfigSynonym\"\n        doc:\n          type: string\n      required:\n        - name\n\n    TopicCreation:\n      type: object\n      properties:\n        name:\n          type: string\n        partitions:\n          type: integer\n        replicationFactor:\n          type: integer\n        configs:\n          type: object\n          additionalProperties:\n            type: string\n      required:\n        - name\n        - partitions\n\n    TopicUpdate:\n      type: object\n      properties:\n        configs:\n          type: object\n          additionalProperties:\n            type: string\n      required:\n        - configs\n\n    Broker:\n      type: object\n      properties:\n        id:\n          type: integer\n        host:\n          type: string\n        port:\n          type: integer\n        bytesInPerSec:\n          type: number\n        bytesOutPerSec:\n          type: number\n        partitionsLeader:\n          type: integer\n        partitions:\n          type: integer\n        inSyncPartitions:\n          type: integer\n        partitionsSkew:\n          type: number\n        leadersSkew:\n          type: number\n      required:\n        - id\n\n    BrokerLogdirUpdate:\n      type: object\n      properties:\n        topic:\n          type: string\n        partition:\n          type: integer\n        logDir:\n          type: string\n\n    ConsumerGroupState:\n      type: string\n      enum:\n        - UNKNOWN\n        - PREPARING_REBALANCE\n        - COMPLETING_REBALANCE\n        - STABLE\n        - DEAD\n        - EMPTY\n\n    MessageFormat:\n      type: string\n      enum:\n        - AVRO\n        - JSON\n        - PROTOBUF\n        - UNKNOWN\n\n    TopicProducerState:\n      type: object\n      properties:\n        partition:\n          type: integer\n          format: int32\n        producerId:\n          type: integer\n          format: int64\n        producerEpoch:\n          type: integer\n          format: int32\n        lastSequence:\n          type: integer\n          format: int32\n        lastTimestampMs:\n          type: integer\n          format: int64\n        coordinatorEpoch:\n          type: integer\n          format: int32\n        currentTransactionStartOffset:\n          type: integer\n          format: int64\n\n    ConsumerGroup:\n      discriminator:\n        propertyName: inherit\n        mapping:\n          details: \"#/components/schemas/ConsumerGroupDetails\"\n      type: object\n      properties:\n        groupId:\n          type: string\n        members:\n          type: integer\n        topics:\n          type: integer\n        simple:\n          type: boolean\n        partitionAssignor:\n          type: string\n        state:\n          $ref: \"#/components/schemas/ConsumerGroupState\"\n        coordinator:\n          $ref: \"#/components/schemas/Broker\"\n        consumerLag:\n          type: integer\n          format: int64\n          description: null if consumer group has no offsets committed\n      required:\n        - groupId\n\n    ConsumerGroupOrdering:\n      type: string\n      enum:\n        - NAME\n        - MEMBERS\n        - STATE\n        - MESSAGES_BEHIND\n        - TOPIC_NUM\n\n    ConsumerGroupsPageResponse:\n      type: object\n      properties:\n        pageCount:\n          type: integer\n        consumerGroups:\n          type: array\n          items:\n            $ref: '#/components/schemas/ConsumerGroup'\n\n    SmartFilterTestExecution:\n      type: object\n      required: [filterCode]\n      properties:\n        filterCode:\n          type: string\n        key:\n          type: string\n        value:\n          type: string\n        headers:\n          type: object\n          additionalProperties:\n            type: string\n        partition:\n          type: integer\n        offset:\n          type: integer\n          format: int64\n        timestampMs:\n          type: integer\n          format: int64\n\n    SmartFilterTestExecutionResult:\n      type: object\n      properties:\n        result:\n          type: boolean\n        error:\n          type: string\n\n    CreateTopicMessage:\n      type: object\n      properties:\n        partition:\n          type: integer\n        key:\n          type: string\n          nullable: true\n        headers:\n          type: object\n          additionalProperties:\n            type: string\n        content:\n          type: string\n          nullable: true\n        keySerde:\n          type: string\n          nullable: true\n        valueSerde:\n          type: string\n          nullable: true\n      required:\n        - partition\n\n    TopicMessageEvent:\n      type: object\n      properties:\n        type:\n          type: string\n          enum:\n            - PHASE\n            - MESSAGE\n            - CONSUMING\n            - DONE\n            - EMIT_THROTTLING\n        message:\n          $ref: \"#/components/schemas/TopicMessage\"\n        phase:\n          $ref: \"#/components/schemas/TopicMessagePhase\"\n        consuming:\n          $ref: \"#/components/schemas/TopicMessageConsuming\"\n\n    TopicMessagePhase:\n      type: object\n      properties:\n        name:\n          type: string\n\n    TimeStampFormat:\n      type: object\n      properties:\n        timeStampFormat:\n          type: string\n\n    TopicMessageConsuming:\n      type: object\n      properties:\n        bytesConsumed:\n          type: integer\n          format: int64\n        elapsedMs:\n          type: integer\n          format: int64\n        isCancelled:\n          type: boolean\n        messagesConsumed:\n          type: integer\n        filterApplyErrors:\n          type: integer\n\n\n    TopicMessage:\n      type: object\n      properties:\n        partition:\n          type: integer\n        offset:\n          type: integer\n          format: int64\n        timestamp:\n          type: string\n          format: date-time\n        timestampType:\n          type: string\n          enum:\n            - NO_TIMESTAMP_TYPE\n            - CREATE_TIME\n            - LOG_APPEND_TIME\n        key:\n          type: string\n        headers:\n          type: object\n          additionalProperties:\n            type: string\n        content:\n          type: string\n        keyFormat:\n          #deprecated - wont be filled - use 'keySerde' field instead\n          $ref: \"#/components/schemas/MessageFormat\"\n        valueFormat:\n          #deprecated - wont be filled - use 'valueSerde' field instead\n          $ref: \"#/components/schemas/MessageFormat\"\n        keySize:\n          type: integer\n          format: int64\n        valueSize:\n          type: integer\n          format: int64\n        keySchemaId:\n          deprecated: true\n          description: deprecated - wont be filled - use 'keyDeserializeProperties' field instead\n          type: string\n        valueSchemaId:\n          deprecated: true\n          description: deprecated - wont be filled - use 'valueDeserializeProperties' field instead\n          type: string\n        headersSize:\n          type: integer\n          format: int64\n        keySerde:\n          type: string\n        valueSerde:\n          type: string\n        keyDeserializeProperties:\n          additionalProperties:\n            type: object\n        valueDeserializeProperties:\n          additionalProperties:\n            type: object\n      required:\n        - partition\n        - offset\n        - timestamp\n\n    SeekType:\n      type: string\n      enum:\n        - BEGINNING\n        - OFFSET\n        - TIMESTAMP\n        - LATEST\n\n    MessageFilterType:\n      type: string\n      enum:\n        - STRING_CONTAINS\n        - GROOVY_SCRIPT\n\n    SeekDirection:\n      type: string\n      enum:\n        - FORWARD\n        - BACKWARD\n        - TAILING\n      default: FORWARD\n\n    Partition:\n      type: object\n      properties:\n        partition:\n          type: integer\n        leader:\n          type: integer\n        replicas:\n          type: array\n          items:\n            $ref: '#/components/schemas/Replica'\n        offsetMax:\n          type: integer\n          format: int64\n        offsetMin:\n          type: integer\n          format: int64\n      required:\n        - topic\n        - partition\n        - offsetMax\n        - offsetMin\n\n    ConsumerGroupTopicPartition:\n      type: object\n      properties:\n        topic:\n          type: string\n        partition:\n          type: integer\n        currentOffset:\n          type: integer\n          format: int64\n        endOffset:\n          type: integer\n          format: int64\n        consumerLag:\n          type: integer\n          format: int64\n          description: null if consumer group has no offsets committed\n        consumerId:\n          type: string\n        host:\n          type: string\n      required:\n        - topic\n        - partition\n\n\n    ConsumerGroupDetails:\n      allOf:\n        - $ref: '#/components/schemas/ConsumerGroup'\n        - type: object\n          properties:\n            partitions:\n              type: array\n              items:\n                $ref: '#/components/schemas/ConsumerGroupTopicPartition'\n\n    Metric:\n      type: object\n      properties:\n        name:\n          type: string\n        labels:\n          type: string\n          additionalProperties:\n            type: string\n        value:\n          type: number\n\n    TopicLogdirs:\n      type: object\n      properties:\n        name:\n          type: string\n        partitions:\n          type: array\n          items:\n            $ref: '#/components/schemas/TopicPartitionLogdir'\n\n    BrokerTopicLogdirs:\n      type: object\n      properties:\n        name:\n          type: string\n        partitions:\n          type: array\n          items:\n            $ref: '#/components/schemas/BrokerTopicPartitionLogdir'\n\n    TopicPartitionLogdir:\n      type: object\n      properties:\n        partition:\n          type: integer\n        size:\n          type: integer\n          format: int64\n        offsetLag:\n          type: integer\n          format: int64\n\n    BrokerTopicPartitionLogdir:\n      allOf:\n        - $ref: '#/components/schemas/TopicPartitionLogdir'\n        - type: object\n          properties:\n            broker:\n              type: integer\n\n    SchemaSubject:\n      type: object\n      properties:\n        subject:\n          type: string\n        version:\n          type: string\n        id:\n          type: integer\n        schema:\n          type: string\n        compatibilityLevel:\n          type: string\n        schemaType:\n          $ref: '#/components/schemas/SchemaType'\n        references:\n          type: array\n          items:\n            $ref: '#/components/schemas/SchemaReference'\n      required:\n        - id\n        - subject\n        - version\n        - schema\n        - compatibilityLevel\n        - schemaType\n\n    NewSchemaSubject:\n      type: object\n      description: should be set for creating/updating schema subject\n      properties:\n        subject:\n          type: string\n        schema:\n          type: string\n        schemaType:\n          $ref: '#/components/schemas/SchemaType' # upon updating a schema, the type of existing schema can't be changed\n        references:\n          type: array\n          items:\n            $ref: '#/components/schemas/SchemaReference'\n      required:\n        - subject\n        - schema\n        - schemaType\n\n    SchemaReference:\n      type: object\n      properties:\n        name:\n          type: string\n        subject:\n          type: string\n        version:\n          type: integer\n      required:\n        - name\n        - subject\n        - version\n\n    CompatibilityLevel:\n      type: object\n      properties:\n        compatibility:\n          type: string\n          enum:\n            - BACKWARD\n            - BACKWARD_TRANSITIVE\n            - FORWARD\n            - FORWARD_TRANSITIVE\n            - FULL\n            - FULL_TRANSITIVE\n            - NONE\n      required:\n        - compatibility\n\n    SchemaType:\n      type: string\n      description: upon updating a schema, the type of an existing schema can't be changed\n      enum:\n        - AVRO\n        - JSON\n        - PROTOBUF\n\n    CompatibilityCheckResponse:\n      type: object\n      properties:\n        isCompatible:\n          type: boolean\n      required:\n        - isCompatible\n\n    SchemaSubjectsResponse:\n      type: object\n      properties:\n        pageCount:\n          type: integer\n        schemas:\n          type: array\n          items:\n            $ref: '#/components/schemas/SchemaSubject'\n\n    Connect:\n      type: object\n      properties:\n        name:\n          type: string\n        address:\n          type: string\n      required:\n        - name\n\n    ConnectorConfig:\n      type: object\n      additionalProperties:\n        type: object\n\n    TaskId:\n      type: object\n      properties:\n        connector:\n          type: string\n        task:\n          type: integer\n\n    Task:\n      type: object\n      properties:\n        id:\n          $ref: '#/components/schemas/TaskId'\n        status:\n          $ref: '#/components/schemas/TaskStatus'\n        config:\n          $ref: '#/components/schemas/ConnectorConfig'\n      required:\n        - status\n\n    NewConnector:\n      type: object\n      properties:\n        name:\n          type: string\n        config:\n          $ref: '#/components/schemas/ConnectorConfig'\n      required:\n        - name\n        - config\n\n    Connector:\n      allOf:\n        - $ref: '#/components/schemas/NewConnector'\n        - type: object\n          properties:\n            tasks:\n              type: array\n              items:\n                $ref: '#/components/schemas/TaskId'\n            type:\n              $ref: '#/components/schemas/ConnectorType'\n            status:\n              $ref: '#/components/schemas/ConnectorStatus'\n            connect:\n              type: string\n          required:\n            - type\n            - status\n            - connect\n\n    ConnectorType:\n      type: string\n      enum:\n        - SOURCE\n        - SINK\n\n    ConsumerGroupOffsetsReset:\n      type: object\n      properties:\n        topic:\n          type: string\n        resetType:\n          $ref: '#/components/schemas/ConsumerGroupOffsetsResetType'\n        partitions:\n          type: array\n          items:\n            type: integer\n          description: list of target partitions, all partitions will be used if it is not set or empty\n        resetToTimestamp:\n          type: integer\n          format: int64\n          description: should be set if resetType is TIMESTAMP\n        partitionsOffsets:\n          type: array\n          items:\n            $ref: '#/components/schemas/PartitionOffset'\n          description: List of partition offsets to reset to, should be set when resetType is OFFSET\n      required:\n        - topic\n        - resetType\n\n    PartitionOffset:\n      type: object\n      properties:\n        partition:\n          type: integer\n        offset:\n          type: integer\n          format: int64\n      required:\n        - partition\n\n    ConsumerGroupOffsetsResetType:\n      type: string\n      enum:\n        - EARLIEST\n        - LATEST\n        - TIMESTAMP\n        - OFFSET\n\n    TaskStatus:\n      type: object\n      properties:\n        id:\n          type: integer\n        state:\n          $ref: '#/components/schemas/ConnectorTaskStatus'\n        worker_id:\n          type: string\n        trace:\n          type: string\n      required:\n        - id\n        - state\n        - worker_id\n\n    ConnectorStatus:\n      type: object\n      properties:\n        state:\n          $ref: '#/components/schemas/ConnectorState'\n        worker_id:\n          type: string\n      required:\n        - state\n\n    ConnectorTaskStatus:\n      type: string\n      enum:\n        - RUNNING\n        - FAILED\n        - PAUSED\n        - RESTARTING\n        - UNASSIGNED\n\n    ConnectorState:\n      type: string\n      enum:\n        - RUNNING\n        - FAILED\n        - PAUSED\n        - UNASSIGNED\n        - TASK_FAILED\n\n    ConnectorAction:\n      type: string\n      enum:\n        - RESTART\n        - RESTART_ALL_TASKS\n        - RESTART_FAILED_TASKS\n        - PAUSE\n        - RESUME\n\n    TaskAction:\n      type: string\n      enum:\n        - restart\n\n    ConnectorPlugin:\n      type: object\n      properties:\n        class:\n          type: string\n\n    ConnectorPluginConfigDefinition:\n      type: object\n      properties:\n        name:\n          type: string\n        type:\n          type: string\n          enum:\n            - BOOLEAN\n            - CLASS\n            - DOUBLE\n            - INT\n            - LIST\n            - LONG\n            - PASSWORD\n            - SHORT\n            - STRING\n        required:\n          type: boolean\n        default_value:\n          type: string\n        importance:\n          type: string\n          enum:\n            - LOW\n            - MEDIUM\n            - HIGH\n        documentation:\n          type: string\n        group:\n          type: string\n        width:\n          type: string\n          enum:\n            - SHORT\n            - MEDIUM\n            - LONG\n            - NONE\n        display_name:\n          type: string\n        dependents:\n          type: array\n          items:\n            type: string\n        order:\n          type: integer\n\n    ConnectorPluginConfigValue:\n      type: object\n      properties:\n        name:\n          type: string\n        value:\n          type: string\n        recommended_values:\n          type: array\n          items:\n            type: string\n        errors:\n          type: array\n          items:\n            type: string\n        visible:\n          type: boolean\n\n    ConnectorPluginConfig:\n      type: object\n      properties:\n        definition:\n          $ref: '#/components/schemas/ConnectorPluginConfigDefinition'\n        value:\n          $ref: '#/components/schemas/ConnectorPluginConfigValue'\n\n    ConnectorPluginConfigValidationResponse:\n      type: object\n      properties:\n        name:\n          type: string\n        error_count:\n          type: integer\n        groups:\n          type: array\n          items:\n            type: string\n        configs:\n          type: array\n          items:\n            $ref: '#/components/schemas/ConnectorPluginConfig'\n\n    KsqlCommandV2:\n      type: object\n      properties:\n        ksql:\n          type: string\n        streamsProperties:\n          type: object\n          additionalProperties:\n            type: string\n      required:\n        - ksql\n\n    KsqlCommandV2Response:\n      type: object\n      properties:\n        pipeId:\n          type: string\n      required:\n        - pipeId\n\n    KsqlTableDescription:\n      type: object\n      properties:\n        name:\n          type: string\n        topic:\n          type: string\n        keyFormat:\n          type: string\n        valueFormat:\n          type: string\n        isWindowed:\n          type: boolean\n\n    KsqlStreamDescription:\n      type: object\n      properties:\n        name:\n          type: string\n        topic:\n          type: string\n        keyFormat:\n          type: string\n        valueFormat:\n          type: string\n\n    KsqlResponse:\n      type: object\n      properties:\n        table:\n          $ref: '#/components/schemas/KsqlTableResponse'\n\n    KsqlTableResponse:\n      type: object\n      properties:\n        header:\n          type: string\n        columnNames:\n          type: array\n          items:\n            type: string\n        values:\n          type: array\n          items:\n            type: array\n            items:\n              type: object\n\n    FullConnectorInfo:\n      type: object\n      properties:\n        connect:\n          type: string\n        name:\n          type: string\n        connector_class:\n          type: string\n        type:\n          $ref: '#/components/schemas/ConnectorType'\n        topics:\n          type: array\n          items:\n            type: string\n        status:\n          $ref: '#/components/schemas/ConnectorStatus'\n        tasks_count:\n          type: integer\n        failed_tasks_count:\n          type: integer\n      required:\n        - name\n        - connect\n        - status\n\n    PartitionsIncrease:\n      type: object\n      properties:\n        totalPartitionsCount:\n          type: integer\n          minimum: 1\n      required:\n        - totalPartitionsCount\n\n    PartitionsIncreaseResponse:\n      type: object\n      properties:\n        totalPartitionsCount:\n          type: integer\n        topicName:\n          type: string\n      required:\n        - totalPartitionsCount\n        - topicName\n\n    ReplicationFactorChange:\n      type: object\n      properties:\n        totalReplicationFactor:\n          type: integer\n      required:\n        - totalReplicationFactor\n\n    ReplicationFactorChangeResponse:\n      type: object\n      properties:\n        totalReplicationFactor:\n          type: integer\n        topicName:\n          type: string\n      required:\n        - totalReplicationFactor\n        - topicName\n\n    BrokerConfigItem:\n      type: object\n      properties:\n        value:\n          type: string\n\n    BrokerConfig:\n      type: object\n      properties:\n        name:\n          type: string\n        value:\n          type: string\n        source:\n          $ref: '#/components/schemas/ConfigSource'\n        isSensitive:\n          type: boolean\n        isReadOnly:\n          type: boolean\n        synonyms:\n          type: array\n          items:\n            $ref: '#/components/schemas/ConfigSynonym'\n      required:\n        - name\n        - value\n        - source\n        - isSensitive\n        - isReadOnly\n\n    ConfigSource:\n      type: string\n      enum:\n        - DYNAMIC_TOPIC_CONFIG\n        - DYNAMIC_BROKER_LOGGER_CONFIG\n        - DYNAMIC_BROKER_CONFIG\n        - DYNAMIC_DEFAULT_BROKER_CONFIG\n        - STATIC_BROKER_CONFIG\n        - DEFAULT_CONFIG\n        - UNKNOWN\n\n    ConfigSynonym:\n      type: object\n      properties:\n        name:\n          type: string\n        value:\n          type: string\n        source:\n          $ref: '#/components/schemas/ConfigSource'\n\n    CleanUpPolicy:\n      type: string\n      enum:\n        - DELETE\n        - COMPACT\n        - COMPACT_DELETE\n        - UNKNOWN\n\n    AuthenticationInfo:\n      type: object\n      properties:\n        rbacEnabled:\n          type: boolean\n          description: true if role based access control is enabled and granular permission access is required\n        userInfo:\n          $ref: '#/components/schemas/UserInfo'\n      required:\n        - rbacEnabled\n\n    UserInfo:\n      type: object\n      properties:\n        username:\n          type: string\n        permissions:\n          type: array\n          items:\n            $ref: '#/components/schemas/UserPermission'\n      required:\n        - username\n        - permissions\n\n    UserPermission:\n      type: object\n      properties:\n        clusters:\n          type: array\n          items:\n            type: string\n        resource:\n          $ref: '#/components/schemas/ResourceType'\n        value:\n          type: string\n        actions:\n          type: array\n          items:\n            $ref: '#/components/schemas/Action'\n      required:\n        - clusters\n        - resource\n        - actions\n\n    Action:\n      type: string\n      enum:\n        - VIEW\n        - EDIT\n        - CREATE\n        - DELETE\n        - RESET_OFFSETS\n        - EXECUTE\n        - MODIFY_GLOBAL_COMPATIBILITY\n        - ANALYSIS_VIEW\n        - ANALYSIS_RUN\n        - MESSAGES_READ\n        - MESSAGES_PRODUCE\n        - MESSAGES_DELETE\n        - RESTART\n\n    ResourceType:\n      type: string\n      enum:\n        - APPLICATIONCONFIG\n        - CLUSTERCONFIG\n        - TOPIC\n        - CONSUMER\n        - SCHEMA\n        - CONNECT\n        - KSQL\n        - ACL\n        - AUDIT\n\n    KafkaAcl:\n      type: object\n      required: [resourceType, resourceName, namePatternType, principal, host, operation, permission]\n      properties:\n        resourceType:\n          $ref: '#/components/schemas/KafkaAclResourceType'\n        resourceName:\n          type: string # \"*\" if acl can be applied to any resource of given type\n        namePatternType:\n          $ref: '#/components/schemas/KafkaAclNamePatternType'\n        principal:\n          type: string\n        host:\n          type: string\n        operation:\n          type: string\n          enum:\n            - UNKNOWN # Unknown operation, need to update mapping code on BE\n            - ALL # Cluster, Topic, Group\n            - READ  # Topic, Group\n            - WRITE # Topic, TransactionalId\n            - CREATE # Cluster, Topic\n            - DELETE  # Topic, Group\n            - ALTER  # Cluster, Topic,\n            - DESCRIBE # Cluster, Topic, Group, TransactionalId, DelegationToken\n            - CLUSTER_ACTION # Cluster\n            - DESCRIBE_CONFIGS # Cluster, Topic\n            - ALTER_CONFIGS   # Cluster, Topic\n            - IDEMPOTENT_WRITE # Cluster\n            - CREATE_TOKENS\n            - DESCRIBE_TOKENS\n        permission:\n          type: string\n          enum:\n            - ALLOW\n            - DENY\n\n    CreateConsumerAcl:\n      type: object\n      required: [principal, host]\n      properties:\n        principal:\n          type: string\n        host:\n          type: string\n        topics:\n          type: array\n          items:\n            type: string\n        topicsPrefix:\n          type: string\n        consumerGroups:\n          type: array\n          items:\n            type: string\n        consumerGroupsPrefix:\n          type: string\n\n    CreateProducerAcl:\n      type: object\n      required: [principal, host]\n      properties:\n        principal:\n          type: string\n        host:\n          type: string\n        topics:\n          type: array\n          items:\n            type: string\n        topicsPrefix:\n          type: string\n        transactionalId:\n          type: string\n        transactionsIdPrefix:\n          type: string\n        idempotent:\n          type: boolean\n          default: false\n\n    CreateStreamAppAcl:\n      type: object\n      required: [principal, host, applicationId, inputTopics, outputTopics]\n      properties:\n        principal:\n          type: string\n        host:\n          type: string\n        inputTopics:\n          type: array\n          items:\n            type: string\n        outputTopics:\n          type: array\n          items:\n            type: string\n        applicationId:\n          nullable: false\n          type: string\n\n    KafkaAclResourceType:\n      type: string\n      enum:\n        - UNKNOWN # Unknown operation, need to update mapping code on BE\n        - TOPIC\n        - GROUP\n        - CLUSTER\n        - TRANSACTIONAL_ID\n        - DELEGATION_TOKEN\n        - USER\n\n    KafkaAclNamePatternType:\n      type: string\n      enum:\n        - MATCH\n        - LITERAL\n        - PREFIXED\n\n    RestartRequest:\n      type: object\n      properties:\n        config:\n          $ref: '#/components/schemas/ApplicationConfig'\n\n    UploadedFileInfo:\n      type: object\n      required: [location]\n      properties:\n        location:\n          type: string\n\n    ApplicationConfigValidation:\n      type: object\n      properties:\n        clusters:\n          type: object\n          additionalProperties:\n            $ref: '#/components/schemas/ClusterConfigValidation'\n\n    ApplicationPropertyValidation:\n      type: object\n      required: [error]\n      properties:\n        error:\n          type: boolean\n        errorMessage:\n          type: string\n          description: Contains error message if error = true\n\n    ClusterConfigValidation:\n      type: object\n      required: [kafka]\n      properties:\n        kafka:\n          $ref: '#/components/schemas/ApplicationPropertyValidation'\n        schemaRegistry:\n          $ref: '#/components/schemas/ApplicationPropertyValidation'\n        kafkaConnects:\n          type: object\n          additionalProperties:\n            $ref: '#/components/schemas/ApplicationPropertyValidation'\n        ksqldb:\n          $ref: '#/components/schemas/ApplicationPropertyValidation'\n\n    ApplicationConfig:\n      type: object\n      properties:\n        properties:\n          type: object\n          properties:\n            auth:\n              type: object\n              properties:\n                type:\n                  type: string\n                oauth2:\n                  type: object\n                  properties:\n                    client:\n                      type: object\n                      additionalProperties:\n                        type: object\n                        properties:\n                          provider:\n                            type: string\n                          clientId:\n                            type: string\n                          clientSecret:\n                            type: string\n                          clientName:\n                            type: string\n                          redirectUri:\n                            type: string\n                          authorizationGrantType:\n                            type: string\n                          issuerUri:\n                            type: string\n                          authorizationUri:\n                            type: string\n                          tokenUri:\n                            type: string\n                          userInfoUri:\n                            type: string\n                          jwkSetUri:\n                            type: string\n                          userNameAttribute:\n                            type: string\n                          scope:\n                            type: array\n                            items:\n                              type: string\n                          customParams:\n                            type: object\n                            additionalProperties:\n                              type: string\n            rbac:\n              type: object\n              properties:\n                roles:\n                  type: array\n                  items:\n                    type: object\n                    properties:\n                      name:\n                        type: string\n                      clusters:\n                        type: array\n                        items:\n                          type: string\n                      subjects:\n                        type: array\n                        items:\n                          type: object\n                          properties:\n                            provider:\n                              type: string\n                            type:\n                              type: string\n                            value:\n                              type: string\n                      permissions:\n                        type: array\n                        items:\n                          type: object\n                          properties:\n                            resource:\n                              $ref: '#/components/schemas/ResourceType'\n                            value:\n                              type: string\n                            actions:\n                              type: array\n                              items:\n                                $ref: '#/components/schemas/Action'\n            webclient:\n              type: object\n              properties:\n                maxInMemoryBufferSize:\n                  type: string\n                  description: \"examples: 20, 12KB, 5MB\"\n            kafka:\n              type: object\n              properties:\n                polling:\n                  type: object\n                  properties:\n                    pollTimeoutMs:\n                      type: integer\n                    maxPageSize:\n                      type: integer\n                    defaultPageSize:\n                      type: integer\n                adminClientTimeout:\n                  type: integer\n                internalTopicPrefix:\n                  type: string\n                clusters:\n                  type: array\n                  items:\n                    type: object\n                    properties:\n                      name:\n                        type: string\n                      bootstrapServers:\n                        type: string\n                      ssl:\n                        type: object\n                        properties:\n                          truststoreLocation:\n                            type: string\n                          truststorePassword:\n                            type: string\n                      schemaRegistry:\n                        type: string\n                      schemaRegistryAuth:\n                        type: object\n                        properties:\n                          username:\n                            type: string\n                          password:\n                            type: string\n                      schemaRegistrySsl:\n                        type: object\n                        properties:\n                          keystoreLocation:\n                            type: string\n                          keystorePassword:\n                            type: string\n                      ksqldbServer:\n                        type: string\n                      ksqldbServerSsl:\n                        type: object\n                        properties:\n                          keystoreLocation:\n                            type: string\n                          keystorePassword:\n                              type: string\n                      ksqldbServerAuth:\n                        type: object\n                        properties:\n                          username:\n                            type: string\n                          password:\n                            type: string\n                      kafkaConnect:\n                        type: array\n                        items:\n                          type: object\n                          properties:\n                            name:\n                              type: string\n                            address:\n                              type: string\n                            username:\n                              type: string\n                            password:\n                              type: string\n                            keystoreLocation:\n                              type: string\n                            keystorePassword:\n                              type: string\n\n                      metrics:\n                        type: object\n                        properties:\n                          type:\n                            type: string\n                          port:\n                            type: integer\n                            format: int32\n                          ssl:\n                            type: boolean\n                          username:\n                            type: string\n                          password:\n                            type: string\n                          keystoreLocation:\n                            type: string\n                          keystorePassword:\n                            type: string\n                      properties:\n                        type: object\n                        additionalProperties: true\n                      readOnly:\n                        type: boolean\n                      disableLogDirsCollection:\n                        type: boolean\n                      serde:\n                        type: array\n                        items:\n                          type: object\n                          properties:\n                            name:\n                              type: string\n                            className:\n                              type: string\n                            filePath:\n                              type: string\n                            properties:\n                              type: object\n                              additionalProperties: true\n                            topicKeysPattern:\n                              type: string\n                            topicValuesPattern:\n                              type: string\n                      defaultKeySerde:\n                        type: string\n                      defaultValueSerde:\n                        type: string\n                      masking:\n                        type: array\n                        items:\n                          type: object\n                          properties:\n                            type:\n                              type: string\n                              enum:\n                                - REMOVE\n                                - MASK\n                                - REPLACE\n                            fields:\n                              type: array\n                              items:\n                                type: string\n                            fieldsNamePattern:\n                              type: string\n                            maskingCharsReplacement:\n                              type: array\n                              items:\n                                type: string\n                            replacement:\n                              type: string\n                            topicKeysPattern:\n                              type: string\n                            topicValuesPattern:\n                              type: string\n                      pollingThrottleRate:\n                        type: integer\n                        format: int64\n                      audit:\n                        type: object\n                        properties:\n                          level:\n                            type: string\n                            enum: [ \"ALL\", \"ALTER_ONLY\" ]\n                          topic:\n                            type: string\n                          auditTopicsPartitions:\n                            type: integer\n                          topicAuditEnabled:\n                            type: boolean\n                          consoleAuditEnabled:\n                            type: boolean\n                          auditTopicProperties:\n                            type: object\n                            additionalProperties:\n                              type: string\n"
  },
  {
    "path": "kafka-ui-e2e-checks/.gitignore",
    "content": ".env\nbuild/\nallure-results/\nselenoid/video/\ntarget/\nselenoid/logs/\n"
  },
  {
    "path": "kafka-ui-e2e-checks/QASE.md",
    "content": "### E2E integration with Qase.io TMS (for internal users)\n\n### Table of Contents\n\n- [Intro](#intro)\n- [Set up Qase.io integration](#set-up-qase-integration)\n- [Test case creation](#test-case-creation)\n- [Test run reporting](#test-run-reporting)\n\n### Intro\n\nWe're using [Qase.io](https://help.qase.io/en/) as TMS to keep test cases and accumulate test runs.\nIntegration is set up through API using [qase-api](https://mvnrepository.com/artifact/io.qase/qase-api)\nand [qase-testng](https://mvnrepository.com/artifact/io.qase/qase-testng) libraries.\n\n### Set up Qase integration\n\nTo set up integration locally add next VM option `-DQASEIO_API_TOKEN='%s'`\n(add your [Qase token](https://app.qase.io/user/api/token) instead of '%s') into your run configuration\n\n### Test case creation\n\nAll new test cases can be added into TMS by default if they have no QaseId and QaseTitle matching already existing\ncases.\nBut to handle `@Suite` and `@Automation` we added custom QaseCreateListener. To create new test case for next sync with\nQase (see example `kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/Template.java`):\n\n1. Create new class in `kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/suit`\n2. Inherit it from `kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/BaseQaseTest.java`\n3. Create new test method with some name inside the class and annotate it with:\n\n- `@Automation` (optional - Not automated by default) - to set one of automation states: NOT_AUTOMATED, TO_BE_AUTOMATED,\n  AUTOMATED\n- `@QaseTitle` (required) - to set title for new test case and to check is there no existing cases with same title in\n  Qase.io\n- `@Status` (optional - Draft by default) - to set one of case statuses: ACTUAL, DRAFT, DEPRECATED\n- `@Suite` (optional) - to store new case in some existing package need to set its id, otherwise case will be stored in\n  the root\n- `@Test` (required) - annotation from TestNG to specify this method as test\n\n4. Create new private void step methods with some name inside the same class and annotate it with\n   @io.qase.api.annotation.Step to specify this method as step.\n5. Use defined step methods inside created test method in concrete order\n6. If there are any additional cases to create you can repeat scenario in a new class\n7. There are two ways to sync newly created cases in the framework with Qase.io:\n\n- sync can be performed locally - run new test classes with\n  already [set up Qase.io integration](#Set up Qase.io integration)\n- also you can commit and push your changes, then\n  run [E2E Manual suite](https://github.com/provectus/kafka-ui/actions/workflows/e2e-manual.yml) on your branch\n\n8. No test run in Qase.io will be created, new test case will be stored defined directory\n   in [project's repository](https://app.qase.io/project/KAFKAUI)\n9. To add expected results into created test case edit in Qase.io manually\n\n### Test run reporting\n\nTo handle manual test cases with status `Skipped` we added custom QaseResultListener. To create new test run:\n\n1. All test methods should be annotated with actual `@QaseId`\n2. There are two ways to sync newly created cases in the framework with Qase.io:\n\n- run can be performed locally - run test classes (or suites) with\n  already [set up Qase.io integration](#Set up Qase.io integration), they will be labeled as `Automation CUSTOM suite`\n- also you can commit and push your changes, then\n  run [E2E Automation suite](https://github.com/provectus/kafka-ui/actions/workflows/e2e-automation.yml) on your branch\n\n3. All new test runs will be added into [project's test runs](https://app.qase.io/run/KAFKAUI) with corresponding label\n   using QaseId to identify existing cases\n4. All test cases from manual suite are set up to have `Skipped` status in test runs to perform them manually\n"
  },
  {
    "path": "kafka-ui-e2e-checks/README.md",
    "content": "### E2E UI automation for Kafka-ui\n\nThis repository is for E2E UI automation.\n\n### Table of Contents\n\n- [Prerequisites](#prerequisites)\n- [How to install](#how-to-install)\n- [How to run checks](#how-to-run-checks)\n- [Qase.io integration (for internal users)](#qase-integration)\n- [Reporting](#reporting)\n- [Environments setup](#environments-setup)\n- [Test Data](#test-data)\n- [Actions](#actions)\n- [Checks](#checks)\n- [Parallelization](#parallelization)\n- [How to develop](#how-to-develop)\n\n### Prerequisites\n\n- Docker & Docker-compose\n- Java (install aarch64 jdk if you have M1/arm chip)\n- Maven\n\n### How to install\n\n```\ngit clone https://github.com/provectus/kafka-ui.git\ncd  kafka-ui-e2e-checks\ndocker pull selenoid/vnc_chrome:103.0 \n```\n\n### How to run checks\n\n1. Run `kafka-ui`:\n\n```\ncd kafka-ui\ndocker-compose -f kafka-ui-e2e-checks/docker/selenoid-local.yaml up -d\ndocker-compose -f documentation/compose/e2e-tests.yaml up -d\n```\n\n2. To run test suite select its name (options: regression, sanity, smoke) and put it instead %s into command below\n\n```\n./mvnw -Dsurefire.suiteXmlFiles='src/test/resources/%s.xml' -f 'kafka-ui-e2e-checks' test -Pprod\n```\n\n3. To run tests on your local Chrome browser just add next VM option to the Run Configuration\n\n```\n-Dbrowser=local\n```\n\nExpected Location of Chrome\n```\nLinux:\t                    /usr/bin/google-chrome1\nMac:\t                    /Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome\nWindows XP:                 %HOMEPATH%\\Local Settings\\Application Data\\Google\\Chrome\\Application\\chrome.exe\nWindows Vista and newer:    C:\\Users%USERNAME%\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe\n```\n\n### Qase integration\n\nFound instruction for Qase.io integration (for internal use only) at `kafka-ui-e2e-checks/QASE.md`\n\n### Reporting\n\nReports are in `allure-results` folder.\nIf you have installed allure commandline [here](https://www.npmjs.com/package/allure-commandline))\nYou can see allure report with command:\n\n```\nallure serve\n```\n\n### Screenshots\n\nReference screenshots are in `SCREENSHOTS_FOLDER`  (default,`kafka-ui-e2e-checks/screenshots`)\n\n### How to develop\n\n> ⚠️ todo\n\n### Setting for different environments\n\n> ⚠️ todo\n\n### Test Data\n\n> ⚠️ todo\n\n### Actions\n\n> ⚠️ todo\n\n### Checks\n\n> ⚠️ todo\n\n### Parallelization\n\n> ⚠️ todo\n\n### Tips\n\n- install `Selenium UI Testing plugin` in IDEA\n\n"
  },
  {
    "path": "kafka-ui-e2e-checks/docker/selenoid-git.yaml",
    "content": "---\nversion: '3'\n\nservices:\n\n  selenoid:\n    network_mode: bridge\n    image: aerokube/selenoid:1.10.7\n    volumes:\n      - \"../selenoid/config:/etc/selenoid\"\n      - \"/var/run/docker.sock:/var/run/docker.sock\"\n      - \"../selenoid/video:/opt/selenoid/video\"\n      - \"../selenoid/logs:/opt/selenoid/logs\"\n    environment:\n      - OVERRIDE_VIDEO_OUTPUT_DIR=../selenoid/video\n    command: [ \"-conf\", \"/etc/selenoid/browsersGit.json\", \"-video-output-dir\", \"/opt/selenoid/video\", \"-log-output-dir\", \"/opt/selenoid/logs\" ]\n    ports:\n      - \"4444:4444\"\n\n  selenoid-ui:\n    network_mode: bridge\n    image: aerokube/selenoid-ui:latest-release\n    links:\n      - selenoid\n    ports:\n      - \"8081:8080\"\n    command: [ \"--selenoid-uri\", \"http://selenoid:4444\" ]\n\n  selenoid-chrome:\n    network_mode: bridge\n    image: selenoid/vnc_chrome:103.0\n    extra_hosts:\n      - \"host.docker.internal:host-gateway\"\n"
  },
  {
    "path": "kafka-ui-e2e-checks/docker/selenoid-local.yaml",
    "content": "---\nversion: '3'\n\nservices:\n\n  selenoid:\n    network_mode: bridge\n    image: aerokube/selenoid:1.10.7\n    volumes:\n      - \"../selenoid/config:/etc/selenoid\"\n      - \"/var/run/docker.sock:/var/run/docker.sock\"\n      - \"../selenoid/video:/opt/selenoid/video\"\n      - \"../selenoid/logs:/opt/selenoid/logs\"\n    environment:\n      - OVERRIDE_VIDEO_OUTPUT_DIR=../selenoid/video\n    command: [ \"-conf\", \"/etc/selenoid/browsersLocal.json\", \"-video-output-dir\", \"/opt/selenoid/video\", \"-log-output-dir\", \"/opt/selenoid/logs\" ]\n    ports:\n      - \"4444:4444\"\n\n  selenoid-ui:\n    network_mode: bridge\n    image: aerokube/selenoid-ui:latest-release\n    links:\n      - selenoid\n    ports:\n      - \"8081:8080\"\n    command: [ \"--selenoid-uri\", \"http://selenoid:4444\" ]\n\n  selenoid-chrome:\n    network_mode: bridge\n    image: selenoid/vnc_chrome:103.0\n    extra_hosts:\n      - \"host.docker.internal:host-gateway\"\n"
  },
  {
    "path": "kafka-ui-e2e-checks/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>kafka-ui</artifactId>\n        <groupId>com.provectus</groupId>\n        <version>0.0.1-SNAPSHOT</version>\n    </parent>\n\n    <modelVersion>4.0.0</modelVersion>\n    <artifactId>kafka-ui-e2e-checks</artifactId>\n\n    <properties>\n        <maven.surefire-plugin.version>3.0.0-M8</maven.surefire-plugin.version>\n        <kafka-ui-contract>${project.version}</kafka-ui-contract>\n        <testcontainers.version>1.17.6</testcontainers.version>\n        <httpcomponents.version>5.2.1</httpcomponents.version>\n        <selenium.version>4.8.1</selenium.version>\n        <selenide.version>6.12.3</selenide.version>\n        <testng.version>7.7.1</testng.version>\n        <allure.version>2.23.0</allure.version>\n        <qase.io.version>3.0.5</qase.io.version>\n        <aspectj.version>1.9.9.1</aspectj.version>\n        <assertj.version>3.24.2</assertj.version>\n        <hamcrest.version>2.2</hamcrest.version>\n        <slf4j.version>2.0.7</slf4j.version>\n        <kafka.version>3.3.1</kafka.version>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.apache.kafka</groupId>\n            <artifactId>kafka_2.13</artifactId>\n            <version>${kafka.version}</version>\n            <exclusions> <!-- could be removed when kafka version will contain zookeeper with netty 4.1.69 -->\n                <exclusion>\n                    <groupId>io.netty</groupId>\n                    <artifactId>netty-buffer</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>io.netty</groupId>\n                    <artifactId>netty-common</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>io.netty</groupId>\n                    <artifactId>netty-codec</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>io.netty</groupId>\n                    <artifactId>netty-handler</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>io.netty</groupId>\n                    <artifactId>netty-resolver</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>io.netty</groupId>\n                    <artifactId>netty-transport</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>io.netty</groupId>\n                    <artifactId>netty-transport-native-epoll</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>io.netty</groupId>\n                    <artifactId>netty-transport-native-unix-common</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n\n        <!--\n        whole netty dependency block could be removed\n        when kafka version will contain zookeeper with netty 4.1.69\n        -->\n        <dependency>\n            <groupId>io.netty</groupId>\n            <artifactId>netty-buffer</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.netty</groupId>\n            <artifactId>netty-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.netty</groupId>\n            <artifactId>netty-codec</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.netty</groupId>\n            <artifactId>netty-handler</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.netty</groupId>\n            <artifactId>netty-resolver</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.netty</groupId>\n            <artifactId>netty-transport</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.netty</groupId>\n            <artifactId>netty-transport-native-epoll</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.netty</groupId>\n            <artifactId>netty-transport-native-unix-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.netty</groupId>\n            <artifactId>netty-resolver-dns-native-macos</artifactId>\n            <classifier>osx-aarch_64</classifier>\n        </dependency>\n\n        <dependency>\n            <groupId>org.testcontainers</groupId>\n            <artifactId>testcontainers</artifactId>\n            <version>${testcontainers.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.testcontainers</groupId>\n            <artifactId>selenium</artifactId>\n            <version>${testcontainers.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <version>${org.projectlombok.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.httpcomponents.core5</groupId>\n            <artifactId>httpcore5</artifactId>\n            <version>${httpcomponents.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.httpcomponents.client5</groupId>\n            <artifactId>httpclient5</artifactId>\n            <version>${httpcomponents.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.seleniumhq.selenium</groupId>\n            <artifactId>selenium-http-jdk-client</artifactId>\n            <version>${selenium.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.seleniumhq.selenium</groupId>\n            <artifactId>selenium-http</artifactId>\n            <version>${selenium.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>com.codeborne</groupId>\n            <artifactId>selenide</artifactId>\n            <version>${selenide.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.testng</groupId>\n            <artifactId>testng</artifactId>\n            <version>${testng.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>io.qameta.allure</groupId>\n            <artifactId>allure-selenide</artifactId>\n            <version>${allure.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>io.qameta.allure</groupId>\n            <artifactId>allure-testng</artifactId>\n            <version>${allure.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>io.qase</groupId>\n            <artifactId>qase-testng</artifactId>\n            <version>${qase.io.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>io.qase</groupId>\n            <artifactId>qase-api</artifactId>\n            <version>${qase.io.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.hamcrest</groupId>\n            <artifactId>hamcrest</artifactId>\n            <version>${hamcrest.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.assertj</groupId>\n            <artifactId>assertj-core</artifactId>\n            <version>${assertj.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.aspectj</groupId>\n            <artifactId>aspectjrt</artifactId>\n            <version>${aspectj.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-simple</artifactId>\n            <version>${slf4j.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>com.provectus</groupId>\n            <artifactId>kafka-ui-contract</artifactId>\n            <version>${kafka-ui-contract}</version>\n        </dependency>\n    </dependencies>\n\n    <profiles>\n        <profile>\n            <id>local</id>\n            <!-- Disabling e2e tests by default (for local dev envs) since complex setup is needed for UI tests -->\n            <activation>\n                <activeByDefault>true</activeByDefault>\n            </activation>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-surefire-plugin</artifactId>\n                        <configuration>\n                            <skipTests>true</skipTests>\n                        </configuration>\n                        <dependencies>\n                            <dependency>\n                                <groupId>org.apache.maven.surefire</groupId>\n                                <artifactId>surefire-testng</artifactId>\n                                <version>${maven.surefire-plugin.version}</version>\n                            </dependency>\n                        </dependencies>\n                    </plugin>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-compiler-plugin</artifactId>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n        <profile>\n            <id>prod</id>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-surefire-plugin</artifactId>\n                        <version>${maven.surefire-plugin.version}</version>\n                        <configuration>\n                            <argLine>\n                                -javaagent:\"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar\"\n                            </argLine>\n                        </configuration>\n                        <dependencies>\n                            <dependency>\n                                <groupId>org.apache.maven.surefire</groupId>\n                                <artifactId>surefire-testng</artifactId>\n                                <version>${maven.surefire-plugin.version}</version>\n                            </dependency>\n                            <dependency>\n                                <groupId>org.aspectj</groupId>\n                                <artifactId>aspectjweaver</artifactId>\n                                <version>${aspectj.version}</version>\n                            </dependency>\n                        </dependencies>\n                    </plugin>\n                    <plugin>\n                        <groupId>io.qameta.allure</groupId>\n                        <artifactId>allure-maven</artifactId>\n                        <version>2.10.0</version>\n                    </plugin>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-checkstyle-plugin</artifactId>\n                        <version>3.3.0</version>\n                        <dependencies>\n                            <dependency>\n                                <groupId>com.puppycrawl.tools</groupId>\n                                <artifactId>checkstyle</artifactId>\n                                <version>10.3.1</version>\n                            </dependency>\n                        </dependencies>\n                        <executions>\n                            <execution>\n                                <id>checkstyle</id>\n                                <phase>validate</phase>\n                                <goals>\n                                    <goal>check</goal>\n                                </goals>\n                                <configuration>\n                                    <violationSeverity>warning</violationSeverity>\n                                    <failOnViolation>true</failOnViolation>\n                                    <failsOnError>true</failsOnError>\n                                    <includeTestSourceDirectory>true</includeTestSourceDirectory>\n                                    <configLocation>file:${basedir}/../etc/checkstyle/checkstyle-e2e.xml</configLocation>\n                                    <headerLocation>file:${basedir}/../etc/checkstyle/apache-header.txt</headerLocation>\n                                </configuration>\n                            </execution>\n                        </executions>\n\n                    </plugin>\n\n                </plugins>\n            </build>\n        </profile>\n    </profiles>\n</project>\n"
  },
  {
    "path": "kafka-ui-e2e-checks/selenoid/config/browsersGit.json",
    "content": "{\n  \"chrome\": {\n    \"default\": \"103.0\",\n    \"versions\": {\n      \"103.0\": {\n        \"image\": \"selenoid/vnc_chrome:103.0\",\n        \"hosts\": [\n          \"host.docker.internal:172.17.0.1\"\n        ],\n        \"port\": \"4444\",\n        \"path\": \"/\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/selenoid/config/browsersLocal.json",
    "content": "{\n  \"chrome\": {\n    \"default\": \"103.0\",\n    \"versions\": {\n      \"103.0\": {\n        \"image\": \"selenoid/vnc_chrome:103.0\",\n        \"port\": \"4444\",\n        \"path\": \"/\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Connector.java",
    "content": "package com.provectus.kafka.ui.models;\n\nimport lombok.Data;\nimport lombok.experimental.Accessors;\n\n@Data\n@Accessors(chain = true)\npublic class Connector {\n\n  private String name, config;\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Schema.java",
    "content": "package com.provectus.kafka.ui.models;\n\nimport static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;\n\nimport com.provectus.kafka.ui.api.model.SchemaType;\nimport lombok.Data;\nimport lombok.experimental.Accessors;\n\n@Data\n@Accessors(chain = true)\npublic class Schema {\n\n  private static final String USER_DIR = \"user.dir\";\n\n  private String name, valuePath;\n  private SchemaType type;\n\n  public static Schema createSchemaAvro() {\n    return new Schema().setName(\"schema_avro-\" + randomAlphabetic(5))\n        .setType(SchemaType.AVRO)\n        .setValuePath(System.getProperty(USER_DIR) + \"/src/main/resources/testData/schemas/schema_avro_value.json\");\n  }\n\n  public static Schema createSchemaJson() {\n    return new Schema().setName(\"schema_json-\" + randomAlphabetic(5))\n        .setType(SchemaType.JSON)\n        .setValuePath(System.getProperty(USER_DIR) + \"/src/main/resources/testData/schemas/schema_json_Value.json\");\n  }\n\n  public static Schema createSchemaProtobuf() {\n    return new Schema().setName(\"schema_protobuf-\" + randomAlphabetic(5))\n        .setType(SchemaType.PROTOBUF)\n        .setValuePath(\n            System.getProperty(USER_DIR) + \"/src/main/resources/testData/schemas/schema_protobuf_value.txt\");\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Topic.java",
    "content": "package com.provectus.kafka.ui.models;\n\nimport com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue;\nimport com.provectus.kafka.ui.pages.topics.enums.CustomParameterType;\nimport com.provectus.kafka.ui.pages.topics.enums.MaxSizeOnDisk;\nimport com.provectus.kafka.ui.pages.topics.enums.TimeToRetain;\nimport lombok.Data;\nimport lombok.experimental.Accessors;\n\n@Data\n@Accessors(chain = true)\npublic class Topic {\n\n  private String name, timeToRetainData, maxMessageBytes, messageKey, messageValue, customParameterValue;\n  private int numberOfPartitions;\n  private CustomParameterType customParameterType;\n  private CleanupPolicyValue cleanupPolicyValue;\n  private MaxSizeOnDisk maxSizeOnDisk;\n  private TimeToRetain timeToRetain;\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/BasePage.java",
    "content": "package com.provectus.kafka.ui.pages;\n\nimport static com.codeborne.selenide.Selenide.$$x;\nimport static com.codeborne.selenide.Selenide.$x;\n\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.ElementsCollection;\nimport com.codeborne.selenide.SelenideElement;\nimport com.codeborne.selenide.WebDriverRunner;\nimport com.provectus.kafka.ui.pages.panels.enums.MenuItem;\nimport com.provectus.kafka.ui.utilities.WebUtils;\nimport java.time.Duration;\nimport lombok.extern.slf4j.Slf4j;\nimport org.openqa.selenium.Keys;\nimport org.openqa.selenium.interactions.Actions;\n\n@Slf4j\npublic abstract class BasePage extends WebUtils {\n\n  protected SelenideElement loadingSpinner = $x(\"//div[@role='progressbar']\");\n  protected SelenideElement submitBtn = $x(\"//button[@type='submit']\");\n  protected SelenideElement tableGrid = $x(\"//table\");\n  protected SelenideElement searchFld = $x(\"//input[@type='text'][contains(@id, ':r')]\");\n  protected SelenideElement dotMenuBtn = $x(\"//button[@aria-label='Dropdown Toggle']\");\n  protected SelenideElement alertHeader = $x(\"//div[@role='alert']//div[@role='heading']\");\n  protected SelenideElement alertMessage = $x(\"//div[@role='alert']//div[@role='contentinfo']\");\n  protected SelenideElement confirmationMdl = $x(\"//div[text()= 'Confirm the action']/..\");\n  protected SelenideElement confirmBtn = $x(\"//button[contains(text(),'Confirm')]\");\n  protected SelenideElement cancelBtn = $x(\"//button[contains(text(),'Cancel')]\");\n  protected SelenideElement backBtn = $x(\"//button[contains(text(),'Back')]\");\n  protected SelenideElement previousBtn = $x(\"//button[contains(text(),'Previous')]\");\n  protected SelenideElement nextBtn = $x(\"//button[contains(text(),'Next')]\");\n  protected ElementsCollection ddlOptions = $$x(\"//li[@value]\");\n  protected ElementsCollection gridItems = $$x(\"//tr[@class]\");\n  protected String summaryCellLocator = \"//div[contains(text(),'%s')]\";\n  protected String tableElementNameLocator = \"//tbody//a[contains(text(),'%s')]\";\n  protected String columnHeaderLocator = \"//table//tr/th//div[text()='%s']\";\n  protected String pageTitleFromHeader = \"//h1[text()='%s']\";\n  protected String pagePathFromHeader = \"//a[text()='%s']/../h1\";\n\n  protected boolean isSpinnerVisible(int... timeoutInSeconds) {\n    return isVisible(loadingSpinner, timeoutInSeconds);\n  }\n\n  protected void waitUntilSpinnerDisappear(int... timeoutInSeconds) {\n    log.debug(\"\\nwaitUntilSpinnerDisappear\");\n    if (isSpinnerVisible(timeoutInSeconds)) {\n      loadingSpinner.shouldBe(Condition.disappear, Duration.ofSeconds(60));\n    }\n  }\n\n  protected void searchItem(String tag) {\n    log.debug(\"\\nsearchItem: {}\", tag);\n    sendKeysAfterClear(searchFld, tag);\n    searchFld.pressEnter().shouldHave(Condition.value(tag));\n    waitUntilSpinnerDisappear(1);\n  }\n\n  protected SelenideElement getPageTitleFromHeader(MenuItem menuItem) {\n    return $x(String.format(pageTitleFromHeader, menuItem.getPageTitle()));\n  }\n\n  protected SelenideElement getPagePathFromHeader(MenuItem menuItem) {\n    return $x(String.format(pagePathFromHeader, menuItem.getPageTitle()));\n  }\n\n  protected void clickSubmitBtn() {\n    clickByJavaScript(submitBtn);\n  }\n\n  protected void clickNextBtn() {\n    clickByJavaScript(nextBtn);\n  }\n\n  protected void clickBackBtn() {\n    clickByJavaScript(backBtn);\n  }\n\n  protected void clickPreviousBtn() {\n    clickByJavaScript(previousBtn);\n  }\n\n  protected void setJsonInputValue(SelenideElement jsonInput, String jsonConfig) {\n    sendKeysByActions(jsonInput, jsonConfig.replace(\"  \", \"\"));\n    new Actions(WebDriverRunner.getWebDriver())\n        .keyDown(Keys.SHIFT)\n        .sendKeys(Keys.PAGE_DOWN)\n        .keyUp(Keys.SHIFT)\n        .sendKeys(Keys.DELETE)\n        .perform();\n  }\n\n  protected SelenideElement getTableElement(String elementName) {\n    log.debug(\"\\ngetTableElement: {}\", elementName);\n    return $x(String.format(tableElementNameLocator, elementName));\n  }\n\n  protected ElementsCollection getDdlOptions() {\n    return ddlOptions;\n  }\n\n  protected String getAlertHeader() {\n    log.debug(\"\\ngetAlertHeader\");\n    String result = alertHeader.shouldBe(Condition.visible).getText();\n    log.debug(\"-> {}\", result);\n    return result;\n  }\n\n  protected String getAlertMessage() {\n    log.debug(\"\\ngetAlertMessage\");\n    String result = alertMessage.shouldBe(Condition.visible).getText();\n    log.debug(\"-> {}\", result);\n    return result;\n  }\n\n  protected boolean isAlertVisible(AlertHeader header) {\n    log.debug(\"\\nisAlertVisible: {}\", header.toString());\n    boolean result = getAlertHeader().equals(header.toString());\n    log.debug(\"-> {}\", result);\n    return result;\n  }\n\n  protected boolean isAlertVisible(AlertHeader header, String message) {\n    log.debug(\"\\nisAlertVisible: {} {}\", header, message);\n    boolean result = isAlertVisible(header) && getAlertMessage().equals(message);\n    log.debug(\"-> {}\", result);\n    return result;\n  }\n\n  protected void clickConfirmButton() {\n    confirmBtn.shouldBe(Condition.enabled).click();\n    confirmBtn.shouldBe(Condition.disappear);\n  }\n\n  protected void clickCancelButton() {\n    cancelBtn.shouldBe(Condition.enabled).click();\n    cancelBtn.shouldBe(Condition.disappear);\n  }\n\n  protected boolean isConfirmationModalVisible() {\n    return isVisible(confirmationMdl);\n  }\n\n  public enum AlertHeader {\n    SUCCESS(\"Success\"),\n    VALIDATION_ERROR(\"Validation Error\"),\n    BAD_REQUEST(\"400 Bad Request\");\n\n    private final String value;\n\n    AlertHeader(String value) {\n      this.value = value;\n    }\n\n    public String toString() {\n      return value;\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersConfigTab.java",
    "content": "package com.provectus.kafka.ui.pages.brokers;\n\nimport static com.codeborne.selenide.Selenide.$$x;\nimport static com.codeborne.selenide.Selenide.$x;\n\nimport com.codeborne.selenide.CollectionCondition;\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.ElementsCollection;\nimport com.codeborne.selenide.SelenideElement;\nimport com.provectus.kafka.ui.pages.BasePage;\nimport io.qameta.allure.Step;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class BrokersConfigTab extends BasePage {\n\n  protected List<SelenideElement> editBtn = $$x(\"//button[@aria-label='editAction']\");\n  protected SelenideElement searchByKeyField = $x(\"//input[@placeholder='Search by Key or Value']\");\n  protected SelenideElement sourceInfoIcon = $x(\"//div[text()='Source']/..//div/div[@class]\");\n  protected SelenideElement sourceInfoTooltip = $x(\"//div[text()='Source']/..//div/div[@style]\");\n  protected ElementsCollection editBtns = $$x(\"//button[@aria-label='editAction']\");\n\n  @Step\n  public BrokersConfigTab waitUntilScreenReady() {\n    waitUntilSpinnerDisappear();\n    searchFld.shouldBe(Condition.visible);\n    return this;\n  }\n\n  @Step\n  public BrokersConfigTab hoverOnSourceInfoIcon() {\n    sourceInfoIcon.shouldBe(Condition.visible).hover();\n    return this;\n  }\n\n  @Step\n  public String getSourceInfoTooltipText() {\n    return sourceInfoTooltip.shouldBe(Condition.visible).getText().trim();\n  }\n\n  @Step\n  public boolean isSearchByKeyVisible() {\n    return isVisible(searchFld);\n  }\n\n  @Step\n  public BrokersConfigTab searchConfig(String key) {\n    searchItem(key);\n    return this;\n  }\n\n  public List<SelenideElement> getColumnHeaders() {\n    return Stream.of(\"Key\", \"Value\", \"Source\")\n        .map(name -> $x(String.format(columnHeaderLocator, name)))\n        .collect(Collectors.toList());\n  }\n\n  public List<SelenideElement> getEditButtons() {\n    return editBtns;\n  }\n\n  @Step\n  public BrokersConfigTab clickNextButton() {\n    clickNextBtn();\n    waitUntilSpinnerDisappear(1);\n    return this;\n  }\n\n  @Step\n  public BrokersConfigTab clickPreviousButton() {\n    clickPreviousBtn();\n    waitUntilSpinnerDisappear(1);\n    return this;\n  }\n\n  private List<BrokersConfigTab.BrokersConfigItem> initGridItems() {\n    List<BrokersConfigTab.BrokersConfigItem> gridItemList = new ArrayList<>();\n    gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0))\n        .forEach(item -> gridItemList.add(new BrokersConfigTab.BrokersConfigItem(item)));\n    return gridItemList;\n  }\n\n  @Step\n  public BrokersConfigTab.BrokersConfigItem getConfig(String key) {\n    return initGridItems().stream()\n        .filter(e -> e.getKey().equals(key))\n        .findFirst().orElseThrow();\n  }\n\n  @Step\n  public List<BrokersConfigTab.BrokersConfigItem> getAllConfigs() {\n    return initGridItems();\n  }\n\n  public static class BrokersConfigItem extends BasePage {\n\n    private final SelenideElement element;\n\n    public BrokersConfigItem(SelenideElement element) {\n      this.element = element;\n    }\n\n    @Step\n    public String getKey() {\n      return element.$x(\"./td[1]\").getText().trim();\n    }\n\n    @Step\n    public String getValue() {\n      return element.$x(\"./td[2]//span\").getText().trim();\n    }\n\n    @Step\n    public BrokersConfigItem setValue(String value) {\n      sendKeysAfterClear(getValueFld(), value);\n      return this;\n    }\n\n    @Step\n    public SelenideElement getValueFld() {\n      return element.$x(\"./td[2]//input\");\n    }\n\n    @Step\n    public SelenideElement getSaveBtn() {\n      return element.$x(\"./td[2]//button[@aria-label='confirmAction']\");\n    }\n\n    @Step\n    public SelenideElement getCancelBtn() {\n      return element.$x(\"./td[2]//button[@aria-label='cancelAction']\");\n    }\n\n    @Step\n    public SelenideElement getEditBtn() {\n      return element.$x(\"./td[2]//button[@aria-label='editAction']\");\n    }\n\n    @Step\n    public BrokersConfigItem clickSaveBtn() {\n      getSaveBtn().shouldBe(Condition.enabled).click();\n      return this;\n    }\n\n    @Step\n    public BrokersConfigItem clickCancelBtn() {\n      getCancelBtn().shouldBe(Condition.enabled).click();\n      return this;\n    }\n\n    @Step\n    public BrokersConfigItem clickEditBtn() {\n      getEditBtn().shouldBe(Condition.enabled).click();\n      return this;\n    }\n\n    @Step\n    public String getSource() {\n      return element.$x(\"./td[3]\").getText().trim();\n    }\n\n    @Step\n    public BrokersConfigItem clickConfirm() {\n      clickConfirmButton();\n      return this;\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersDetails.java",
    "content": "package com.provectus.kafka.ui.pages.brokers;\n\nimport static com.codeborne.selenide.Selenide.$x;\n\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.SelenideElement;\nimport com.provectus.kafka.ui.pages.BasePage;\nimport io.qameta.allure.Step;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class BrokersDetails extends BasePage {\n\n  protected String brokersTabLocator = \"//a[text()='%s']\";\n\n  @Step\n  public BrokersDetails waitUntilScreenReady() {\n    waitUntilSpinnerDisappear();\n    $x(String.format(brokersTabLocator, DetailsTab.LOG_DIRECTORIES)).shouldBe(Condition.visible);\n    return this;\n  }\n\n  @Step\n  public BrokersDetails openDetailsTab(DetailsTab menu) {\n    $x(String.format(brokersTabLocator, menu.toString())).shouldBe(Condition.enabled).click();\n    waitUntilSpinnerDisappear();\n    return this;\n  }\n\n  private List<SelenideElement> getVisibleColumnHeaders() {\n    return Stream.of(\"Name\", \"Topics\", \"Error\", \"Partitions\")\n        .map(name -> $x(String.format(columnHeaderLocator, name)))\n        .collect(Collectors.toList());\n  }\n\n  private List<SelenideElement> getEnabledColumnHeaders() {\n    return Stream.of(\"Name\", \"Error\")\n        .map(name -> $x(String.format(columnHeaderLocator, name)))\n        .collect(Collectors.toList());\n  }\n\n  private List<SelenideElement> getVisibleSummaryCells() {\n    return Stream.of(\"Segment Size\", \"Segment Count\", \"Port\", \"Host\")\n        .map(name -> $x(String.format(summaryCellLocator, name)))\n        .collect(Collectors.toList());\n  }\n\n  private List<SelenideElement> getDetailsTabs() {\n    return Stream.of(DetailsTab.values())\n        .map(name -> $x(String.format(brokersTabLocator, name)))\n        .collect(Collectors.toList());\n  }\n\n  @Step\n  public List<SelenideElement> getAllEnabledElements() {\n    List<SelenideElement> enabledElements = new ArrayList<>(getEnabledColumnHeaders());\n    enabledElements.addAll(getDetailsTabs());\n    return enabledElements;\n  }\n\n  @Step\n  public List<SelenideElement> getAllVisibleElements() {\n    List<SelenideElement> visibleElements = new ArrayList<>(getVisibleSummaryCells());\n    visibleElements.addAll(getVisibleColumnHeaders());\n    visibleElements.addAll(getDetailsTabs());\n    return visibleElements;\n  }\n\n  public enum DetailsTab {\n    LOG_DIRECTORIES(\"Log directories\"),\n    CONFIGS(\"Configs\"),\n    METRICS(\"Metrics\");\n\n    private final String value;\n\n    DetailsTab(String value) {\n      this.value = value;\n    }\n\n    public String toString() {\n      return value;\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersList.java",
    "content": "package com.provectus.kafka.ui.pages.brokers;\n\nimport static com.codeborne.selenide.Selenide.$x;\nimport static com.provectus.kafka.ui.pages.panels.enums.MenuItem.BROKERS;\n\nimport com.codeborne.selenide.CollectionCondition;\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.SelenideElement;\nimport com.provectus.kafka.ui.pages.BasePage;\nimport io.qameta.allure.Step;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class BrokersList extends BasePage {\n\n  @Step\n  public BrokersList waitUntilScreenReady() {\n    waitUntilSpinnerDisappear();\n    getPageTitleFromHeader(BROKERS).shouldBe(Condition.visible);\n    return this;\n  }\n\n  @Step\n  public BrokersList openBroker(int brokerId) {\n    getBroker(brokerId).openItem();\n    return this;\n  }\n\n  private List<SelenideElement> getUptimeSummaryCells() {\n    return Stream.of(\"Broker Count\", \"Active Controller\", \"Version\")\n        .map(name -> $x(String.format(summaryCellLocator, name)))\n        .collect(Collectors.toList());\n  }\n\n  private List<SelenideElement> getPartitionsSummaryCells() {\n    return Stream.of(\"Online\", \"URP\", \"In Sync Replicas\", \"Out Of Sync Replicas\")\n        .map(name -> $x(String.format(summaryCellLocator, name)))\n        .collect(Collectors.toList());\n  }\n\n  @Step\n  public List<SelenideElement> getAllVisibleElements() {\n    List<SelenideElement> visibleElements = new ArrayList<>(getUptimeSummaryCells());\n    visibleElements.addAll(getPartitionsSummaryCells());\n    return visibleElements;\n  }\n\n  private List<SelenideElement> getEnabledColumnHeaders() {\n    return Stream.of(\"Broker ID\", \"Disk usage\", \"Partitions skew\",\n            \"Leaders\", \"Leader skew\", \"Online partitions\", \"Port\", \"Host\")\n        .map(name -> $x(String.format(columnHeaderLocator, name)))\n        .collect(Collectors.toList());\n  }\n\n  @Step\n  public List<SelenideElement> getAllEnabledElements() {\n    return getEnabledColumnHeaders();\n  }\n\n  private List<BrokersGridItem> initGridItems() {\n    List<BrokersGridItem> gridItemList = new ArrayList<>();\n    gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0))\n        .forEach(item -> gridItemList.add(new BrokersGridItem(item)));\n    return gridItemList;\n  }\n\n  @Step\n  public BrokersGridItem getBroker(int id) {\n    return initGridItems().stream()\n        .filter(e -> e.getId() == id)\n        .findFirst().orElseThrow();\n  }\n\n  @Step\n  public List<BrokersGridItem> getAllBrokers() {\n    return initGridItems();\n  }\n\n  public static class BrokersGridItem extends BasePage {\n\n    private final SelenideElement element;\n\n    public BrokersGridItem(SelenideElement element) {\n      this.element = element;\n    }\n\n    private SelenideElement getIdElm() {\n      return element.$x(\"./td[1]/div/a\");\n    }\n\n    @Step\n    public int getId() {\n      return Integer.parseInt(getIdElm().getText().trim());\n    }\n\n    @Step\n    public void openItem() {\n      getIdElm().click();\n    }\n\n    @Step\n    public int getSegmentSize() {\n      return Integer.parseInt(element.$x(\"./td[2]\").getText().trim());\n    }\n\n    @Step\n    public int getSegmentCount() {\n      return Integer.parseInt(element.$x(\"./td[3]\").getText().trim());\n    }\n\n    @Step\n    public int getPort() {\n      return Integer.parseInt(element.$x(\"./td[4]\").getText().trim());\n    }\n\n    @Step\n    public String getHost() {\n      return element.$x(\"./td[5]\").getText().trim();\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorCreateForm.java",
    "content": "package com.provectus.kafka.ui.pages.connectors;\n\nimport static com.codeborne.selenide.Selenide.$x;\n\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.SelenideElement;\nimport com.provectus.kafka.ui.pages.BasePage;\nimport io.qameta.allure.Step;\n\npublic class ConnectorCreateForm extends BasePage {\n\n  protected SelenideElement nameField = $x(\"//input[@name='name']\");\n  protected SelenideElement contentTextArea = $x(\"//textarea[@class='ace_text-input']\");\n  protected SelenideElement configField = $x(\"//div[@id='config']\");\n\n  @Step\n  public ConnectorCreateForm waitUntilScreenReady() {\n    waitUntilSpinnerDisappear();\n    nameField.shouldBe(Condition.visible);\n    return this;\n  }\n\n  @Step\n  public ConnectorCreateForm setName(String connectName) {\n    nameField.shouldBe(Condition.enabled).setValue(connectName);\n    return this;\n  }\n\n  @Step\n  public ConnectorCreateForm setConfig(String configJson) {\n    configField.shouldBe(Condition.enabled).click();\n    setJsonInputValue(contentTextArea, configJson);\n    return this;\n  }\n\n  @Step\n  public ConnectorCreateForm setConnectorDetails(String connectName, String configJson) {\n    setName(connectName);\n    setConfig(configJson);\n    return this;\n  }\n\n  @Step\n  public ConnectorCreateForm clickSubmitButton() {\n    clickSubmitBtn();\n    waitUntilSpinnerDisappear();\n    return this;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/ConnectorDetails.java",
    "content": "package com.provectus.kafka.ui.pages.connectors;\n\nimport static com.codeborne.selenide.Selenide.$x;\n\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.SelenideElement;\nimport com.provectus.kafka.ui.pages.BasePage;\nimport io.qameta.allure.Step;\n\npublic class ConnectorDetails extends BasePage {\n\n  protected SelenideElement deleteBtn = $x(\"//li/div[contains(text(),'Delete')]\");\n  protected SelenideElement confirmBtnMdl = $x(\"//div[@role='dialog']//button[contains(text(),'Confirm')]\");\n  protected SelenideElement contentTextArea = $x(\"//textarea[@class='ace_text-input']\");\n  protected SelenideElement taskTab = $x(\"//a[contains(text(),'Tasks')]\");\n  protected SelenideElement configTab = $x(\"//a[contains(text(),'Config')]\");\n  protected SelenideElement configField = $x(\"//div[@id='config']\");\n  protected String connectorHeaderLocator = \"//h1[contains(text(),'%s')]\";\n\n  @Step\n  public ConnectorDetails waitUntilScreenReady() {\n    waitUntilSpinnerDisappear();\n    dotMenuBtn.shouldBe(Condition.visible);\n    return this;\n  }\n\n  @Step\n  public ConnectorDetails openConfigTab() {\n    clickByJavaScript(configTab);\n    return this;\n  }\n\n  @Step\n  public ConnectorDetails setConfig(String configJson) {\n    configField.shouldBe(Condition.enabled).click();\n    clearByKeyboard(contentTextArea);\n    contentTextArea.setValue(configJson);\n    configField.shouldBe(Condition.enabled).click();\n    return this;\n  }\n\n  @Step\n  public ConnectorDetails clickSubmitButton() {\n    clickSubmitBtn();\n    return this;\n  }\n\n  @Step\n  public ConnectorDetails openDotMenu() {\n    clickByJavaScript(dotMenuBtn);\n    return this;\n  }\n\n  @Step\n  public ConnectorDetails clickDeleteBtn() {\n    clickByJavaScript(deleteBtn);\n    return this;\n  }\n\n  @Step\n  public ConnectorDetails clickConfirmBtn() {\n    confirmBtnMdl.shouldBe(Condition.enabled).click();\n    confirmBtnMdl.shouldBe(Condition.disappear);\n    return this;\n  }\n\n  @Step\n  public ConnectorDetails deleteConnector() {\n    openDotMenu();\n    clickDeleteBtn();\n    clickConfirmBtn();\n    return this;\n  }\n\n  @Step\n  public boolean isConnectorHeaderVisible(String connectorName) {\n    return isVisible($x(String.format(connectorHeaderLocator, connectorName)));\n  }\n\n  @Step\n  public boolean isAlertWithMessageVisible(AlertHeader header, String message) {\n    return isAlertVisible(header, message);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connectors/KafkaConnectList.java",
    "content": "package com.provectus.kafka.ui.pages.connectors;\n\nimport static com.codeborne.selenide.Selenide.$x;\nimport static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KAFKA_CONNECT;\n\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.SelenideElement;\nimport com.provectus.kafka.ui.pages.BasePage;\nimport io.qameta.allure.Step;\n\n\npublic class KafkaConnectList extends BasePage {\n\n  protected SelenideElement createConnectorBtn = $x(\"//button[contains(text(),'Create Connector')]\");\n\n  public KafkaConnectList() {\n    tableElementNameLocator = \"//tbody//td[contains(text(),'%s')]\";\n  }\n\n  @Step\n  public KafkaConnectList waitUntilScreenReady() {\n    waitUntilSpinnerDisappear();\n    getPageTitleFromHeader(KAFKA_CONNECT).shouldBe(Condition.visible);\n    return this;\n  }\n\n  @Step\n  public KafkaConnectList clickCreateConnectorBtn() {\n    clickByJavaScript(createConnectorBtn);\n    return this;\n  }\n\n  @Step\n  public KafkaConnectList openConnector(String connectorName) {\n    getTableElement(connectorName).shouldBe(Condition.enabled).click();\n    return this;\n  }\n\n  @Step\n  public boolean isConnectorVisible(String connectorName) {\n    tableGrid.shouldBe(Condition.visible);\n    return isVisible(getTableElement(connectorName));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersDetails.java",
    "content": "package com.provectus.kafka.ui.pages.consumers;\n\nimport static com.codeborne.selenide.Selenide.$x;\n\nimport com.codeborne.selenide.Condition;\nimport com.provectus.kafka.ui.pages.BasePage;\nimport io.qameta.allure.Step;\n\npublic class ConsumersDetails extends BasePage {\n\n  protected String consumerIdHeaderLocator = \"//h1[contains(text(),'%s')]\";\n  protected String topicElementLocator = \"//tbody//td//a[text()='%s']\";\n\n  @Step\n  public ConsumersDetails waitUntilScreenReady() {\n    waitUntilSpinnerDisappear();\n    tableGrid.shouldBe(Condition.visible);\n    return this;\n  }\n\n  @Step\n  public boolean isRedirectedConsumerTitleVisible(String consumerGroupId) {\n    return isVisible($x(String.format(consumerIdHeaderLocator, consumerGroupId)));\n  }\n\n  @Step\n  public boolean isTopicInConsumersDetailsVisible(String topicName) {\n    tableGrid.shouldBe(Condition.visible);\n    return isVisible($x(String.format(topicElementLocator, topicName)));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/consumers/ConsumersList.java",
    "content": "package com.provectus.kafka.ui.pages.consumers;\n\nimport static com.provectus.kafka.ui.pages.panels.enums.MenuItem.CONSUMERS;\n\nimport com.codeborne.selenide.Condition;\nimport com.provectus.kafka.ui.pages.BasePage;\nimport io.qameta.allure.Step;\n\npublic class ConsumersList extends BasePage {\n\n  @Step\n  public ConsumersList waitUntilScreenReady() {\n    waitUntilSpinnerDisappear();\n    getPageTitleFromHeader(CONSUMERS).shouldBe(Condition.visible);\n    return this;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlDbList.java",
    "content": "package com.provectus.kafka.ui.pages.ksqldb;\n\nimport static com.codeborne.selenide.Condition.visible;\nimport static com.codeborne.selenide.Selenide.$;\nimport static com.codeborne.selenide.Selenide.$x;\nimport static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KSQL_DB;\n\nimport com.codeborne.selenide.CollectionCondition;\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.SelenideElement;\nimport com.provectus.kafka.ui.pages.BasePage;\nimport com.provectus.kafka.ui.pages.ksqldb.enums.KsqlMenuTabs;\nimport io.qameta.allure.Step;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.openqa.selenium.By;\n\npublic class KsqlDbList extends BasePage {\n  protected SelenideElement executeKsqlBtn = $x(\"//button[text()='Execute KSQL Request']\");\n  protected SelenideElement tablesTab = $x(\"//nav[@role='navigation']/a[text()='Tables']\");\n  protected SelenideElement streamsTab = $x(\"//nav[@role='navigation']/a[text()='Streams']\");\n\n  @Step\n  public KsqlDbList waitUntilScreenReady() {\n    waitUntilSpinnerDisappear();\n    getPageTitleFromHeader(KSQL_DB).shouldBe(Condition.visible);\n    return this;\n  }\n\n  @Step\n  public KsqlDbList clickExecuteKsqlRequestBtn() {\n    clickByJavaScript(executeKsqlBtn);\n    return this;\n  }\n\n  @Step\n  public KsqlDbList openDetailsTab(KsqlMenuTabs menu) {\n    $(By.linkText(menu.toString())).shouldBe(Condition.visible).click();\n    waitUntilSpinnerDisappear();\n    return this;\n  }\n\n  private List<KsqlDbList.KsqlTablesGridItem> initTablesItems() {\n    List<KsqlDbList.KsqlTablesGridItem> gridItemList = new ArrayList<>();\n    gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0))\n        .forEach(item -> gridItemList.add(new KsqlDbList.KsqlTablesGridItem(item)));\n    return gridItemList;\n  }\n\n  @Step\n  public KsqlDbList.KsqlTablesGridItem getTableByName(String tableName) {\n    return initTablesItems().stream()\n        .filter(e -> e.getTableName().equals(tableName))\n        .findFirst().orElseThrow();\n  }\n\n  private List<KsqlDbList.KsqlStreamsGridItem> initStreamsItems() {\n    List<KsqlDbList.KsqlStreamsGridItem> gridItemList = new ArrayList<>();\n    gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0))\n        .forEach(item -> gridItemList.add(new KsqlDbList.KsqlStreamsGridItem(item)));\n    return gridItemList;\n  }\n\n  @Step\n  public KsqlDbList.KsqlStreamsGridItem getStreamByName(String streamName) {\n    return initStreamsItems().stream()\n        .filter(e -> e.getStreamName().equals(streamName))\n        .findFirst().orElseThrow();\n  }\n\n  public static class KsqlTablesGridItem extends BasePage {\n\n    private final SelenideElement element;\n\n    public KsqlTablesGridItem(SelenideElement element) {\n      this.element = element;\n    }\n\n    private SelenideElement getNameElm() {\n      return element.$x(\"./td[1]\");\n    }\n\n    @Step\n    public String getTableName() {\n      return getNameElm().getText().trim();\n    }\n\n    @Step\n    public boolean isVisible() {\n      boolean isVisible = false;\n      try {\n        getNameElm().shouldBe(visible, Duration.ofMillis(500));\n        isVisible = true;\n      } catch (Throwable ignored) {\n      }\n      return isVisible;\n    }\n\n    @Step\n    public String getTopicName() {\n      return element.$x(\"./td[2]\").getText().trim();\n    }\n\n    @Step\n    public String getKeyFormat() {\n      return element.$x(\"./td[3]\").getText().trim();\n    }\n\n    @Step\n    public String getValueFormat() {\n      return element.$x(\"./td[4]\").getText().trim();\n    }\n\n    @Step\n    public String getIsWindowed() {\n      return element.$x(\"./td[5]\").getText().trim();\n    }\n  }\n\n  public static class KsqlStreamsGridItem extends BasePage {\n\n    private final SelenideElement element;\n\n    public KsqlStreamsGridItem(SelenideElement element) {\n      this.element = element;\n    }\n\n    private SelenideElement getNameElm() {\n      return element.$x(\"./td[1]\");\n    }\n\n    @Step\n    public String getStreamName() {\n      return getNameElm().getText().trim();\n    }\n\n    @Step\n    public boolean isVisible() {\n      boolean isVisible = false;\n      try {\n        getNameElm().shouldBe(visible, Duration.ofMillis(500));\n        isVisible = true;\n      } catch (Throwable ignored) {\n      }\n      return isVisible;\n    }\n\n    @Step\n    public String getTopicName() {\n      return element.$x(\"./td[2]\").getText().trim();\n    }\n\n    @Step\n    public String getKeyFormat() {\n      return element.$x(\"./td[3]\").getText().trim();\n    }\n\n    @Step\n    public String getValueFormat() {\n      return element.$x(\"./td[4]\").getText().trim();\n    }\n\n    @Step\n    public String getIsWindowed() {\n      return element.$x(\"./td[5]\").getText().trim();\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlQueryForm.java",
    "content": "package com.provectus.kafka.ui.pages.ksqldb;\n\nimport static com.codeborne.selenide.Condition.visible;\nimport static com.codeborne.selenide.Selenide.$$x;\nimport static com.codeborne.selenide.Selenide.$x;\nimport static com.codeborne.selenide.Selenide.sleep;\n\nimport com.codeborne.selenide.CollectionCondition;\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.ElementsCollection;\nimport com.codeborne.selenide.SelenideElement;\nimport com.provectus.kafka.ui.pages.BasePage;\nimport io.qameta.allure.Step;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class KsqlQueryForm extends BasePage {\n  protected SelenideElement clearBtn = $x(\"//div/button[text()='Clear']\");\n  protected SelenideElement executeBtn = $x(\"//div/button[text()='Execute']\");\n  protected SelenideElement clearResultsBtn = $x(\"//div/button[text()='Clear results']\");\n  protected SelenideElement addStreamPropertyBtn = $x(\"//button[text()='Add Stream Property']\");\n  protected SelenideElement queryAreaValue = $x(\"//div[@class='ace_content']\");\n  protected SelenideElement queryArea = $x(\"//div[@id='ksql']/textarea[@class='ace_text-input']\");\n  protected SelenideElement abortButton = $x(\"//div[@role='status']/div[text()='Abort']\");\n  protected SelenideElement cancelledAlert = $x(\"//div[@role='status'][text()='Cancelled']\");\n  protected ElementsCollection ksqlGridItems = $$x(\"//tbody//tr\");\n  protected ElementsCollection keyField = $$x(\"//input[@aria-label='key']\");\n  protected ElementsCollection valueField = $$x(\"//input[@aria-label='value']\");\n\n  @Step\n  public KsqlQueryForm waitUntilScreenReady() {\n    waitUntilSpinnerDisappear();\n    executeBtn.shouldBe(Condition.visible);\n    return this;\n  }\n\n  @Step\n  public KsqlQueryForm clickClearBtn() {\n    clickByJavaScript(clearBtn);\n    sleep(500);\n    return this;\n  }\n\n  @Step\n  public String getEnteredQuery() {\n    return queryAreaValue.getText().trim();\n  }\n\n  @Step\n  public KsqlQueryForm clickExecuteBtn(String query) {\n    clickByActions(executeBtn);\n    if (query.contains(\"EMIT CHANGES\")) {\n      abortButton.shouldBe(Condition.visible);\n    } else {\n      waitUntilSpinnerDisappear();\n    }\n    return this;\n  }\n\n  @Step\n  public boolean isAbortBtnVisible() {\n    return isVisible(abortButton);\n  }\n\n  @Step\n  public KsqlQueryForm clickAbortBtn() {\n    clickByActions(abortButton);\n    return this;\n  }\n\n  @Step\n  public boolean isCancelledAlertVisible() {\n    return isVisible(cancelledAlert);\n  }\n\n  @Step\n  public boolean isClearResultsBtnEnabled() {\n    return isEnabled(clearResultsBtn);\n  }\n\n  @Step\n  public KsqlQueryForm clickClearResultsBtn() {\n    clickByActions(clearResultsBtn);\n    waitUntilSpinnerDisappear();\n    return this;\n  }\n\n  @Step\n  public KsqlQueryForm clickAddStreamProperty() {\n    clickByActions(addStreamPropertyBtn);\n    return this;\n  }\n\n  @Step\n  public KsqlQueryForm setQuery(String query) {\n    queryAreaValue.shouldBe(Condition.visible).click();\n    sendKeysByActions(queryArea, query);\n    return this;\n  }\n\n  @Step\n  public KsqlQueryForm.KsqlResponseGridItem getItemByName(String name) {\n    return initItems().stream()\n        .filter(e -> e.getName().equalsIgnoreCase(name))\n        .findFirst().orElseThrow();\n  }\n\n  @Step\n  public boolean areResultsVisible() {\n    boolean visible = false;\n    try {\n      visible = initItems().size() > 0;\n    } catch (Throwable ignored) {\n    }\n    return visible;\n  }\n\n  private List<KsqlQueryForm.KsqlResponseGridItem> initItems() {\n    List<KsqlQueryForm.KsqlResponseGridItem> gridItemList = new ArrayList<>();\n    ksqlGridItems.shouldHave(CollectionCondition.sizeGreaterThan(0))\n        .forEach(item -> gridItemList.add(new KsqlQueryForm.KsqlResponseGridItem(item)));\n    return gridItemList;\n  }\n\n  public static class KsqlResponseGridItem extends BasePage {\n\n    private final SelenideElement element;\n\n    private KsqlResponseGridItem(SelenideElement element) {\n      this.element = element;\n    }\n\n    @Step\n    public String getType() {\n      return element.$x(\"./td[1]\").getText().trim();\n    }\n\n    private SelenideElement getNameElm() {\n      return element.$x(\"./td[2]\");\n    }\n\n    @Step\n    public String getName() {\n      return getNameElm().scrollTo().getText().trim();\n    }\n\n    @Step\n    public boolean isVisible() {\n      boolean isVisible = false;\n      try {\n        getNameElm().shouldBe(visible, Duration.ofMillis(500));\n        isVisible = true;\n      } catch (Throwable ignored) {\n      }\n      return isVisible;\n    }\n\n    @Step\n    public String getTopic() {\n      return element.$x(\"./td[3]\").getText().trim();\n    }\n\n    @Step\n    public String getKeyFormat() {\n      return element.$x(\"./td[4]\").getText().trim();\n    }\n\n    @Step\n    public String getValueFormat() {\n      return element.$x(\"./td[5]\").getText().trim();\n    }\n\n    @Step\n    public String getIsWindowed() {\n      return element.$x(\"./td[6]\").getText().trim();\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/enums/KsqlMenuTabs.java",
    "content": "package com.provectus.kafka.ui.pages.ksqldb.enums;\n\npublic enum KsqlMenuTabs {\n\n  TABLES(\"Table\"),\n  STREAMS(\"Streams\");\n\n  private final String value;\n\n  KsqlMenuTabs(String value) {\n    this.value = value;\n  }\n\n  public String toString() {\n    return value;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/enums/KsqlQueryConfig.java",
    "content": "package com.provectus.kafka.ui.pages.ksqldb.enums;\n\npublic enum KsqlQueryConfig {\n\n  SHOW_TABLES(\"show tables;\"),\n  SHOW_STREAMS(\"show streams;\"),\n  SELECT_ALL_FROM(\"SELECT * FROM %s\\n\" + \"EMIT CHANGES;\");\n\n  private final String query;\n\n  KsqlQueryConfig(String query) {\n    this.query = query;\n  }\n\n  public String getQuery() {\n    return query;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/models/Stream.java",
    "content": "package com.provectus.kafka.ui.pages.ksqldb.models;\n\nimport lombok.Data;\nimport lombok.experimental.Accessors;\n\n@Data\n@Accessors(chain = true)\npublic class Stream {\n\n  private String name, topicName, valueFormat, partitions;\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/models/Table.java",
    "content": "package com.provectus.kafka.ui.pages.ksqldb.models;\n\nimport lombok.Data;\nimport lombok.experimental.Accessors;\n\n@Data\n@Accessors(chain = true)\npublic class Table {\n\n  private String name, streamName;\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/NaviSideBar.java",
    "content": "package com.provectus.kafka.ui.pages.panels;\n\nimport static com.codeborne.selenide.Selenide.$x;\nimport static com.provectus.kafka.ui.settings.BaseSource.CLUSTER_NAME;\n\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.SelenideElement;\nimport com.provectus.kafka.ui.pages.BasePage;\nimport com.provectus.kafka.ui.pages.panels.enums.MenuItem;\nimport io.qameta.allure.Step;\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class NaviSideBar extends BasePage {\n\n  protected SelenideElement dashboardMenuItem = $x(\"//a[@title='Dashboard']\");\n  protected String sideMenuOptionElementLocator = \".//ul/li[contains(.,'%s')]\";\n  protected String clusterElementLocator = \"//aside/ul/li[contains(.,'%s')]\";\n\n  private SelenideElement expandCluster(String clusterName) {\n    SelenideElement clusterElement = $x(String.format(clusterElementLocator, clusterName)).shouldBe(Condition.visible);\n    if (clusterElement.parent().$$x(\".//ul\").size() == 0) {\n      clickByActions(clusterElement);\n    }\n    return clusterElement;\n  }\n\n  @Step\n  public NaviSideBar waitUntilScreenReady() {\n    waitUntilSpinnerDisappear();\n    dashboardMenuItem.shouldBe(Condition.visible, Duration.ofSeconds(30));\n    return this;\n  }\n\n  @Step\n  public String getPagePath(MenuItem menuItem) {\n    return getPagePathFromHeader(menuItem)\n        .shouldBe(Condition.visible)\n        .getText().trim();\n  }\n\n  @Step\n  public NaviSideBar openSideMenu(String clusterName, MenuItem menuItem) {\n    clickByActions(expandCluster(clusterName).parent()\n        .$x(String.format(sideMenuOptionElementLocator, menuItem.getNaviTitle())));\n    return this;\n  }\n\n  @Step\n  public NaviSideBar openSideMenu(MenuItem menuItem) {\n    openSideMenu(CLUSTER_NAME, menuItem);\n    return this;\n  }\n\n  public List<SelenideElement> getAllMenuButtons() {\n    expandCluster(CLUSTER_NAME);\n    return Stream.of(MenuItem.values())\n        .map(menuItem -> $x(String.format(sideMenuOptionElementLocator, menuItem.getNaviTitle())))\n        .collect(Collectors.toList());\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/TopPanel.java",
    "content": "package com.provectus.kafka.ui.pages.panels;\n\nimport static com.codeborne.selenide.Selenide.$x;\n\nimport com.codeborne.selenide.SelenideElement;\nimport com.provectus.kafka.ui.pages.BasePage;\nimport java.util.Arrays;\nimport java.util.List;\n\npublic class TopPanel extends BasePage {\n\n  protected SelenideElement kafkaLogo = $x(\"//a[contains(text(),'UI for Apache Kafka')]\");\n  protected SelenideElement kafkaVersion = $x(\"//a[@title='Current commit']\");\n  protected SelenideElement logOutBtn = $x(\"//button[contains(text(),'Log out')]\");\n  protected SelenideElement gitBtn = $x(\"//a[@href='https://github.com/provectus/kafka-ui']\");\n  protected SelenideElement discordBtn = $x(\"//a[contains(@href,'https://discord.com/invite')]\");\n\n  public List<SelenideElement> getAllVisibleElements() {\n    return Arrays.asList(kafkaLogo, kafkaVersion, gitBtn, discordBtn);\n  }\n\n  public List<SelenideElement> getAllEnabledElements() {\n    return Arrays.asList(gitBtn, discordBtn, kafkaLogo);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/panels/enums/MenuItem.java",
    "content": "package com.provectus.kafka.ui.pages.panels.enums;\n\npublic enum MenuItem {\n\n  DASHBOARD(\"Dashboard\", \"Dashboard\"),\n  BROKERS(\"Brokers\", \"Brokers\"),\n  TOPICS(\"Topics\", \"Topics\"),\n  CONSUMERS(\"Consumers\", \"Consumers\"),\n  SCHEMA_REGISTRY(\"Schema Registry\", \"Schema Registry\"),\n  KAFKA_CONNECT(\"Kafka Connect\", \"Connectors\"),\n  KSQL_DB(\"KSQL DB\", \"KSQL DB\");\n\n  private final String naviTitle;\n  private final String pageTitle;\n\n  MenuItem(String naviTitle, String pageTitle) {\n    this.naviTitle = naviTitle;\n    this.pageTitle = pageTitle;\n  }\n\n  public String getNaviTitle() {\n    return naviTitle;\n  }\n\n  public String getPageTitle() {\n    return pageTitle;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaCreateForm.java",
    "content": "package com.provectus.kafka.ui.pages.schemas;\n\nimport static com.codeborne.selenide.Selenide.$;\nimport static com.codeborne.selenide.Selenide.$$x;\nimport static com.codeborne.selenide.Selenide.$x;\nimport static org.openqa.selenium.By.id;\n\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.SelenideElement;\nimport com.codeborne.selenide.WebDriverRunner;\nimport com.provectus.kafka.ui.api.model.CompatibilityLevel;\nimport com.provectus.kafka.ui.api.model.SchemaType;\nimport com.provectus.kafka.ui.pages.BasePage;\nimport io.qameta.allure.Step;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport org.openqa.selenium.Keys;\nimport org.openqa.selenium.interactions.Actions;\n\npublic class SchemaCreateForm extends BasePage {\n\n  protected SelenideElement schemaNameField = $x(\"//input[@name='subject']\");\n  protected SelenideElement pageTitle = $x(\"//h1['Edit']\");\n  protected SelenideElement schemaTextArea = $x(\"//textarea[@name='schema']\");\n  protected SelenideElement newSchemaInput = $(\"#newSchema [wrap]\");\n  protected SelenideElement schemaTypeDdl = $x(\"//ul[@name='schemaType']\");\n  protected SelenideElement compatibilityLevelList = $x(\"//ul[@name='compatibilityLevel']\");\n  protected SelenideElement newSchemaTextArea = $x(\"//div[@id='newSchema']\");\n  protected SelenideElement latestSchemaTextArea = $x(\"//div[@id='latestSchema']\");\n  protected SelenideElement leftVersionDdl = $(id(\"left-select\"));\n  protected SelenideElement rightVersionDdl = $(id(\"right-select\"));\n  protected List<SelenideElement> visibleMarkers =\n      $$x(\"//div[@class='ace_scroller']//div[contains(@class,'codeMarker')]\");\n  protected List<SelenideElement> elementsCompareVersionDdl = $$x(\"//ul[@role='listbox']/ul/li\");\n  protected String ddlElementLocator = \"//li[@value='%s']\";\n\n  @Step\n  public SchemaCreateForm waitUntilScreenReady() {\n    waitUntilSpinnerDisappear();\n    pageTitle.shouldBe(Condition.visible);\n    return this;\n  }\n\n  @Step\n  public SchemaCreateForm setSubjectName(String name) {\n    schemaNameField.setValue(name);\n    return this;\n  }\n\n  @Step\n  public SchemaCreateForm setSchemaField(String text) {\n    schemaTextArea.setValue(text);\n    return this;\n  }\n\n  @Step\n  public SchemaCreateForm selectSchemaTypeFromDropdown(SchemaType schemaType) {\n    schemaTypeDdl.shouldBe(Condition.enabled).click();\n    $x(String.format(ddlElementLocator, schemaType.getValue())).shouldBe(Condition.visible).click();\n    return this;\n  }\n\n  @Step\n  public SchemaCreateForm clickSubmitButton() {\n    clickSubmitBtn();\n    return this;\n  }\n\n  @Step\n  public SchemaCreateForm selectCompatibilityLevelFromDropdown(CompatibilityLevel.CompatibilityEnum level) {\n    compatibilityLevelList.shouldBe(Condition.enabled).click();\n    $x(String.format(ddlElementLocator, level.getValue())).shouldBe(Condition.visible).click();\n    return this;\n  }\n\n  @Step\n  public SchemaCreateForm openLeftVersionDdl() {\n    leftVersionDdl.shouldBe(Condition.enabled).click();\n    return this;\n  }\n\n  @Step\n  public SchemaCreateForm openRightVersionDdl() {\n    rightVersionDdl.shouldBe(Condition.enabled).click();\n    return this;\n  }\n\n  @Step\n  public int getVersionsNumberFromList() {\n    return elementsCompareVersionDdl.size();\n  }\n\n  @Step\n  public SchemaCreateForm selectVersionFromDropDown(int versionNumberDd) {\n    $x(String.format(ddlElementLocator, versionNumberDd)).shouldBe(Condition.visible).click();\n    return this;\n  }\n\n  @Step\n  public int getMarkedLinesNumber() {\n    return visibleMarkers.size();\n  }\n\n  @Step\n  public SchemaCreateForm setNewSchemaValue(String configJson) {\n    newSchemaTextArea.shouldBe(Condition.visible).click();\n    newSchemaInput.shouldBe(Condition.enabled);\n    new Actions(WebDriverRunner.getWebDriver())\n        .sendKeys(Keys.PAGE_UP)\n        .keyDown(Keys.SHIFT)\n        .sendKeys(Keys.PAGE_DOWN)\n        .keyUp(Keys.SHIFT)\n        .sendKeys(Keys.DELETE)\n        .perform();\n    setJsonInputValue(newSchemaInput, configJson);\n    return this;\n  }\n\n  @Step\n  public List<SelenideElement> getAllDetailsPageElements() {\n    return Stream.of(compatibilityLevelList, newSchemaTextArea, latestSchemaTextArea, submitBtn, schemaTypeDdl)\n        .collect(Collectors.toList());\n  }\n\n  @Step\n  public boolean isSubmitBtnEnabled() {\n    return isEnabled(submitBtn);\n  }\n\n  @Step\n  public boolean isSchemaDropDownEnabled() {\n    boolean enabled = true;\n    try {\n      String attribute = schemaTypeDdl.getAttribute(\"disabled\");\n      enabled = false;\n    } catch (Throwable ignored) {\n    }\n    return enabled;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaDetails.java",
    "content": "package com.provectus.kafka.ui.pages.schemas;\n\nimport static com.codeborne.selenide.Selenide.$x;\n\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.SelenideElement;\nimport com.provectus.kafka.ui.pages.BasePage;\nimport io.qameta.allure.Step;\n\npublic class SchemaDetails extends BasePage {\n\n  protected SelenideElement actualVersionTextArea = $x(\"//div[@id='schema']\");\n  protected SelenideElement compatibilityField = $x(\"//h4[contains(text(),'Compatibility')]/../p\");\n  protected SelenideElement editSchemaBtn = $x(\"//button[contains(text(),'Edit Schema')]\");\n  protected SelenideElement removeBtn = $x(\"//*[contains(text(),'Remove')]\");\n  protected SelenideElement schemaConfirmBtn = $x(\"//div[@role='dialog']//button[contains(text(),'Confirm')]\");\n  protected SelenideElement schemaTypeField = $x(\"//h4[contains(text(),'Type')]/../p\");\n  protected SelenideElement latestVersionField = $x(\"//h4[contains(text(),'Latest version')]/../p\");\n  protected SelenideElement compareVersionBtn = $x(\"//button[text()='Compare Versions']\");\n  protected String schemaHeaderLocator = \"//h1[contains(text(),'%s')]\";\n\n  @Step\n  public SchemaDetails waitUntilScreenReady() {\n    waitUntilSpinnerDisappear();\n    actualVersionTextArea.shouldBe(Condition.visible);\n    return this;\n  }\n\n  @Step\n  public String getCompatibility() {\n    return compatibilityField.getText();\n  }\n\n  @Step\n  public boolean isSchemaHeaderVisible(String schemaName) {\n    return isVisible($x(String.format(schemaHeaderLocator, schemaName)));\n  }\n\n  @Step\n  public int getLatestVersion() {\n    return Integer.parseInt(latestVersionField.getText());\n  }\n\n  @Step\n  public String getSchemaType() {\n    return schemaTypeField.getText();\n  }\n\n  @Step\n  public SchemaDetails openEditSchema() {\n    editSchemaBtn.shouldBe(Condition.visible).click();\n    return this;\n  }\n\n  @Step\n  public SchemaDetails openCompareVersionMenu() {\n    compareVersionBtn.shouldBe(Condition.enabled).click();\n    return this;\n  }\n\n  @Step\n  public SchemaDetails removeSchema() {\n    clickByJavaScript(dotMenuBtn);\n    removeBtn.shouldBe(Condition.enabled).click();\n    schemaConfirmBtn.shouldBe(Condition.visible).click();\n    schemaConfirmBtn.shouldBe(Condition.disappear);\n    return this;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaRegistryList.java",
    "content": "package com.provectus.kafka.ui.pages.schemas;\n\nimport static com.codeborne.selenide.Selenide.$x;\nimport static com.provectus.kafka.ui.pages.panels.enums.MenuItem.SCHEMA_REGISTRY;\n\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.SelenideElement;\nimport com.provectus.kafka.ui.pages.BasePage;\nimport io.qameta.allure.Step;\n\npublic class SchemaRegistryList extends BasePage {\n\n  protected SelenideElement createSchemaBtn = $x(\"//button[contains(text(),'Create Schema')]\");\n\n  @Step\n  public SchemaRegistryList waitUntilScreenReady() {\n    waitUntilSpinnerDisappear();\n    getPageTitleFromHeader(SCHEMA_REGISTRY).shouldBe(Condition.visible);\n    return this;\n  }\n\n  @Step\n  public SchemaRegistryList clickCreateSchema() {\n    clickByJavaScript(createSchemaBtn);\n    return this;\n  }\n\n  @Step\n  public SchemaRegistryList openSchema(String schemaName) {\n    getTableElement(schemaName)\n        .shouldBe(Condition.enabled).click();\n    return this;\n  }\n\n  @Step\n  public boolean isSchemaVisible(String schemaName) {\n    tableGrid.shouldBe(Condition.visible);\n    return isVisible(getTableElement(schemaName));\n  }\n}\n\n\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/ProduceMessagePanel.java",
    "content": "package com.provectus.kafka.ui.pages.topics;\n\nimport static com.codeborne.selenide.Selenide.$x;\nimport static com.codeborne.selenide.Selenide.refresh;\n\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.SelenideElement;\nimport com.provectus.kafka.ui.pages.BasePage;\nimport io.qameta.allure.Step;\nimport java.util.Arrays;\n\npublic class ProduceMessagePanel extends BasePage {\n\n  protected SelenideElement keyTextArea = $x(\"//div[@id='key']/textarea\");\n  protected SelenideElement valueTextArea = $x(\"//div[@id='content']/textarea\");\n  protected SelenideElement headersTextArea = $x(\"//div[@id='headers']/textarea\");\n  protected SelenideElement submitProduceMessageBtn = headersTextArea.$x(\"../../../..//button[@type='submit']\");\n  protected SelenideElement partitionDdl = $x(\"//ul[@name='partition']\");\n  protected SelenideElement keySerdeDdl = $x(\"//ul[@name='keySerde']\");\n  protected SelenideElement contentSerdeDdl = $x(\"//ul[@name='valueSerde']\");\n\n  @Step\n  public ProduceMessagePanel waitUntilScreenReady() {\n    waitUntilSpinnerDisappear();\n    Arrays.asList(partitionDdl, keySerdeDdl, contentSerdeDdl).forEach(element -> element.shouldBe(Condition.visible));\n    return this;\n  }\n\n  @Step\n  public ProduceMessagePanel setKeyField(String value) {\n    clearByKeyboard(keyTextArea);\n    keyTextArea.setValue(value);\n    return this;\n  }\n\n  @Step\n  public ProduceMessagePanel setValueFiled(String value) {\n    clearByKeyboard(valueTextArea);\n    valueTextArea.setValue(value);\n    return this;\n  }\n\n  @Step\n  public ProduceMessagePanel setHeadersFld(String value) {\n    headersTextArea.setValue(value);\n    return this;\n  }\n\n  @Step\n  public ProduceMessagePanel submitProduceMessage() {\n    clickByActions(submitProduceMessageBtn);\n    submitProduceMessageBtn.shouldBe(Condition.disappear);\n    refresh();\n    return this;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicCreateEditForm.java",
    "content": "package com.provectus.kafka.ui.pages.topics;\n\nimport static com.codeborne.selenide.Selenide.$;\nimport static com.codeborne.selenide.Selenide.$$;\nimport static com.codeborne.selenide.Selenide.$x;\nimport static org.openqa.selenium.By.id;\n\nimport com.codeborne.selenide.ClickOptions;\nimport com.codeborne.selenide.CollectionCondition;\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.ElementsCollection;\nimport com.codeborne.selenide.SelenideElement;\nimport com.provectus.kafka.ui.pages.BasePage;\nimport com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue;\nimport com.provectus.kafka.ui.pages.topics.enums.CustomParameterType;\nimport com.provectus.kafka.ui.pages.topics.enums.MaxSizeOnDisk;\nimport com.provectus.kafka.ui.pages.topics.enums.TimeToRetain;\nimport io.qameta.allure.Step;\n\npublic class TopicCreateEditForm extends BasePage {\n\n  private static final String RETENTION_BYTES = \"retentionBytes\";\n\n  protected SelenideElement timeToRetainField = $x(\"//input[@id='timeToRetain']\");\n  protected SelenideElement partitionsField = $x(\"//input[@name='partitions']\");\n  protected SelenideElement nameField = $(id(\"topicFormName\"));\n  protected SelenideElement maxMessageBytesField = $x(\"//input[@name='maxMessageBytes']\");\n  protected SelenideElement minInSyncReplicasField = $x(\"//input[@name='minInSyncReplicas']\");\n  protected SelenideElement cleanUpPolicyDdl = $x(\"//ul[@id='topicFormCleanupPolicy']\");\n  protected SelenideElement maxSizeOnDiscDdl = $x(\"//ul[@id='topicFormRetentionBytes']\");\n  protected SelenideElement customParameterDdl = $x(\"//ul[contains(@name,'customParams')]\");\n  protected SelenideElement deleteCustomParameterBtn = $x(\"//span[contains(@title,'Delete customParam')]\");\n  protected SelenideElement addCustomParameterTypeBtn = $x(\"//button[contains(text(),'Add Custom Parameter')]\");\n  protected SelenideElement customParameterValueField = $x(\"//input[@placeholder='Value']\");\n  protected SelenideElement validationCustomParameterValueMsg = $x(\"//p[contains(text(),'Value is required')]\");\n  protected String ddlElementLocator = \"//li[@value='%s']\";\n  protected String btnTimeToRetainLocator = \"//button[@class][text()='%s']\";\n\n\n  @Step\n  public TopicCreateEditForm waitUntilScreenReady() {\n    waitUntilSpinnerDisappear();\n    nameField.shouldBe(Condition.visible);\n    return this;\n  }\n\n  public boolean isCreateTopicButtonEnabled() {\n    return isEnabled(submitBtn);\n  }\n\n  public boolean isDeleteCustomParameterButtonEnabled() {\n    return isEnabled(deleteCustomParameterBtn);\n  }\n\n  public boolean isNameFieldEnabled() {\n    return isEnabled(nameField);\n  }\n\n  @Step\n  public TopicCreateEditForm setTopicName(String topicName) {\n    sendKeysAfterClear(nameField, topicName);\n    return this;\n  }\n\n  @Step\n  public TopicCreateEditForm setMinInsyncReplicas(Integer minInsyncReplicas) {\n    minInSyncReplicasField.setValue(minInsyncReplicas.toString());\n    return this;\n  }\n\n  @Step\n  public TopicCreateEditForm setTimeToRetainDataInMs(Long ms) {\n    timeToRetainField.setValue(ms.toString());\n    return this;\n  }\n\n  @Step\n  public TopicCreateEditForm setTimeToRetainDataInMs(String ms) {\n    timeToRetainField.setValue(ms);\n    return this;\n  }\n\n  @Step\n  public TopicCreateEditForm setMaxSizeOnDiskInGB(MaxSizeOnDisk maxSizeOnDisk) {\n    maxSizeOnDiscDdl.shouldBe(Condition.visible).click();\n    $x(String.format(ddlElementLocator, maxSizeOnDisk.getOptionValue())).shouldBe(Condition.visible).click();\n    return this;\n  }\n\n  @Step\n  public TopicCreateEditForm clickAddCustomParameterTypeButton() {\n    addCustomParameterTypeBtn.click();\n    return this;\n  }\n\n  @Step\n  public TopicCreateEditForm openCustomParameterTypeDdl() {\n    customParameterDdl.shouldBe(Condition.visible).click();\n    ddlOptions.shouldHave(CollectionCondition.sizeGreaterThan(0));\n    return this;\n  }\n\n  @Step\n  public ElementsCollection getAllDdlOptions() {\n    return getDdlOptions();\n  }\n\n  @Step\n  public TopicCreateEditForm setCustomParameterType(CustomParameterType customParameterType) {\n    openCustomParameterTypeDdl();\n    $x(String.format(ddlElementLocator, customParameterType.getOptionValue())).shouldBe(Condition.visible).click();\n    return this;\n  }\n\n  @Step\n  public TopicCreateEditForm clearCustomParameterValue() {\n    clearByKeyboard(customParameterValueField);\n    return this;\n  }\n\n  @Step\n  public TopicCreateEditForm setNumberOfPartitions(int partitions) {\n    partitionsField.shouldBe(Condition.enabled).clear();\n    partitionsField.sendKeys(String.valueOf(partitions));\n    return this;\n  }\n\n  @Step\n  public TopicCreateEditForm setTimeToRetainDataByButtons(TimeToRetain timeToRetain) {\n    $x(String.format(btnTimeToRetainLocator, timeToRetain.getButton())).shouldBe(Condition.enabled).click();\n    return this;\n  }\n\n  @Step\n  public TopicCreateEditForm selectCleanupPolicy(CleanupPolicyValue cleanupPolicyOptionValue) {\n    cleanUpPolicyDdl.shouldBe(Condition.visible).click();\n    $x(String.format(ddlElementLocator, cleanupPolicyOptionValue.getOptionValue())).shouldBe(Condition.visible).click();\n    return this;\n  }\n\n  @Step\n  public TopicCreateEditForm selectRetentionBytes(String visibleValue) {\n    return selectFromDropDownByVisibleText(RETENTION_BYTES, visibleValue);\n  }\n\n  @Step\n  public TopicCreateEditForm selectRetentionBytes(Long optionValue) {\n    return selectFromDropDownByOptionValue(RETENTION_BYTES, optionValue.toString());\n  }\n\n  @Step\n  public TopicCreateEditForm clickSaveTopicBtn() {\n    clickSubmitBtn();\n    return this;\n  }\n\n  @Step\n  public TopicCreateEditForm addCustomParameter(String customParameterName,\n                                                String customParameterValue) {\n    ElementsCollection customParametersElements =\n        $$(\"ul[role=listbox][name^=customParams][name$=name]\");\n    KafkaUiSelectElement kafkaUiSelectElement = null;\n    if (customParametersElements.size() == 1) {\n      if (\"Select\".equals(customParametersElements.first().getText())) {\n        kafkaUiSelectElement = new KafkaUiSelectElement(customParametersElements.first());\n      }\n    } else {\n      $$(\"button\")\n          .find(Condition.exactText(\"Add Custom Parameter\"))\n          .click();\n      customParametersElements = $$(\"ul[role=listbox][name^=customParams][name$=name]\");\n      kafkaUiSelectElement = new KafkaUiSelectElement(customParametersElements.last());\n    }\n    if (kafkaUiSelectElement != null) {\n      kafkaUiSelectElement.selectByVisibleText(customParameterName);\n    }\n    $(String.format(\"input[name=\\\"customParams.%d.value\\\"]\", customParametersElements.size() - 1))\n        .setValue(customParameterValue);\n    return this;\n  }\n\n  @Step\n  public TopicCreateEditForm updateCustomParameter(String customParameterName,\n                                                   String customParameterValue) {\n    SelenideElement selenideElement = $$(\"ul[role=listbox][name^=customParams][name$=name]\")\n        .find(Condition.exactText(customParameterName));\n    String name = selenideElement.getAttribute(\"name\");\n    if (name != null) {\n      name = name.substring(0, name.lastIndexOf(\".\"));\n    }\n    $(String.format(\"input[name^=%s]\", name)).setValue(customParameterValue);\n    return this;\n  }\n\n  @Step\n  public String getCleanupPolicy() {\n    return new KafkaUiSelectElement(\"cleanupPolicy\").getCurrentValue();\n  }\n\n  @Step\n  public String getTimeToRetain() {\n    return timeToRetainField.getValue();\n  }\n\n  @Step\n  public String getMaxSizeOnDisk() {\n    return new KafkaUiSelectElement(RETENTION_BYTES).getCurrentValue();\n  }\n\n  @Step\n  public String getMaxMessageBytes() {\n    return maxMessageBytesField.getValue();\n  }\n\n  @Step\n  public TopicCreateEditForm setMaxMessageBytes(Long bytes) {\n    maxMessageBytesField.setValue(bytes.toString());\n    return this;\n  }\n\n  @Step\n  public TopicCreateEditForm setMaxMessageBytes(String bytes) {\n    return setMaxMessageBytes(Long.parseLong(bytes));\n  }\n\n  @Step\n  public boolean isValidationMessageCustomParameterValueVisible() {\n    return isVisible(validationCustomParameterValueMsg);\n  }\n\n  @Step\n  public String getCustomParameterValue() {\n    return customParameterValueField.getValue();\n  }\n\n  private TopicCreateEditForm selectFromDropDownByOptionValue(String dropDownElementName,\n                                                              String optionValue) {\n    KafkaUiSelectElement select = new KafkaUiSelectElement(dropDownElementName);\n    select.selectByOptionValue(optionValue);\n    return this;\n  }\n\n  private TopicCreateEditForm selectFromDropDownByVisibleText(String dropDownElementName,\n                                                              String visibleText) {\n    KafkaUiSelectElement select = new KafkaUiSelectElement(dropDownElementName);\n    select.selectByVisibleText(visibleText);\n    return this;\n  }\n\n  private static class KafkaUiSelectElement {\n\n    private final SelenideElement selectElement;\n\n    public KafkaUiSelectElement(String selectElementName) {\n      this.selectElement = $(\"ul[role=listbox][name=\" + selectElementName + \"]\");\n    }\n\n    public KafkaUiSelectElement(SelenideElement selectElement) {\n      this.selectElement = selectElement;\n    }\n\n    public void selectByOptionValue(String optionValue) {\n      selectElement.click();\n      selectElement\n          .$$x(\".//ul/li[@role='option']\")\n          .find(Condition.attribute(\"value\", optionValue))\n          .click(ClickOptions.usingJavaScript());\n    }\n\n    public void selectByVisibleText(String visibleText) {\n      selectElement.click();\n      selectElement\n          .$$(\"ul>li[role=option]\")\n          .find(Condition.exactText(visibleText))\n          .click();\n    }\n\n    public String getCurrentValue() {\n      return selectElement.$(\"li\").getText();\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicDetails.java",
    "content": "package com.provectus.kafka.ui.pages.topics;\n\nimport static com.codeborne.selenide.Selenide.$$x;\nimport static com.codeborne.selenide.Selenide.$x;\nimport static com.codeborne.selenide.Selenide.sleep;\nimport static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.OVERVIEW;\nimport static org.testcontainers.shaded.org.apache.commons.lang3.RandomUtils.nextInt;\n\nimport com.codeborne.selenide.CollectionCondition;\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.ElementsCollection;\nimport com.codeborne.selenide.SelenideElement;\nimport com.provectus.kafka.ui.pages.BasePage;\nimport io.qameta.allure.Step;\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.LocalTime;\nimport java.time.YearMonth;\nimport java.time.format.DateTimeFormatter;\nimport java.time.format.DateTimeFormatterBuilder;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Objects;\n\npublic class TopicDetails extends BasePage {\n\n  protected SelenideElement clearMessagesBtn = $x((\"//div[contains(text(), 'Clear messages')]\"));\n  protected SelenideElement recreateTopicBtn = $x(\"//div[text()='Recreate Topic']\");\n  protected SelenideElement messageAmountCell = $x(\"//tbody/tr/td[5]\");\n  protected SelenideElement overviewTab = $x(\"//a[contains(text(),'Overview')]\");\n  protected SelenideElement messagesTab = $x(\"//a[contains(text(),'Messages')]\");\n  protected SelenideElement seekTypeDdl = $x(\"//ul[@id='selectSeekType']//li\");\n  protected SelenideElement seekTypeField = $x(\"//label[text()='Seek Type']//..//div/input\");\n  protected SelenideElement addFiltersBtn = $x(\"//button[text()='Add Filters']\");\n  protected SelenideElement savedFiltersLink = $x(\"//div[text()='Saved Filters']\");\n  protected SelenideElement addFilterCodeModalTitle = $x(\"//label[text()='Filter code']\");\n  protected SelenideElement addFilterCodeEditor = $x(\"//div[@id='ace-editor']\");\n  protected SelenideElement addFilterCodeTextarea = $x(\"//div[@id='ace-editor']//textarea\");\n  protected SelenideElement saveThisFilterCheckBoxAddFilterMdl = $x(\"//input[@name='saveFilter']\");\n  protected SelenideElement displayNameInputAddFilterMdl = $x(\"//input[@placeholder='Enter Name']\");\n  protected SelenideElement cancelBtnAddFilterMdl = $x(\"//button[text()='Cancel']\");\n  protected SelenideElement addFilterBtnAddFilterMdl = $x(\"//button[text()='Add filter']\");\n  protected SelenideElement saveFilterBtnEditFilterMdl = $x(\"//button[text()='Save']\");\n  protected SelenideElement addFiltersBtnMessages = $x(\"//button[text()='Add Filters']\");\n  protected SelenideElement selectFilterBtnAddFilterMdl = $x(\"//button[text()='Select filter']\");\n  protected SelenideElement editSettingsMenu = $x(\"//li[@role][contains(text(),'Edit settings')]\");\n  protected SelenideElement removeTopicBtn = $x(\"//ul[@role='menu']//div[contains(text(),'Remove Topic')]\");\n  protected SelenideElement produceMessageBtn = $x(\"//div//button[text()='Produce Message']\");\n  protected SelenideElement contentMessageTab = $x(\"//html//div[@id='root']/div/main//table//p\");\n  protected SelenideElement cleanUpPolicyField = $x(\"//div[contains(text(),'Clean Up Policy')]/../span/*\");\n  protected SelenideElement partitionsField = $x(\"//div[contains(text(),'Partitions')]/../span\");\n  protected SelenideElement backToCreateFiltersLink = $x(\"//div[text()='Back To create filters']\");\n  protected ElementsCollection messageGridItems = $$x(\"//tbody//tr\");\n  protected SelenideElement actualCalendarDate = $x(\"//div[@class='react-datepicker__current-month']\");\n  protected SelenideElement previousMonthButton = $x(\"//button[@aria-label='Previous Month']\");\n  protected SelenideElement nextMonthButton = $x(\"//button[@aria-label='Next Month']\");\n  protected SelenideElement calendarTimeFld = $x(\"//input[@placeholder='Time']\");\n  protected String detailsTabLtr = \"//nav//a[contains(text(),'%s')]\";\n  protected String dayCellLtr = \"//div[@role='option'][contains(text(),'%d')]\";\n  protected String seekFilterDdlLocator = \"//ul[@id='selectSeekType']/ul/li[text()='%s']\";\n  protected String savedFilterNameLocator = \"//div[@role='savedFilter']/div[contains(text(),'%s')]\";\n  protected String consumerIdLocator = \"//a[@title='%s']\";\n  protected String topicHeaderLocator = \"//h1[contains(text(),'%s')]\";\n  protected String activeFilterNameLocator = \"//div[@data-testid='activeSmartFilter']/div[1][contains(text(),'%s')]\";\n  protected String editActiveFilterBtnLocator = \"//div[text()='%s']/../div[@data-testid='editActiveSmartFilterBtn']\";\n  protected String settingsGridValueLocator = \"//tbody/tr/td/span[text()='%s']//ancestor::tr/td[2]/span\";\n\n  @Step\n  public TopicDetails waitUntilScreenReady() {\n    waitUntilSpinnerDisappear();\n    $x(String.format(detailsTabLtr, OVERVIEW)).shouldBe(Condition.visible);\n    return this;\n  }\n\n  @Step\n  public TopicDetails openDetailsTab(TopicMenu menu) {\n    $x(String.format(detailsTabLtr, menu.toString())).shouldBe(Condition.enabled).click();\n    waitUntilSpinnerDisappear();\n    return this;\n  }\n\n  @Step\n  public String getSettingsGridValueByKey(String key) {\n    return $x(String.format(settingsGridValueLocator, key)).scrollTo().shouldBe(Condition.visible).getText();\n  }\n\n  @Step\n  public TopicDetails openDotMenu() {\n    clickByJavaScript(dotMenuBtn);\n    return this;\n  }\n\n  @Step\n  public boolean isAlertWithMessageVisible(AlertHeader header, String message) {\n    return isAlertVisible(header, message);\n  }\n\n  @Step\n  public TopicDetails clickEditSettingsMenu() {\n    editSettingsMenu.shouldBe(Condition.visible).click();\n    return this;\n  }\n\n  @Step\n  public boolean isConfirmationMdlVisible() {\n    return isConfirmationModalVisible();\n  }\n\n  @Step\n  public TopicDetails clickClearMessagesMenu() {\n    clearMessagesBtn.shouldBe(Condition.visible).click();\n    return this;\n  }\n\n  @Step\n  public boolean isClearMessagesMenuEnabled() {\n    return !Objects.requireNonNull(clearMessagesBtn.shouldBe(Condition.visible)\n            .$x(\"./..\").getAttribute(\"class\"))\n        .contains(\"disabled\");\n  }\n\n  @Step\n  public TopicDetails clickRecreateTopicMenu() {\n    recreateTopicBtn.shouldBe(Condition.visible).click();\n    return this;\n  }\n\n  @Step\n  public String getCleanUpPolicy() {\n    return cleanUpPolicyField.getText();\n  }\n\n  @Step\n  public int getPartitions() {\n    return Integer.parseInt(partitionsField.getText().trim());\n  }\n\n  @Step\n  public boolean isTopicHeaderVisible(String topicName) {\n    return isVisible($x(String.format(topicHeaderLocator, topicName)));\n  }\n\n  @Step\n  public TopicDetails clickDeleteTopicMenu() {\n    removeTopicBtn.shouldBe(Condition.visible).click();\n    return this;\n  }\n\n  @Step\n  public TopicDetails clickConfirmBtnMdl() {\n    clickConfirmButton();\n    return this;\n  }\n\n  @Step\n  public TopicDetails clickProduceMessageBtn() {\n    clickByJavaScript(produceMessageBtn);\n    return this;\n  }\n\n  @Step\n  public TopicDetails selectSeekTypeDdlMessagesTab(String seekTypeName) {\n    seekTypeDdl.shouldBe(Condition.enabled).click();\n    $x(String.format(seekFilterDdlLocator, seekTypeName)).shouldBe(Condition.visible).click();\n    return this;\n  }\n\n  @Step\n  public TopicDetails setSeekTypeValueFldMessagesTab(String seekTypeValue) {\n    seekTypeField.shouldBe(Condition.enabled).sendKeys(seekTypeValue);\n    return this;\n  }\n\n  @Step\n  public TopicDetails clickSubmitFiltersBtnMessagesTab() {\n    clickByJavaScript(submitBtn);\n    waitUntilSpinnerDisappear();\n    return this;\n  }\n\n  @Step\n  public TopicDetails clickMessagesAddFiltersBtn() {\n    addFiltersBtn.shouldBe(Condition.enabled).click();\n    return this;\n  }\n\n  @Step\n  public TopicDetails clickEditActiveFilterBtn(String filterName) {\n    $x(String.format(editActiveFilterBtnLocator, filterName))\n        .shouldBe(Condition.enabled).click();\n    return this;\n  }\n\n  @Step\n  public TopicDetails clickNextButton() {\n    clickNextBtn();\n    waitUntilSpinnerDisappear();\n    return this;\n  }\n\n  @Step\n  public TopicDetails openSavedFiltersListMdl() {\n    savedFiltersLink.shouldBe(Condition.enabled).click();\n    backToCreateFiltersLink.shouldBe(Condition.visible);\n    return this;\n  }\n\n  @Step\n  public boolean isFilterVisibleAtSavedFiltersMdl(String filterName) {\n    return isVisible($x(String.format(savedFilterNameLocator, filterName)));\n  }\n\n  @Step\n  public TopicDetails selectFilterAtSavedFiltersMdl(String filterName) {\n    $x(String.format(savedFilterNameLocator, filterName)).shouldBe(Condition.enabled).click();\n    return this;\n  }\n\n  @Step\n  public TopicDetails clickSelectFilterBtnAtSavedFiltersMdl() {\n    selectFilterBtnAddFilterMdl.shouldBe(Condition.enabled).click();\n    addFilterCodeModalTitle.shouldBe(Condition.disappear);\n    return this;\n  }\n\n  @Step\n  public TopicDetails waitUntilAddFiltersMdlVisible() {\n    addFilterCodeModalTitle.shouldBe(Condition.visible);\n    return this;\n  }\n\n  @Step\n  public TopicDetails setFilterCodeFldAddFilterMdl(String filterCode) {\n    addFilterCodeTextarea.shouldBe(Condition.enabled).setValue(filterCode);\n    return this;\n  }\n\n  @Step\n  public String getFilterCodeValue() {\n    addFilterCodeEditor.shouldBe(Condition.enabled).click();\n    String value = addFilterCodeTextarea.getValue();\n    if (value == null) {\n      return null;\n    } else {\n      return value.substring(0, value.length() - 2);\n    }\n  }\n\n  @Step\n  public String getFilterNameValue() {\n    return displayNameInputAddFilterMdl.shouldBe(Condition.enabled).getValue();\n  }\n\n  @Step\n  public TopicDetails selectSaveThisFilterCheckboxMdl(boolean select) {\n    selectElement(saveThisFilterCheckBoxAddFilterMdl, select);\n    return this;\n  }\n\n  @Step\n  public boolean isSaveThisFilterCheckBoxSelected() {\n    return isSelected(saveThisFilterCheckBoxAddFilterMdl);\n  }\n\n  @Step\n  public TopicDetails setDisplayNameFldAddFilterMdl(String displayName) {\n    displayNameInputAddFilterMdl.shouldBe(Condition.enabled).setValue(displayName);\n    return this;\n  }\n\n  @Step\n  public TopicDetails clickAddFilterBtnAndCloseMdl(boolean closeModal) {\n    addFilterBtnAddFilterMdl.shouldBe(Condition.enabled).click();\n    if (closeModal) {\n      addFilterCodeModalTitle.shouldBe(Condition.hidden);\n    } else {\n      addFilterCodeModalTitle.shouldBe(Condition.visible);\n    }\n    return this;\n  }\n\n  @Step\n  public TopicDetails clickSaveFilterBtnAndCloseMdl(boolean closeModal) {\n    saveFilterBtnEditFilterMdl.shouldBe(Condition.enabled).click();\n    if (closeModal) {\n      addFilterCodeModalTitle.shouldBe(Condition.hidden);\n    } else {\n      addFilterCodeModalTitle.shouldBe(Condition.visible);\n    }\n    return this;\n  }\n\n  @Step\n  public boolean isAddFilterBtnAddFilterMdlEnabled() {\n    return isEnabled(addFilterBtnAddFilterMdl);\n  }\n\n  @Step\n  public boolean isBackButtonEnabled() {\n    return isEnabled(backBtn);\n  }\n\n  @Step\n  public boolean isNextButtonEnabled() {\n    return isEnabled(nextBtn);\n  }\n\n  @Step\n  public boolean isActiveFilterVisible(String filterName) {\n    return isVisible($x(String.format(activeFilterNameLocator, filterName)));\n  }\n\n  @Step\n  public String getSearchFieldValue() {\n    return searchFld.shouldBe(Condition.visible).getValue();\n  }\n\n  public List<SelenideElement> getAllAddFilterModalVisibleElements() {\n    return Arrays.asList(savedFiltersLink, displayNameInputAddFilterMdl, addFilterBtnAddFilterMdl,\n        cancelBtnAddFilterMdl);\n  }\n\n  public List<SelenideElement> getAllAddFilterModalEnabledElements() {\n    return Arrays.asList(displayNameInputAddFilterMdl, cancelBtnAddFilterMdl);\n  }\n\n  public List<SelenideElement> getAllAddFilterModalDisabledElements() {\n    return Collections.singletonList(addFilterBtnAddFilterMdl);\n  }\n\n  @Step\n  public TopicDetails openConsumerGroup(String consumerId) {\n    $x(String.format(consumerIdLocator, consumerId)).click();\n    return this;\n  }\n\n  private void selectYear(int expectedYear) {\n    while (getActualCalendarDate().getYear() > expectedYear) {\n      clickByJavaScript(previousMonthButton);\n      sleep(1000);\n      if (LocalTime.now().plusMinutes(3).isBefore(LocalTime.now())) {\n        throw new IllegalArgumentException(\"Unable to select year\");\n      }\n    }\n  }\n\n  private void selectMonth(int expectedMonth) {\n    while (getActualCalendarDate().getMonthValue() > expectedMonth) {\n      clickByJavaScript(previousMonthButton);\n      sleep(1000);\n      if (LocalTime.now().plusMinutes(3).isBefore(LocalTime.now())) {\n        throw new IllegalArgumentException(\"Unable to select month\");\n      }\n    }\n  }\n\n  private void selectDay(int expectedDay) {\n    Objects.requireNonNull($$x(String.format(dayCellLtr, expectedDay)).stream()\n        .filter(day -> !Objects.requireNonNull(day.getAttribute(\"class\")).contains(\"outside-month\"))\n        .findFirst().orElseThrow()).shouldBe(Condition.enabled).click();\n  }\n\n  private void setTime(LocalDateTime dateTime) {\n    calendarTimeFld.shouldBe(Condition.enabled)\n        .sendKeys(String.valueOf(dateTime.getHour()), String.valueOf(dateTime.getMinute()));\n  }\n\n  @Step\n  public TopicDetails selectDateAndTimeByCalendar(LocalDateTime dateTime) {\n    setTime(dateTime);\n    selectYear(dateTime.getYear());\n    selectMonth(dateTime.getMonthValue());\n    selectDay(dateTime.getDayOfMonth());\n    return this;\n  }\n\n  private LocalDate getActualCalendarDate() {\n    String monthAndYearStr = actualCalendarDate.getText().trim();\n    DateTimeFormatter formatter = new DateTimeFormatterBuilder()\n        .parseCaseInsensitive()\n        .append(DateTimeFormatter.ofPattern(\"MMMM yyyy\"))\n        .toFormatter(Locale.ENGLISH);\n    YearMonth yearMonth = formatter.parse(monthAndYearStr, YearMonth::from);\n    return yearMonth.atDay(1);\n  }\n\n  @Step\n  public TopicDetails openCalendarSeekType() {\n    seekTypeField.shouldBe(Condition.enabled).click();\n    actualCalendarDate.shouldBe(Condition.visible);\n    return this;\n  }\n\n  @Step\n  public int getMessageCountAmount() {\n    return Integer.parseInt(messageAmountCell.getText().trim());\n  }\n\n  private List<TopicDetails.MessageGridItem> initItems() {\n    List<TopicDetails.MessageGridItem> gridItemList = new ArrayList<>();\n    gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0))\n        .forEach(item -> gridItemList.add(new TopicDetails.MessageGridItem(item)));\n    return gridItemList;\n  }\n\n  @Step\n  public TopicDetails.MessageGridItem getMessageByOffset(int offset) {\n    return initItems().stream()\n        .filter(e -> e.getOffset() == offset)\n        .findFirst().orElseThrow();\n  }\n\n  @Step\n  public TopicDetails.MessageGridItem getMessageByKey(String key) {\n    return initItems().stream()\n        .filter(e -> e.getKey().equals(key))\n        .findFirst().orElseThrow();\n  }\n\n  @Step\n  public List<MessageGridItem> getAllMessages() {\n    return initItems();\n  }\n\n  @Step\n  public TopicDetails.MessageGridItem getRandomMessage() {\n    return getMessageByOffset(nextInt(0, initItems().size() - 1));\n  }\n\n  public enum TopicMenu {\n    OVERVIEW(\"Overview\"),\n    MESSAGES(\"Messages\"),\n    CONSUMERS(\"Consumers\"),\n    SETTINGS(\"Settings\");\n\n    private final String value;\n\n    TopicMenu(String value) {\n      this.value = value;\n    }\n\n    public String toString() {\n      return value;\n    }\n  }\n\n  public static class MessageGridItem extends BasePage {\n\n    private final SelenideElement element;\n\n    private MessageGridItem(SelenideElement element) {\n      this.element = element;\n    }\n\n    @Step\n    public MessageGridItem clickExpand() {\n      clickByJavaScript(element.$x(\"./td[1]/span\"));\n      return this;\n    }\n\n    private SelenideElement getOffsetElm() {\n      return element.$x(\"./td[2]\");\n    }\n\n    @Step\n    public int getOffset() {\n      return Integer.parseInt(getOffsetElm().getText().trim());\n    }\n\n    @Step\n    public int getPartition() {\n      return Integer.parseInt(element.$x(\"./td[3]\").getText().trim());\n    }\n\n    @Step\n    public LocalDateTime getTimestamp() {\n      String timestampValue = element.$x(\"./td[4]/div\").getText().trim();\n      DateTimeFormatter formatter = DateTimeFormatter.ofPattern(\"M/d/yyyy, HH:mm:ss\");\n      return LocalDateTime.parse(timestampValue, formatter);\n    }\n\n    @Step\n    public String getKey() {\n      return element.$x(\"./td[5]\").getText().trim();\n    }\n\n    @Step\n    public String getValue() {\n      return element.$x(\"./td[6]\").getAttribute(\"title\");\n    }\n\n    @Step\n    public MessageGridItem openDotMenu() {\n      getOffsetElm().hover();\n      element.$x(\"./td[7]/div/button[@aria-label='Dropdown Toggle']\")\n          .shouldBe(Condition.visible).click();\n      return this;\n    }\n\n    @Step\n    public MessageGridItem clickCopyToClipBoard() {\n      clickByJavaScript(element.$x(\"./td[7]//li[text() = 'Copy to clipboard']\")\n          .shouldBe(Condition.visible));\n      return this;\n    }\n\n    @Step\n    public MessageGridItem clickSaveAsFile() {\n      clickByJavaScript(element.$x(\"./td[7]//li[text() = 'Save as a file']\")\n          .shouldBe(Condition.visible));\n      return this;\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicSettingsTab.java",
    "content": "package com.provectus.kafka.ui.pages.topics;\n\nimport static com.codeborne.selenide.Selenide.$x;\n\nimport com.codeborne.selenide.CollectionCondition;\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.SelenideElement;\nimport com.provectus.kafka.ui.pages.BasePage;\nimport io.qameta.allure.Step;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class TopicSettingsTab extends BasePage {\n\n  protected SelenideElement defaultValueColumnHeaderLocator = $x(\"//div[text() = 'Default Value']\");\n\n  @Step\n  public TopicSettingsTab waitUntilScreenReady() {\n    waitUntilSpinnerDisappear();\n    defaultValueColumnHeaderLocator.shouldBe(Condition.visible);\n    return this;\n  }\n\n  private List<SettingsGridItem> initGridItems() {\n    List<SettingsGridItem> gridItemList = new ArrayList<>();\n    gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0))\n        .forEach(item -> gridItemList.add(new SettingsGridItem(item)));\n    return gridItemList;\n  }\n\n  private TopicSettingsTab.SettingsGridItem getItemByKey(String key) {\n    return initGridItems().stream()\n        .filter(e -> e.getKey().equals(key))\n        .findFirst().orElseThrow();\n  }\n\n  @Step\n  public String getValueByKey(String key) {\n    return getItemByKey(key).getValue();\n  }\n\n  public static class SettingsGridItem extends BasePage {\n\n    private final SelenideElement element;\n\n    public SettingsGridItem(SelenideElement element) {\n      this.element = element;\n    }\n\n    @Step\n    public String getKey() {\n      return element.$x(\"./td[1]/span\").getText().trim();\n    }\n\n    @Step\n    public String getValue() {\n      return element.$x(\"./td[2]/span\").getText().trim();\n    }\n\n    @Step\n    public String getDefaultValue() {\n      return element.$x(\"./td[3]/span\").getText().trim();\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/TopicsList.java",
    "content": "package com.provectus.kafka.ui.pages.topics;\n\nimport static com.codeborne.selenide.Condition.visible;\nimport static com.codeborne.selenide.Selenide.$x;\nimport static com.provectus.kafka.ui.pages.panels.enums.MenuItem.TOPICS;\n\nimport com.codeborne.selenide.CollectionCondition;\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.SelenideElement;\nimport com.provectus.kafka.ui.pages.BasePage;\nimport io.qameta.allure.Step;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class TopicsList extends BasePage {\n\n  protected SelenideElement addTopicBtn = $x(\"//button[normalize-space(text()) ='Add a Topic']\");\n  protected SelenideElement searchField = $x(\"//input[@placeholder='Search by Topic Name']\");\n  protected SelenideElement showInternalRadioBtn = $x(\"//input[@name='ShowInternalTopics']\");\n  protected SelenideElement deleteSelectedTopicsBtn = $x(\"//button[text()='Delete selected topics']\");\n  protected SelenideElement copySelectedTopicBtn = $x(\"//button[text()='Copy selected topic']\");\n  protected SelenideElement purgeMessagesOfSelectedTopicsBtn =\n      $x(\"//button[text()='Purge messages of selected topics']\");\n  protected SelenideElement clearMessagesBtn = $x(\"//ul[contains(@class ,'open')]//div[text()='Clear Messages']\");\n  protected SelenideElement recreateTopicBtn = $x(\"//ul[contains(@class ,'open')]//div[text()='Recreate Topic']\");\n  protected SelenideElement removeTopicBtn = $x(\"//ul[contains(@class ,'open')]//div[text()='Remove Topic']\");\n\n  @Step\n  public TopicsList waitUntilScreenReady() {\n    waitUntilSpinnerDisappear();\n    getPageTitleFromHeader(TOPICS).shouldBe(visible);\n    return this;\n  }\n\n  @Step\n  public TopicsList clickAddTopicBtn() {\n    clickByJavaScript(addTopicBtn);\n    return this;\n  }\n\n  @Step\n  public boolean isTopicVisible(String topicName) {\n    tableGrid.shouldBe(visible);\n    return isVisible(getTableElement(topicName));\n  }\n\n  @Step\n  public boolean isShowInternalRadioBtnSelected() {\n    return isSelected(showInternalRadioBtn);\n  }\n\n  @Step\n  public TopicsList setShowInternalRadioButton(boolean select) {\n    if (select) {\n      if (!showInternalRadioBtn.isSelected()) {\n        clickByJavaScript(showInternalRadioBtn);\n        waitUntilSpinnerDisappear(1);\n      }\n    } else {\n      if (showInternalRadioBtn.isSelected()) {\n        clickByJavaScript(showInternalRadioBtn);\n        waitUntilSpinnerDisappear(1);\n      }\n    }\n    return this;\n  }\n\n  @Step\n  public TopicsList goToLastPage() {\n    if (nextBtn.exists()) {\n      while (nextBtn.isEnabled()) {\n        clickNextBtn();\n        waitUntilSpinnerDisappear(1);\n      }\n    }\n    return this;\n  }\n\n  @Step\n  public TopicsList openTopic(String topicName) {\n    getTopicItem(topicName).openItem();\n    return this;\n  }\n\n  @Step\n  public TopicsList openDotMenuByTopicName(String topicName) {\n    getTopicItem(topicName).openDotMenu();\n    return this;\n  }\n\n  @Step\n  public boolean isCopySelectedTopicBtnEnabled() {\n    return isEnabled(copySelectedTopicBtn);\n  }\n\n  @Step\n  public List<SelenideElement> getActionButtons() {\n    return Stream.of(deleteSelectedTopicsBtn, copySelectedTopicBtn, purgeMessagesOfSelectedTopicsBtn)\n        .collect(Collectors.toList());\n  }\n\n  @Step\n  public TopicsList clickCopySelectedTopicBtn() {\n    copySelectedTopicBtn.shouldBe(Condition.enabled).click();\n    return this;\n  }\n\n  @Step\n  public TopicsList clickPurgeMessagesOfSelectedTopicsBtn() {\n    purgeMessagesOfSelectedTopicsBtn.shouldBe(Condition.enabled).click();\n    return this;\n  }\n\n  @Step\n  public TopicsList clickClearMessagesBtn() {\n    clickByJavaScript(clearMessagesBtn.shouldBe(visible));\n    return this;\n  }\n\n  @Step\n  public TopicsList clickRecreateTopicBtn() {\n    clickByJavaScript(recreateTopicBtn.shouldBe(visible));\n    return this;\n  }\n\n  @Step\n  public TopicsList clickRemoveTopicBtn() {\n    clickByJavaScript(removeTopicBtn.shouldBe(visible));\n    return this;\n  }\n\n  @Step\n  public TopicsList clickConfirmBtnMdl() {\n    clickConfirmButton();\n    return this;\n  }\n\n  @Step\n  public TopicsList clickCancelBtnMdl() {\n    clickCancelButton();\n    return this;\n  }\n\n  @Step\n  public boolean isConfirmationMdlVisible() {\n    return isConfirmationModalVisible();\n  }\n\n  @Step\n  public boolean isAlertWithMessageVisible(AlertHeader header, String message) {\n    return isAlertVisible(header, message);\n  }\n\n  private List<SelenideElement> getVisibleColumnHeaders() {\n    return Stream.of(\"Replication Factor\", \"Number of messages\", \"Topic Name\", \"Partitions\", \"Out of sync replicas\",\n            \"Size\")\n        .map(name -> $x(String.format(columnHeaderLocator, name)))\n        .collect(Collectors.toList());\n  }\n\n  private List<SelenideElement> getEnabledColumnHeaders() {\n    return Stream.of(\"Topic Name\", \"Partitions\", \"Out of sync replicas\", \"Size\")\n        .map(name -> $x(String.format(columnHeaderLocator, name)))\n        .collect(Collectors.toList());\n  }\n\n  @Step\n  public List<SelenideElement> getAllVisibleElements() {\n    List<SelenideElement> visibleElements = new ArrayList<>(getVisibleColumnHeaders());\n    visibleElements.addAll(Arrays.asList(searchField, addTopicBtn, tableGrid));\n    visibleElements.addAll(getActionButtons());\n    return visibleElements;\n  }\n\n  @Step\n  public List<SelenideElement> getAllEnabledElements() {\n    List<SelenideElement> enabledElements = new ArrayList<>(getEnabledColumnHeaders());\n    enabledElements.addAll(Arrays.asList(searchField, showInternalRadioBtn, addTopicBtn));\n    return enabledElements;\n  }\n\n  private List<TopicGridItem> initGridItems() {\n    List<TopicGridItem> gridItemList = new ArrayList<>();\n    gridItems.shouldHave(CollectionCondition.sizeGreaterThan(0))\n        .forEach(item -> gridItemList.add(new TopicGridItem(item)));\n    return gridItemList;\n  }\n\n  @Step\n  public TopicGridItem getTopicItem(String name) {\n    TopicGridItem topicGridItem = initGridItems().stream()\n        .filter(e -> e.getName().equals(name))\n        .findFirst().orElse(null);\n    if (topicGridItem == null) {\n      searchItem(name);\n      topicGridItem = initGridItems().stream()\n          .filter(e -> e.getName().equals(name))\n          .findFirst().orElseThrow();\n    }\n    return topicGridItem;\n  }\n\n  @Step\n  public TopicGridItem getAnyNonInternalTopic() {\n    return getNonInternalTopics().stream()\n        .findAny().orElseThrow();\n  }\n\n  @Step\n  public List<TopicGridItem> getNonInternalTopics() {\n    return initGridItems().stream()\n        .filter(e -> !e.isInternal())\n        .collect(Collectors.toList());\n  }\n\n  @Step\n  public List<TopicGridItem> getInternalTopics() {\n    return initGridItems().stream()\n        .filter(TopicGridItem::isInternal)\n        .collect(Collectors.toList());\n  }\n\n  public static class TopicGridItem extends BasePage {\n\n    private final SelenideElement element;\n\n    public TopicGridItem(SelenideElement element) {\n      this.element = element;\n    }\n\n    @Step\n    public TopicsList selectItem(boolean select) {\n      selectElement(element.$x(\"./td[1]/input\"), select);\n      return new TopicsList();\n    }\n\n    private SelenideElement getNameElm() {\n      return element.$x(\"./td[2]\");\n    }\n\n    @Step\n    public boolean isInternal() {\n      boolean internal = false;\n      try {\n        internal = getNameElm().$x(\"./a/span\").isDisplayed();\n      } catch (Throwable ignored) {\n      }\n      return internal;\n    }\n\n    @Step\n    public String getName() {\n      return getNameElm().$x(\"./a\").getAttribute(\"title\");\n    }\n\n    @Step\n    public void openItem() {\n      getNameElm().click();\n    }\n\n    @Step\n    public int getPartition() {\n      return Integer.parseInt(element.$x(\"./td[3]\").getText().trim());\n    }\n\n    @Step\n    public int getOutOfSyncReplicas() {\n      return Integer.parseInt(element.$x(\"./td[4]\").getText().trim());\n    }\n\n    @Step\n    public int getReplicationFactor() {\n      return Integer.parseInt(element.$x(\"./td[5]\").getText().trim());\n    }\n\n    @Step\n    public int getNumberOfMessages() {\n      return Integer.parseInt(element.$x(\"./td[6]\").getText().trim());\n    }\n\n    @Step\n    public int getSize() {\n      return Integer.parseInt(element.$x(\"./td[7]\").getText().trim());\n    }\n\n    @Step\n    public void openDotMenu() {\n      element.$x(\"./td[8]//button\").click();\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CleanupPolicyValue.java",
    "content": "package com.provectus.kafka.ui.pages.topics.enums;\n\npublic enum CleanupPolicyValue {\n\n  DELETE(\"delete\", \"Delete\"),\n  COMPACT(\"compact\", \"Compact\"),\n  COMPACT_DELETE(\"compact,delete\", \"Compact,Delete\");\n\n  private final String optionValue;\n  private final String visibleText;\n\n  CleanupPolicyValue(String optionValue, String visibleText) {\n    this.optionValue = optionValue;\n    this.visibleText = visibleText;\n  }\n\n  public String getOptionValue() {\n    return optionValue;\n  }\n\n  public String getVisibleText() {\n    return visibleText;\n  }\n}\n\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/CustomParameterType.java",
    "content": "package com.provectus.kafka.ui.pages.topics.enums;\n\npublic enum CustomParameterType {\n\n  COMPRESSION_TYPE(\"compression.type\"),\n  DELETE_RETENTION_MS(\"delete.retention.ms\"),\n  FILE_DELETE_DELAY_MS(\"file.delete.delay.ms\"),\n  FLUSH_MESSAGES(\"flush.messages\"),\n  FLUSH_MS(\"flush.ms\"),\n  FOLLOWER_REPLICATION_THROTTLED_REPLICAS(\"follower.replication.throttled.replicas\"),\n  INDEX_INTERVAL_BYTES(\"index.interval.bytes\"),\n  LEADER_REPLICATION_THROTTLED_REPLICAS(\"leader.replication.throttled.replicas\"),\n  MAX_COMPACTION_LAG_MS(\"max.compaction.lag.ms\"),\n  MESSAGE_DOWNCONVERSION_ENABLE(\"message.downconversion.enable\"),\n  MESSAGE_FORMAT_VERSION(\"message.format.version\"),\n  MESSAGE_TIMESTAMP_DIFFERENCE_MAX_MS(\"message.timestamp.difference.max.ms\"),\n  MESSAGE_TIMESTAMP_TYPE(\"message.timestamp.type\"),\n  MIN_CLEANABLE_DIRTY_RATIO(\"min.cleanable.dirty.ratio\"),\n  MIN_COMPACTION_LAG_MS(\"min.compaction.lag.ms\"),\n  PREALLOCATE(\"preallocate\"),\n  RETENTION_BYTES(\"retention.bytes\"),\n  SEGMENT_BYTES(\"segment.bytes\"),\n  SEGMENT_INDEX_BYTES(\"segment.index.bytes\"),\n  SEGMENT_JITTER_MS(\"segment.jitter.ms\"),\n  SEGMENT_MS(\"segment.ms\"),\n  UNCLEAN_LEADER_ELECTION_ENABLE(\"unclean.leader.election.enable\");\n\n  private final String optionValue;\n\n  CustomParameterType(String optionValue) {\n    this.optionValue = optionValue;\n  }\n\n  public String getOptionValue() {\n    return optionValue;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/MaxSizeOnDisk.java",
    "content": "package com.provectus.kafka.ui.pages.topics.enums;\n\npublic enum MaxSizeOnDisk {\n\n  NOT_SET(\"-1\", \"Not Set\"),\n  SIZE_1_GB(\"1073741824\", \"1 GB\"),\n  SIZE_10_GB(\"10737418240\", \"10 GB\"),\n  SIZE_20_GB(\"21474836480\", \"20 GB\"),\n  SIZE_50_GB(\"53687091200\", \"50 GB\");\n\n  private final String optionValue;\n  private final String visibleText;\n\n  MaxSizeOnDisk(String optionValue, String visibleText) {\n    this.optionValue = optionValue;\n    this.visibleText = visibleText;\n  }\n\n  public String getOptionValue() {\n    return optionValue;\n  }\n\n  public String getVisibleText() {\n    return visibleText;\n  }\n}\n\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/enums/TimeToRetain.java",
    "content": "package com.provectus.kafka.ui.pages.topics.enums;\n\npublic enum TimeToRetain {\n\n  BTN_12_HOURS(\"12 hours\", \"43200000\"),\n  BTN_1_DAY(\"1 day\", \"86400000\"),\n  BTN_2_DAYS(\"2 days\", \"172800000\"),\n  BTN_7_DAYS(\"7 days\", \"604800000\"),\n  BTN_4_WEEKS(\"4 weeks\", \"2419200000\");\n\n  private final String button;\n  private final String value;\n\n  TimeToRetain(String button, String value) {\n    this.button = button;\n    this.value = value;\n  }\n\n  public String getButton() {\n    return button;\n  }\n\n  public String getValue() {\n    return value;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java",
    "content": "package com.provectus.kafka.ui.services;\n\nimport static com.codeborne.selenide.Selenide.sleep;\nimport static com.provectus.kafka.ui.utilities.FileUtils.fileToString;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.provectus.kafka.ui.api.ApiClient;\nimport com.provectus.kafka.ui.api.api.KafkaConnectApi;\nimport com.provectus.kafka.ui.api.api.KsqlApi;\nimport com.provectus.kafka.ui.api.api.MessagesApi;\nimport com.provectus.kafka.ui.api.api.SchemasApi;\nimport com.provectus.kafka.ui.api.api.TopicsApi;\nimport com.provectus.kafka.ui.api.model.CreateTopicMessage;\nimport com.provectus.kafka.ui.api.model.KsqlCommandV2;\nimport com.provectus.kafka.ui.api.model.KsqlCommandV2Response;\nimport com.provectus.kafka.ui.api.model.KsqlResponse;\nimport com.provectus.kafka.ui.api.model.NewConnector;\nimport com.provectus.kafka.ui.api.model.NewSchemaSubject;\nimport com.provectus.kafka.ui.api.model.TopicCreation;\nimport com.provectus.kafka.ui.models.Connector;\nimport com.provectus.kafka.ui.models.Schema;\nimport com.provectus.kafka.ui.models.Topic;\nimport com.provectus.kafka.ui.pages.ksqldb.models.Stream;\nimport com.provectus.kafka.ui.pages.ksqldb.models.Table;\nimport com.provectus.kafka.ui.settings.BaseSource;\nimport io.qameta.allure.Step;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport lombok.SneakyThrows;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.web.reactive.function.client.WebClientResponseException;\n\n\n@Slf4j\npublic class ApiService extends BaseSource {\n\n  private final ApiClient apiClient = new ApiClient().setBasePath(BASE_API_URL);\n\n  @SneakyThrows\n  private TopicsApi topicApi() {\n    return new TopicsApi(apiClient);\n  }\n\n  @SneakyThrows\n  private SchemasApi schemaApi() {\n    return new SchemasApi(apiClient);\n  }\n\n  @SneakyThrows\n  private KafkaConnectApi connectorApi() {\n    return new KafkaConnectApi(apiClient);\n  }\n\n  @SneakyThrows\n  private MessagesApi messageApi() {\n    return new MessagesApi(apiClient);\n  }\n\n  @SneakyThrows\n  private KsqlApi ksqlApi() {\n    return new KsqlApi(apiClient);\n  }\n\n  @SneakyThrows\n  private void createTopic(String clusterName, String topicName) {\n    TopicCreation topic = new TopicCreation();\n    topic.setName(topicName);\n    topic.setPartitions(1);\n    topic.setReplicationFactor(1);\n    try {\n      topicApi().createTopic(clusterName, topic).block();\n      sleep(2000);\n    } catch (WebClientResponseException ex) {\n      ex.printStackTrace();\n    }\n  }\n\n  @Step\n  public ApiService createTopic(Topic topic) {\n    createTopic(CLUSTER_NAME, topic.getName());\n    return this;\n  }\n\n  @SneakyThrows\n  private void deleteTopic(String clusterName, String topicName) {\n    try {\n      topicApi().deleteTopic(clusterName, topicName).block();\n    } catch (WebClientResponseException ignored) {\n    }\n  }\n\n  @Step\n  public ApiService deleteTopic(String topicName) {\n    deleteTopic(CLUSTER_NAME, topicName);\n    return this;\n  }\n\n  @SneakyThrows\n  private void createSchema(String clusterName, Schema schema) {\n    NewSchemaSubject schemaSubject = new NewSchemaSubject();\n    schemaSubject.setSubject(schema.getName());\n    schemaSubject.setSchema(fileToString(schema.getValuePath()));\n    schemaSubject.setSchemaType(schema.getType());\n    try {\n      schemaApi().createNewSchema(clusterName, schemaSubject).block();\n    } catch (WebClientResponseException ex) {\n      ex.printStackTrace();\n    }\n  }\n\n  @Step\n  public ApiService createSchema(Schema schema) {\n    createSchema(CLUSTER_NAME, schema);\n    return this;\n  }\n\n  @SneakyThrows\n  private void deleteSchema(String clusterName, String schemaName) {\n    try {\n      schemaApi().deleteSchema(clusterName, schemaName).block();\n    } catch (WebClientResponseException ignored) {\n    }\n  }\n\n  @Step\n  public ApiService deleteSchema(String schemaName) {\n    deleteSchema(CLUSTER_NAME, schemaName);\n    return this;\n  }\n\n  @SneakyThrows\n  private void deleteConnector(String clusterName, String connectName, String connectorName) {\n    try {\n      connectorApi().deleteConnector(clusterName, connectName, connectorName).block();\n    } catch (WebClientResponseException ignored) {\n    }\n  }\n\n  @Step\n  public ApiService deleteConnector(String connectName, String connectorName) {\n    deleteConnector(CLUSTER_NAME, connectName, connectorName);\n    return this;\n  }\n\n  @Step\n  public ApiService deleteConnector(String connectorName) {\n    deleteConnector(CLUSTER_NAME, CONNECT_NAME, connectorName);\n    return this;\n  }\n\n  @SneakyThrows\n  private void createConnector(String clusterName, String connectName, Connector connector) {\n    NewConnector connectorProperties = new NewConnector();\n    connectorProperties.setName(connector.getName());\n    Map<String, Object> configMap = new ObjectMapper().readValue(connector.getConfig(), HashMap.class);\n    connectorProperties.setConfig(configMap);\n    try {\n      connectorApi().deleteConnector(clusterName, connectName, connector.getName()).block();\n    } catch (WebClientResponseException ignored) {\n    }\n    connectorApi().createConnector(clusterName, connectName, connectorProperties).block();\n  }\n\n  @Step\n  public ApiService createConnector(String connectName, Connector connector) {\n    createConnector(CLUSTER_NAME, connectName, connector);\n    return this;\n  }\n\n  @Step\n  public ApiService createConnector(Connector connector) {\n    createConnector(CLUSTER_NAME, CONNECT_NAME, connector);\n    return this;\n  }\n\n  @Step\n  public String getFirstConnectName(String clusterName) {\n    return Objects.requireNonNull(connectorApi().getConnects(clusterName).blockFirst()).getName();\n  }\n\n  @SneakyThrows\n  private void sendMessage(String clusterName, Topic topic) {\n    CreateTopicMessage createMessage = new CreateTopicMessage();\n    createMessage.setPartition(0);\n    createMessage.setKeySerde(\"String\");\n    createMessage.setValueSerde(\"String\");\n    createMessage.setKey(topic.getMessageKey());\n    createMessage.setContent(topic.getMessageValue());\n    try {\n      messageApi().sendTopicMessages(clusterName, topic.getName(), createMessage).block();\n    } catch (WebClientResponseException ex) {\n      ex.getRawStatusCode();\n    }\n  }\n\n  @Step\n  public ApiService sendMessage(Topic topic) {\n    sendMessage(CLUSTER_NAME, topic);\n    return this;\n  }\n\n  @Step\n  public ApiService createStream(Stream stream) {\n    KsqlCommandV2Response pipeIdStream = ksqlApi()\n        .executeKsql(CLUSTER_NAME, new KsqlCommandV2()\n            .ksql(String.format(\"CREATE STREAM %s (profileId VARCHAR, latitude DOUBLE, longitude DOUBLE) \",\n                stream.getName())\n                + String.format(\"WITH (kafka_topic='%s', value_format='json', partitions=1);\",\n                stream.getTopicName())))\n        .block();\n    assert pipeIdStream != null;\n    List<KsqlResponse> responseListStream = ksqlApi()\n        .openKsqlResponsePipe(CLUSTER_NAME, pipeIdStream.getPipeId())\n        .collectList()\n        .block();\n    assert Objects.requireNonNull(responseListStream).size() != 0;\n    return this;\n  }\n\n  @Step\n  public ApiService createTables(Table firstTable, Table secondTable) {\n    KsqlCommandV2Response pipeIdTable1 = ksqlApi()\n        .executeKsql(CLUSTER_NAME, new KsqlCommandV2()\n            .ksql(String.format(\"CREATE TABLE %s AS \", firstTable.getName())\n                + \"  SELECT profileId, \"\n                + \"         LATEST_BY_OFFSET(latitude) AS la, \"\n                + \"         LATEST_BY_OFFSET(longitude) AS lo \"\n                + String.format(\"  FROM %s \", firstTable.getStreamName())\n                + \"  GROUP BY profileId \"\n                + \"  EMIT CHANGES;\"))\n        .block();\n    assert pipeIdTable1 != null;\n    List<KsqlResponse> responseListTable = ksqlApi()\n        .openKsqlResponsePipe(CLUSTER_NAME, pipeIdTable1.getPipeId())\n        .collectList()\n        .block();\n    assert Objects.requireNonNull(responseListTable).size() != 0;\n    KsqlCommandV2Response pipeIdTable2 = ksqlApi()\n        .executeKsql(CLUSTER_NAME, new KsqlCommandV2()\n            .ksql(String.format(\"CREATE TABLE %s AS \", secondTable.getName())\n                + \"  SELECT ROUND(GEO_DISTANCE(la, lo, 37.4133, -122.1162), -1) AS distanceInMiles, \"\n                + \"         COLLECT_LIST(profileId) AS riders, \"\n                + \"         COUNT(*) AS count \"\n                + String.format(\"  FROM %s \", firstTable.getName())\n                + \"  GROUP BY ROUND(GEO_DISTANCE(la, lo, 37.4133, -122.1162), -1);\"))\n        .block();\n    assert pipeIdTable2 != null;\n    List<KsqlResponse> responseListTable2 = ksqlApi()\n        .openKsqlResponsePipe(CLUSTER_NAME, pipeIdTable2.getPipeId())\n        .collectList()\n        .block();\n    assert Objects.requireNonNull(responseListTable2).size() != 0;\n    return this;\n  }\n\n  @Step\n  public ApiService insertInto(Stream stream) {\n    String streamName = stream.getName();\n    KsqlCommandV2Response pipeIdInsert = ksqlApi()\n        .executeKsql(CLUSTER_NAME, new KsqlCommandV2()\n            .ksql(\"INSERT INTO \" + streamName\n                + \" (profileId, latitude, longitude) VALUES ('c2309eec', 37.7877, -122.4205);\"\n                + \"INSERT INTO \" + streamName\n                + \" (profileId, latitude, longitude) VALUES ('18f4ea86', 37.3903, -122.0643); \"\n                + \"INSERT INTO \" + streamName\n                + \" (profileId, latitude, longitude) VALUES ('4ab5cbad', 37.3952, -122.0813); \"\n                + \"INSERT INTO \" + streamName\n                + \" (profileId, latitude, longitude) VALUES ('8b6eae59', 37.3944, -122.0813); \"\n                + \"INSERT INTO \" + streamName\n                + \" (profileId, latitude, longitude) VALUES ('4a7c7b41', 37.4049, -122.0822); \"\n                + \"INSERT INTO \" + streamName\n                + \" (profileId, latitude, longitude) VALUES ('4ddad000', 37.7857, -122.4011);\"))\n        .block();\n    assert pipeIdInsert != null;\n    List<KsqlResponse> responseListInsert = ksqlApi()\n        .openKsqlResponsePipe(CLUSTER_NAME, pipeIdInsert.getPipeId())\n        .collectList()\n        .block();\n    assert Objects.requireNonNull(responseListInsert).size() != 0;\n    return this;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/BaseSource.java",
    "content": "package com.provectus.kafka.ui.settings;\n\nimport static com.provectus.kafka.ui.variables.Browser.LOCAL;\n\nimport com.provectus.kafka.ui.settings.configs.Config;\nimport org.aeonbits.owner.ConfigFactory;\n\npublic abstract class BaseSource {\n\n  public static final String CLUSTER_NAME = \"local\";\n  public static final String CONNECT_NAME = \"first\";\n  private static final String LOCAL_HOST = \"localhost\";\n  public static final String REMOTE_URL = String.format(\"http://%s:4444/wd/hub\", LOCAL_HOST);\n  public static final String BASE_API_URL = String.format(\"http://%s:8080\", LOCAL_HOST);\n  private static Config config;\n  public static final String BROWSER = config().browser();\n  public static final String BASE_HOST = BROWSER.equals(LOCAL)\n      ? LOCAL_HOST\n      : \"host.docker.internal\";\n  public static final String BASE_UI_URL = String.format(\"http://%s:8080\", BASE_HOST);\n  public static final String SUITE_NAME = config().suite();\n\n  private static Config config() {\n    if (config == null) {\n      config = ConfigFactory.create(Config.class, System.getProperties());\n    }\n    return config;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/configs/Config.java",
    "content": "package com.provectus.kafka.ui.settings.configs;\n\npublic interface Config extends Profiles {\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/configs/Profiles.java",
    "content": "package com.provectus.kafka.ui.settings.configs;\n\nimport static com.provectus.kafka.ui.variables.Browser.CONTAINER;\nimport static com.provectus.kafka.ui.variables.Suite.CUSTOM;\n\nimport org.aeonbits.owner.Config;\n\npublic interface Profiles extends Config {\n\n  @Key(\"browser\")\n  @DefaultValue(CONTAINER)\n  String browser();\n\n  @Key(\"suite\")\n  @DefaultValue(CUSTOM)\n  String suite();\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/drivers/WebDriver.java",
    "content": "package com.provectus.kafka.ui.settings.drivers;\n\nimport static com.codeborne.selenide.Selenide.clearBrowserCookies;\nimport static com.codeborne.selenide.Selenide.clearBrowserLocalStorage;\nimport static com.codeborne.selenide.Selenide.refresh;\nimport static com.provectus.kafka.ui.settings.BaseSource.BROWSER;\nimport static com.provectus.kafka.ui.settings.BaseSource.REMOTE_URL;\nimport static com.provectus.kafka.ui.variables.Browser.CONTAINER;\nimport static com.provectus.kafka.ui.variables.Browser.LOCAL;\n\nimport com.codeborne.selenide.Configuration;\nimport com.codeborne.selenide.Selenide;\nimport com.codeborne.selenide.WebDriverRunner;\nimport com.codeborne.selenide.logevents.SelenideLogger;\nimport io.qameta.allure.Step;\nimport io.qameta.allure.selenide.AllureSelenide;\nimport java.util.HashMap;\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\nimport org.openqa.selenium.chrome.ChromeOptions;\n\n@Slf4j\npublic abstract class WebDriver {\n\n  @Step\n  public static void browserSetup() {\n    Configuration.headless = false;\n    Configuration.browser = \"chrome\";\n    Configuration.browserSize = \"1920x1080\";\n    Configuration.screenshots = true;\n    Configuration.savePageSource = false;\n    Configuration.pageLoadTimeout = 120000;\n    ChromeOptions chromeOptions = new ChromeOptions()\n        .addArguments(\"--no-sandbox\")\n        .addArguments(\"--verbose\")\n        .addArguments(\"--remote-allow-origins=*\")\n        .addArguments(\"--disable-dev-shm-usage\")\n        .addArguments(\"--disable-gpu\")\n        .addArguments(\"--lang=en_US\");\n    switch (BROWSER) {\n      case (LOCAL) -> Configuration.browserCapabilities = chromeOptions;\n      case (CONTAINER) -> {\n        Configuration.remote = REMOTE_URL;\n        Configuration.remoteConnectionTimeout = 180000;\n        Map<String, Object> selenoidOptions = new HashMap<>();\n        selenoidOptions.put(\"enableVNC\", true);\n        selenoidOptions.put(\"enableVideo\", false);\n        chromeOptions.setCapability(\"selenoid:options\", selenoidOptions);\n        Configuration.browserCapabilities = chromeOptions;\n      }\n      default -> throw new IllegalStateException(\"Unexpected value: \" + BROWSER);\n    }\n  }\n\n  private static org.openqa.selenium.WebDriver getWebDriver() {\n    try {\n      return WebDriverRunner.getWebDriver();\n    } catch (IllegalStateException ex) {\n      browserSetup();\n      Selenide.open();\n      return WebDriverRunner.getWebDriver();\n    }\n  }\n\n  @Step\n  public static void openUrl(String url) {\n    org.openqa.selenium.WebDriver driver = getWebDriver();\n    if (!driver.getCurrentUrl().equals(url)) {\n      driver.get(url);\n    }\n  }\n\n  @Step\n  public static void browserInit() {\n    getWebDriver();\n  }\n\n  @Step\n  public static void browserClear() {\n    clearBrowserLocalStorage();\n    clearBrowserCookies();\n    refresh();\n  }\n\n  @Step\n  public static void browserQuit() {\n    org.openqa.selenium.WebDriver driver = null;\n    try {\n      driver = WebDriverRunner.getWebDriver();\n    } catch (Throwable ignored) {\n    }\n    if (driver != null) {\n      driver.quit();\n    }\n  }\n\n  @Step\n  public static void loggerSetup() {\n    SelenideLogger.addListener(\"AllureSelenide\", new AllureSelenide()\n        .screenshots(true)\n        .savePageSource(false));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/AllureListener.java",
    "content": "package com.provectus.kafka.ui.settings.listeners;\n\nimport static java.nio.file.Files.newInputStream;\n\nimport com.codeborne.selenide.Screenshots;\nimport io.qameta.allure.Allure;\nimport io.qameta.allure.testng.AllureTestNg;\nimport java.io.File;\nimport java.io.IOException;\nimport lombok.extern.slf4j.Slf4j;\nimport org.testng.ITestListener;\nimport org.testng.ITestResult;\n\n@Slf4j\npublic class AllureListener extends AllureTestNg implements ITestListener {\n\n  private void takeScreenshot() {\n    File screenshot = Screenshots.takeScreenShotAsFile();\n    try {\n      if (screenshot != null) {\n        Allure.addAttachment(screenshot.getName(), newInputStream(screenshot.toPath()));\n      } else {\n        log.warn(\"Unable to take screenshot\");\n      }\n    } catch (IOException e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  @Override\n  public void onTestFailure(ITestResult result) {\n    takeScreenshot();\n  }\n\n  @Override\n  public void onTestSkipped(ITestResult result) {\n    takeScreenshot();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/LoggerListener.java",
    "content": "package com.provectus.kafka.ui.settings.listeners;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.testng.ITestResult;\nimport org.testng.TestListenerAdapter;\n\n@Slf4j\npublic class LoggerListener extends TestListenerAdapter {\n\n  @Override\n  public void onTestStart(final ITestResult testResult) {\n    log.info(String.format(\"\\n------------------------------------------------------------------------ \"\n            + \"\\nTEST STARTED: %s.%s \\n------------------------------------------------------------------------ \\n\",\n        testResult.getInstanceName(), testResult.getName()));\n  }\n\n  @Override\n  public void onTestSuccess(final ITestResult testResult) {\n    log.info(String.format(\"\\n------------------------------------------------------------------------ \"\n            + \"\\nTEST PASSED: %s.%s \\n------------------------------------------------------------------------ \\n\",\n        testResult.getInstanceName(), testResult.getName()));\n  }\n\n  @Override\n  public void onTestFailure(final ITestResult testResult) {\n    log.info(String.format(\"\\n------------------------------------------------------------------------ \"\n            + \"\\nTEST FAILED: %s.%s \\n------------------------------------------------------------------------ \\n\",\n        testResult.getInstanceName(), testResult.getName()));\n  }\n\n  @Override\n  public void onTestSkipped(final ITestResult testResult) {\n    log.info(String.format(\"\\n------------------------------------------------------------------------ \"\n            + \"\\nTEST SKIPPED: %s.%s \\n------------------------------------------------------------------------ \\n\",\n        testResult.getInstanceName(), testResult.getName()));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseCreateListener.java",
    "content": "package com.provectus.kafka.ui.settings.listeners;\n\nimport static io.qase.api.utils.IntegrationUtils.getCaseTitle;\n\nimport com.provectus.kafka.ui.utilities.qase.annotations.Automation;\nimport com.provectus.kafka.ui.utilities.qase.annotations.Status;\nimport com.provectus.kafka.ui.utilities.qase.annotations.Suite;\nimport io.qase.api.QaseClient;\nimport io.qase.api.StepStorage;\nimport io.qase.api.annotation.QaseId;\nimport io.qase.client.ApiClient;\nimport io.qase.client.api.CasesApi;\nimport io.qase.client.model.GetCasesFiltersParameter;\nimport io.qase.client.model.ResultCreateStepsInner;\nimport io.qase.client.model.TestCase;\nimport io.qase.client.model.TestCaseCreate;\nimport io.qase.client.model.TestCaseCreateStepsInner;\nimport io.qase.client.model.TestCaseListResponse;\nimport io.qase.client.model.TestCaseListResponseAllOfResult;\nimport java.lang.reflect.Method;\nimport java.util.HashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport lombok.SneakyThrows;\nimport lombok.extern.slf4j.Slf4j;\nimport org.testng.Assert;\nimport org.testng.ITestListener;\nimport org.testng.ITestResult;\nimport org.testng.TestListenerAdapter;\n\n@Slf4j\npublic class QaseCreateListener extends TestListenerAdapter implements ITestListener {\n\n  private static final CasesApi QASE_API = getQaseApi();\n\n  private static CasesApi getQaseApi() {\n    ApiClient apiClient = QaseClient.getApiClient();\n    apiClient.setApiKey(System.getProperty(\"QASEIO_API_TOKEN\"));\n    return new CasesApi(apiClient);\n  }\n\n  private static int getStatus(Method method) {\n    if (method.isAnnotationPresent(Status.class)) {\n      return method.getDeclaredAnnotation(Status.class).status().getValue();\n    }\n    return 1;\n  }\n\n  private static int getAutomation(Method method) {\n    if (method.isAnnotationPresent(Automation.class)) {\n      return method.getDeclaredAnnotation(Automation.class).state().getValue();\n    }\n    return 0;\n  }\n\n  @SneakyThrows\n  private static HashMap<Long, String> getCaseTitlesAndIdsFromQase() {\n    HashMap<Long, String> cases = new HashMap<>();\n    boolean getCases = true;\n    int offSet = 0;\n    while (getCases) {\n      getCases = false;\n      TestCaseListResponse response = QASE_API.getCases(System.getProperty(\"QASE_PROJECT_CODE\"),\n          new GetCasesFiltersParameter().status(GetCasesFiltersParameter.SERIALIZED_NAME_STATUS), 100, offSet);\n      TestCaseListResponseAllOfResult result = response.getResult();\n      Assert.assertNotNull(result);\n      List<TestCase> entities = result.getEntities();\n      Assert.assertNotNull(entities);\n      if (entities.size() > 0) {\n        for (TestCase testCase : entities) {\n          cases.put(testCase.getId(), testCase.getTitle());\n        }\n        offSet = offSet + 100;\n        getCases = true;\n      }\n    }\n    return cases;\n  }\n\n  private static boolean isCaseWithTitleExistInQase(Method method) {\n    HashMap<Long, String> cases = getCaseTitlesAndIdsFromQase();\n    String title = getCaseTitle(method);\n    if (cases.containsValue(title)) {\n      for (Map.Entry<Long, String> map : cases.entrySet()) {\n        if (map.getValue().matches(title)) {\n          long id = map.getKey();\n          log.warn(String.format(\"Test case with @QaseTitle='%s' already exists with @QaseId=%d. \"\n              + \"Please verify @QaseTitle annotation\", title, id));\n          return true;\n        }\n      }\n    }\n    return false;\n  }\n\n  @Override\n  @SneakyThrows\n  public void onTestSuccess(final ITestResult testResult) {\n    Method method = testResult.getMethod()\n        .getConstructorOrMethod()\n        .getMethod();\n    String title = getCaseTitle(method);\n    if (!method.isAnnotationPresent(QaseId.class)) {\n      if (title != null) {\n        if (!isCaseWithTitleExistInQase(method)) {\n          LinkedList<ResultCreateStepsInner> resultSteps = StepStorage.stopSteps();\n          LinkedList<TestCaseCreateStepsInner> createSteps = new LinkedList<>();\n          resultSteps.forEach(step -> {\n            TestCaseCreateStepsInner caseStep = new TestCaseCreateStepsInner();\n            caseStep.setAction(step.getAction());\n            caseStep.setExpectedResult(step.getExpectedResult());\n            createSteps.add(caseStep);\n          });\n          TestCaseCreate newCase = new TestCaseCreate();\n          newCase.setTitle(title);\n          newCase.setStatus(getStatus(method));\n          newCase.setAutomation(getAutomation(method));\n          newCase.setSteps(createSteps);\n          if (method.isAnnotationPresent(Suite.class)) {\n            long suiteId = method.getDeclaredAnnotation(Suite.class).id();\n            newCase.suiteId(suiteId);\n          }\n          Long id = Objects.requireNonNull(QASE_API.createCase(System.getProperty(\"QASE_PROJECT_CODE\"),\n              newCase).getResult()).getId();\n          log.info(String.format(\"New test case '%s' was created with @QaseId=%d\", title, id));\n        }\n      } else {\n        log.warn(\"To create new test case in Qase.io please add @QaseTitle annotation\");\n      }\n    } else {\n      log.warn(\"To create new test case in Qase.io please remove @QaseId annotation\");\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/listeners/QaseResultListener.java",
    "content": "package com.provectus.kafka.ui.settings.listeners;\n\nimport static io.qase.api.utils.IntegrationUtils.getCaseId;\nimport static io.qase.api.utils.IntegrationUtils.getCaseTitle;\nimport static io.qase.api.utils.IntegrationUtils.getStacktrace;\nimport static io.qase.client.model.ResultCreate.StatusEnum.FAILED;\nimport static io.qase.client.model.ResultCreate.StatusEnum.PASSED;\nimport static io.qase.client.model.ResultCreate.StatusEnum.SKIPPED;\n\nimport io.qase.api.StepStorage;\nimport io.qase.api.config.QaseConfig;\nimport io.qase.api.services.QaseTestCaseListener;\nimport io.qase.client.model.ResultCreate;\nimport io.qase.client.model.ResultCreateCase;\nimport io.qase.client.model.ResultCreateStepsInner;\nimport io.qase.testng.guice.module.TestNgModule;\nimport java.lang.reflect.Method;\nimport java.util.LinkedList;\nimport java.util.Optional;\nimport lombok.AccessLevel;\nimport lombok.Getter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.testng.ITestContext;\nimport org.testng.ITestListener;\nimport org.testng.ITestResult;\nimport org.testng.TestListenerAdapter;\n\n@Slf4j\npublic class QaseResultListener extends TestListenerAdapter implements ITestListener {\n\n  private static final String REPORTER_NAME = \"TestNG\";\n\n  static {\n    System.setProperty(QaseConfig.QASE_CLIENT_REPORTER_NAME_KEY, REPORTER_NAME);\n  }\n\n  @Getter(lazy = true, value = AccessLevel.PRIVATE)\n  private final QaseTestCaseListener qaseTestCaseListener = createQaseListener();\n\n  private static QaseTestCaseListener createQaseListener() {\n    return TestNgModule.getInjector().getInstance(QaseTestCaseListener.class);\n  }\n\n  @Override\n  public void onTestStart(ITestResult tr) {\n    getQaseTestCaseListener().onTestCaseStarted();\n    super.onTestStart(tr);\n  }\n\n  @Override\n  public void onTestSuccess(ITestResult tr) {\n    getQaseTestCaseListener()\n        .onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, PASSED));\n    super.onTestSuccess(tr);\n  }\n\n  @Override\n  public void onTestSkipped(ITestResult tr) {\n    getQaseTestCaseListener()\n        .onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, SKIPPED));\n    super.onTestSuccess(tr);\n  }\n\n  @Override\n  public void onTestFailure(ITestResult tr) {\n    getQaseTestCaseListener()\n        .onTestCaseFinished(resultCreate -> setupResultItem(resultCreate, tr, FAILED));\n    super.onTestFailure(tr);\n  }\n\n  @Override\n  public void onFinish(ITestContext testContext) {\n    getQaseTestCaseListener().onTestCasesSetFinished();\n    super.onFinish(testContext);\n  }\n\n  private void setupResultItem(ResultCreate resultCreate, ITestResult result, ResultCreate.StatusEnum status) {\n    Optional<Throwable> resultThrowable = Optional.ofNullable(result.getThrowable());\n    String comment = resultThrowable\n        .flatMap(throwable -> Optional.of(throwable.toString())).orElse(null);\n    Boolean isDefect = resultThrowable\n        .flatMap(throwable -> Optional.of(throwable instanceof AssertionError))\n        .orElse(false);\n    String stacktrace = resultThrowable\n        .flatMap(throwable -> Optional.of(getStacktrace(throwable)))\n        .orElse(null);\n    Method method = result.getMethod()\n        .getConstructorOrMethod()\n        .getMethod();\n    Long caseId = getCaseId(method);\n    String caseTitle = null;\n    if (caseId == null) {\n      caseTitle = getCaseTitle(method);\n    }\n    LinkedList<ResultCreateStepsInner> steps = StepStorage.stopSteps();\n    resultCreate\n        ._case(caseTitle == null ? null : new ResultCreateCase().title(caseTitle))\n        .caseId(caseId)\n        .status(status)\n        .comment(comment)\n        .stacktrace(stacktrace)\n        .steps(steps.isEmpty() ? null : steps)\n        .defect(isDefect);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/FileUtils.java",
    "content": "package com.provectus.kafka.ui.utilities;\n\nimport static org.apache.kafka.common.utils.Utils.readFileAsString;\n\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport org.testcontainers.shaded.org.apache.commons.io.IOUtils;\n\npublic class FileUtils {\n\n  public static String getResourceAsString(String resourceFileName) {\n    try {\n      return IOUtils.resourceToString(\"/\" + resourceFileName, StandardCharsets.UTF_8);\n    } catch (IOException e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  public static String fileToString(String path) {\n    try {\n      return readFileAsString(path);\n    } catch (IOException e) {\n      throw new RuntimeException(e);\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/StringUtils.java",
    "content": "package com.provectus.kafka.ui.utilities;\n\nimport java.util.stream.IntStream;\nimport lombok.extern.slf4j.Slf4j;\n\n@Slf4j\npublic class StringUtils {\n\n  public static String getMixedCase(String original) {\n    return IntStream.range(0, original.length())\n        .mapToObj(i -> i % 2 == 0 ? Character.toUpperCase(original.charAt(i)) : original.charAt(i))\n        .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)\n        .toString();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/TimeUtils.java",
    "content": "package com.provectus.kafka.ui.utilities;\n\nimport static com.codeborne.selenide.Selenide.sleep;\n\nimport java.time.LocalTime;\nimport lombok.extern.slf4j.Slf4j;\n\n@Slf4j\npublic class TimeUtils {\n\n  public static void waitUntilNewMinuteStarted() {\n    int secondsLeft = 60 - LocalTime.now().getSecond();\n    log.debug(\"\\nwaitUntilNewMinuteStarted: {}s\", secondsLeft);\n    sleep(secondsLeft * 1000);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/WebUtils.java",
    "content": "package com.provectus.kafka.ui.utilities;\n\nimport static com.codeborne.selenide.Selenide.executeJavaScript;\n\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.SelenideElement;\nimport com.codeborne.selenide.WebDriverRunner;\nimport java.time.Duration;\nimport lombok.extern.slf4j.Slf4j;\nimport org.openqa.selenium.Keys;\nimport org.openqa.selenium.interactions.Actions;\n\n@Slf4j\npublic class WebUtils {\n\n  public static int getTimeout(int... timeoutInSeconds) {\n    return (timeoutInSeconds != null && timeoutInSeconds.length > 0) ? timeoutInSeconds[0] : 4;\n  }\n\n  public static void sendKeysAfterClear(SelenideElement element, String keys) {\n    log.debug(\"\\nsendKeysAfterClear: {} \\nsend keys '{}'\", element.getSearchCriteria(), keys);\n    element.shouldBe(Condition.enabled).clear();\n    if (keys != null) {\n      element.sendKeys(keys);\n    }\n  }\n\n  public static void clickByActions(SelenideElement element) {\n    log.debug(\"\\nclickByActions: {}\", element.getSearchCriteria());\n    element.shouldBe(Condition.enabled);\n    new Actions(WebDriverRunner.getWebDriver())\n        .moveToElement(element)\n        .click(element)\n        .perform();\n  }\n\n  public static void sendKeysByActions(SelenideElement element, String keys) {\n    log.debug(\"\\nsendKeysByActions: {} \\nsend keys '{}'\", element.getSearchCriteria(), keys);\n    element.shouldBe(Condition.enabled);\n    new Actions(WebDriverRunner.getWebDriver())\n        .moveToElement(element)\n        .sendKeys(element, keys)\n        .perform();\n  }\n\n  public static void clickByJavaScript(SelenideElement element) {\n    log.debug(\"\\nclickByJavaScript: {}\", element.getSearchCriteria());\n    element.shouldBe(Condition.enabled);\n    String script = \"arguments[0].click();\";\n    executeJavaScript(script, element);\n  }\n\n  public static void clearByKeyboard(SelenideElement field) {\n    log.debug(\"\\nclearByKeyboard: {}\", field.getSearchCriteria());\n    field.shouldBe(Condition.enabled).sendKeys(Keys.END);\n    field.sendKeys(Keys.chord(Keys.CONTROL + \"a\"), Keys.DELETE);\n  }\n\n  public static boolean isVisible(SelenideElement element, int... timeoutInSeconds) {\n    log.debug(\"\\nisVisible: {}\", element.getSearchCriteria());\n    boolean isVisible = false;\n    try {\n      element.shouldBe(Condition.visible,\n          Duration.ofSeconds(getTimeout(timeoutInSeconds)));\n      isVisible = true;\n    } catch (Throwable e) {\n      log.debug(\"{} is not visible\", element.getSearchCriteria());\n    }\n    return isVisible;\n  }\n\n  public static boolean isEnabled(SelenideElement element, int... timeoutInSeconds) {\n    log.debug(\"\\nisEnabled: {}\", element.getSearchCriteria());\n    boolean isEnabled = false;\n    try {\n      element.shouldBe(Condition.enabled,\n          Duration.ofSeconds(getTimeout(timeoutInSeconds)));\n      isEnabled = true;\n    } catch (Throwable e) {\n      log.debug(\"{} is not enabled\", element.getSearchCriteria());\n    }\n    return isEnabled;\n  }\n\n  public static boolean isSelected(SelenideElement element, int... timeoutInSeconds) {\n    log.debug(\"\\nisSelected: {}\", element.getSearchCriteria());\n    boolean isSelected = false;\n    try {\n      element.shouldBe(Condition.selected,\n          Duration.ofSeconds(getTimeout(timeoutInSeconds)));\n      isSelected = true;\n    } catch (Throwable e) {\n      log.debug(\"{} is not selected\", element.getSearchCriteria());\n    }\n    return isSelected;\n  }\n\n  public static void selectElement(SelenideElement element, boolean select) {\n    if (select) {\n      if (!element.isSelected()) {\n        clickByJavaScript(element);\n      }\n    } else {\n      if (element.isSelected()) {\n        clickByJavaScript(element);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/QaseSetup.java",
    "content": "package com.provectus.kafka.ui.utilities.qase;\n\nimport static com.provectus.kafka.ui.settings.BaseSource.SUITE_NAME;\nimport static com.provectus.kafka.ui.variables.Suite.MANUAL;\nimport static org.apache.commons.lang3.BooleanUtils.FALSE;\nimport static org.apache.commons.lang3.BooleanUtils.TRUE;\nimport static org.apache.commons.lang3.StringUtils.isEmpty;\n\nimport java.time.OffsetDateTime;\nimport java.time.ZoneOffset;\nimport java.time.format.DateTimeFormatter;\nimport lombok.extern.slf4j.Slf4j;\n\n@Slf4j\npublic class QaseSetup {\n\n  public static void qaseIntegrationSetup() {\n    String qaseApiToken = System.getProperty(\"QASEIO_API_TOKEN\");\n    if (isEmpty(qaseApiToken)) {\n      log.warn(\"Integration with Qase is disabled due to run config or token wasn't defined.\");\n      System.setProperty(\"QASE_ENABLE\", FALSE);\n    } else {\n      log.warn(\"Integration with Qase is enabled. Find this run at https://app.qase.io/run/KAFKAUI.\");\n      String automation = SUITE_NAME.equalsIgnoreCase(MANUAL) ? \"\" : \"Automation \";\n      System.setProperty(\"QASE_ENABLE\", TRUE);\n      System.setProperty(\"QASE_PROJECT_CODE\", \"KAFKAUI\");\n      System.setProperty(\"QASE_API_TOKEN\", qaseApiToken);\n      System.setProperty(\"QASE_USE_BULK\", TRUE);\n      System.setProperty(\"QASE_RUN_NAME\", DateTimeFormatter.ofPattern(\"dd.MM.yyyy HH:mm\")\n          .format(OffsetDateTime.now(ZoneOffset.UTC)) + \": \" + automation + SUITE_NAME.toUpperCase() + \" suite\");\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Automation.java",
    "content": "package com.provectus.kafka.ui.utilities.qase.annotations;\n\nimport com.provectus.kafka.ui.utilities.qase.enums.State;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Target(ElementType.METHOD)\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface Automation {\n\n  State state();\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Status.java",
    "content": "package com.provectus.kafka.ui.utilities.qase.annotations;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Target(ElementType.METHOD)\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface Status {\n\n  com.provectus.kafka.ui.utilities.qase.enums.Status status();\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/annotations/Suite.java",
    "content": "package com.provectus.kafka.ui.utilities.qase.annotations;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Target(ElementType.METHOD)\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface Suite {\n\n  long id();\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/enums/State.java",
    "content": "package com.provectus.kafka.ui.utilities.qase.enums;\n\npublic enum State {\n\n  NOT_AUTOMATED(0),\n  TO_BE_AUTOMATED(1),\n  AUTOMATED(2);\n\n  private final int value;\n\n  State(int value) {\n    this.value = value;\n  }\n\n  public int getValue() {\n    return value;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/qase/enums/Status.java",
    "content": "package com.provectus.kafka.ui.utilities.qase.enums;\n\npublic enum Status {\n\n  ACTUAL(0),\n  DRAFT(1),\n  DEPRECATED(2);\n\n  private final int value;\n\n  Status(int value) {\n    this.value = value;\n  }\n\n  public int getValue() {\n    return value;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Browser.java",
    "content": "package com.provectus.kafka.ui.variables;\n\npublic interface Browser {\n\n  String CONTAINER = \"container\";\n  String LOCAL = \"local\";\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Expected.java",
    "content": "package com.provectus.kafka.ui.variables;\n\npublic interface Expected {\n\n  String BROKER_SOURCE_INFO_TOOLTIP =\n      \"DYNAMIC_TOPIC_CONFIG = dynamic topic config that is configured for a specific topic\\n\"\n          + \"DYNAMIC_BROKER_LOGGER_CONFIG = dynamic broker logger config that is configured for a specific broker\\n\"\n          + \"DYNAMIC_BROKER_CONFIG = dynamic broker config that is configured for a specific broker\\n\"\n          + \"DYNAMIC_DEFAULT_BROKER_CONFIG = dynamic broker config that is configured as default \"\n          + \"for all brokers in the cluster\\n\"\n          + \"STATIC_BROKER_CONFIG = static broker config provided as broker properties at start up \"\n          + \"(e.g. server.properties file)\\n\"\n          + \"DEFAULT_CONFIG = built-in default configuration for configs that have a default value\\n\"\n          + \"UNKNOWN = source unknown e.g. in the ConfigEntry used for alter requests where source is not set\";\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Suite.java",
    "content": "package com.provectus.kafka.ui.variables;\n\npublic interface Suite {\n\n  String CUSTOM = \"custom\";\n  String MANUAL = \"manual\";\n  String REGRESSION = \"regression\";\n  String SANITY = \"sanity\";\n  String SMOKE = \"smoke\";\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/variables/Url.java",
    "content": "package com.provectus.kafka.ui.variables;\n\npublic interface Url {\n\n  String BROKERS_LIST_URL = \"http://%s:8080/ui/clusters/local/brokers\";\n  String TOPICS_LIST_URL = \"http://%s:8080/ui/clusters/local/all-topics\";\n  String CONSUMERS_LIST_URL = \"http://%s:8080/ui/clusters/local/consumer-groups\";\n  String SCHEMA_REGISTRY_LIST_URL = \"http://%s:8080/ui/clusters/local/schemas\";\n  String KAFKA_CONNECT_LIST_URL = \"http://%s:8080/ui/clusters/local/connectors\";\n  String KSQL_DB_LIST_URL = \"http://%s:8080/ui/clusters/local/ksqldb/tables\";\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/resources/allure.properties",
    "content": "allure.results.directory=allure-results\nallure.link.issue.pattern=https://github.com/provectus/kafka-ui/issues/{}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/resources/testData/connectors/config_for_create_connector.json",
    "content": "{\n  \"connector.class\": \"io.confluent.connect.jdbc.JdbcSinkConnector\",\n  \"connection.url\": \"jdbc:postgresql://postgres-db:5432/test\",\n  \"connection.user\": \"dev_user\",\n  \"connection.password\": \"12345\",\n  \"topics\": \"topic_for_connector\",\n  \"table.name.format\": \"sink_activities_e2e_test_connector_creating\",\n  \"key.converter\": \"org.apache.kafka.connect.storage.StringConverter\",\n  \"key.converter.schema.registry.url\": \"http://schemaregistry0:8085\",\n  \"value.converter\": \"org.apache.kafka.connect.json.JsonConverter\",\n  \"value.converter.schema.registry.url\": \"http://schemaregistry0:8085\",\n  \"auto.create\": \"true\",\n  \"pk.mode\": \"record_value\",\n  \"pk.fields\": \"id\",\n  \"insert.mode\": \"upsert\",\n  \"errors.log.enable\": \"true\",\n  \"errors.log.include.messages\": \"true\"\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/resources/testData/connectors/config_for_create_connector_via_api.json",
    "content": "{\n  \"connector.class\": \"io.confluent.connect.jdbc.JdbcSinkConnector\",\n  \"connection.url\": \"jdbc:postgresql://postgres-db:5432/test\",\n  \"connection.user\": \"dev_user\",\n  \"connection.password\": \"12345\",\n  \"topics\": \"topic_for_connector\"\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/resources/testData/connectors/config_for_update_connector.json",
    "content": "{\n  \"connector.class\": \"io.confluent.connect.jdbc.JdbcSinkConnector\",\n  \"connection.url\": \"jdbc:postgresql://postgres-db:5432/test\",\n  \"connection.user\": \"dev_user\",\n  \"connection.password\": \"12345\",\n  \"topics\": \"topic_for_update_connector\",\n  \"table.name.format\": \"sink_activities_e2e_test_connector_updating\",\n  \"key.converter\": \"org.apache.kafka.connect.storage.StringConverter\",\n  \"key.converter.schema.registry.url\": \"http://schemaregistry0:8085\",\n  \"value.converter\": \"org.apache.kafka.connect.json.JsonConverter\",\n  \"value.converter.schema.registry.url\": \"http://schemaregistry0:8085\",\n  \"auto.create\": \"true\",\n  \"pk.mode\": \"record_value\",\n  \"pk.fields\": \"id\",\n  \"insert.mode\": \"upsert\",\n  \"errors.log.enable\": \"true\",\n  \"errors.log.include.messages\": \"true\"\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/resources/testData/connectors/delete_connector_config.json",
    "content": "{\n  \"connector.class\": \"io.confluent.connect.jdbc.JdbcSinkConnector\",\n  \"connection.url\": \"jdbc:postgresql://postgres-db:5432/test\",\n  \"connection.user\": \"dev_user\",\n  \"connection.password\": \"12345\",\n  \"topics\": \"topic_for_delete_connector\",\n  \"table.name.format\": \"sink_activities_e2e_test_connector_deleting\",\n  \"key.converter\": \"org.apache.kafka.connect.storage.StringConverter\",\n  \"key.converter.schema.registry.url\": \"http://schemaregistry0:8085\",\n  \"value.converter\": \"org.apache.kafka.connect.json.JsonConverter\",\n  \"value.converter.schema.registry.url\": \"http://schemaregistry0:8085\",\n  \"auto.create\": \"false\",\n  \"pk.mode\": \"record_value\",\n  \"pk.fields\": \"id\",\n  \"insert.mode\": \"upsert\",\n  \"errors.log.enable\": \"true\",\n  \"errors.log.include.messages\": \"true\"\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_avro_for_update.json",
    "content": "{\n  \"type\": \"record\",\n  \"name\": \"Message\",\n  \"namespace\": \"com.provectus.kafka\",\n  \"fields\": [\n    {\n      \"name\": \"text\",\n      \"type\": \"string\",\n      \"default\": null\n    },\n    {\n      \"name\": \"value\",\n      \"type\": \"string\",\n      \"default\": null\n    }\n  ]\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_avro_value.json",
    "content": "{\n  \"type\": \"record\",\n  \"name\": \"Student\",\n  \"namespace\": \"DataFlair\",\n  \"fields\": [\n    {\n      \"name\": \"Name\",\n      \"type\": \"string\"\n    },\n    {\n      \"name\": \"Age\",\n      \"type\": \"int\"\n    }\n  ]\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_json_Value.json",
    "content": "{\n  \"connector.class\": \"io.confluent.connect.jdbc.JdbcSinkConnector\",\n  \"connection.url\": \"jdbc:postgresql://postgres-db:5432/test\",\n  \"connection.user\": \"dev_user\",\n  \"connection.password\": \"12345\",\n  \"topics\": \"topic_for_connector\"\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/resources/testData/schemas/schema_protobuf_value.txt",
    "content": "enum SchemaType {\n  AVRO = 0;\n  JSON = 1;\n  PROTOBUF = 2;\n  }"
  },
  {
    "path": "kafka-ui-e2e-checks/src/main/resources/testData/topics/message_content_create_topic.json",
    "content": "{\n  \"schema\":\n  {\n    \"type\":\"struct\",\n    \"fields\": [\n      {\n        \"type\":\"string\",\n        \"optional\":false,\n        \"field\":\"id\"\n      },{\n        \"type\":\"string\",\n        \"optional\":false,\n        \"field\":\"value\"\n      }\n    ],\n    \"optional\":false,\n    \"name\":\"test\"\n  },\n  \"payload\":\n  {\n    \"id\":\"1\",\n    \"value\":\"kafka\"\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/BaseTest.java",
    "content": "package com.provectus.kafka.ui;\n\nimport static com.provectus.kafka.ui.pages.panels.enums.MenuItem.BROKERS;\nimport static com.provectus.kafka.ui.pages.panels.enums.MenuItem.CONSUMERS;\nimport static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KAFKA_CONNECT;\nimport static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KSQL_DB;\nimport static com.provectus.kafka.ui.pages.panels.enums.MenuItem.SCHEMA_REGISTRY;\nimport static com.provectus.kafka.ui.pages.panels.enums.MenuItem.TOPICS;\nimport static com.provectus.kafka.ui.settings.BaseSource.BASE_UI_URL;\nimport static com.provectus.kafka.ui.settings.drivers.WebDriver.browserClear;\nimport static com.provectus.kafka.ui.settings.drivers.WebDriver.browserQuit;\nimport static com.provectus.kafka.ui.settings.drivers.WebDriver.browserSetup;\nimport static com.provectus.kafka.ui.settings.drivers.WebDriver.loggerSetup;\nimport static com.provectus.kafka.ui.utilities.qase.QaseSetup.qaseIntegrationSetup;\n\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.Selenide;\nimport com.codeborne.selenide.SelenideElement;\nimport com.provectus.kafka.ui.settings.listeners.AllureListener;\nimport com.provectus.kafka.ui.settings.listeners.LoggerListener;\nimport com.provectus.kafka.ui.settings.listeners.QaseResultListener;\nimport io.qameta.allure.Step;\nimport java.util.List;\nimport lombok.extern.slf4j.Slf4j;\nimport org.testng.annotations.AfterMethod;\nimport org.testng.annotations.AfterSuite;\nimport org.testng.annotations.BeforeMethod;\nimport org.testng.annotations.BeforeSuite;\nimport org.testng.annotations.Listeners;\nimport org.testng.asserts.SoftAssert;\n\n@Slf4j\n@Listeners({AllureListener.class, LoggerListener.class, QaseResultListener.class})\npublic abstract class BaseTest extends Facade {\n\n  @BeforeSuite(alwaysRun = true)\n  public void beforeSuite() {\n    qaseIntegrationSetup();\n    loggerSetup();\n    browserSetup();\n  }\n\n  @AfterSuite(alwaysRun = true)\n  public void afterSuite() {\n    browserQuit();\n  }\n\n  @BeforeMethod(alwaysRun = true)\n  public void beforeMethod() {\n    Selenide.open(BASE_UI_URL);\n    naviSideBar.waitUntilScreenReady();\n  }\n\n  @AfterMethod(alwaysRun = true)\n  public void afterMethod() {\n    browserClear();\n  }\n\n  @Step\n  protected void navigateToBrokers() {\n    naviSideBar\n        .openSideMenu(BROKERS);\n    brokersList\n        .waitUntilScreenReady();\n  }\n\n  @Step\n  protected void navigateToBrokersAndOpenDetails(int brokerId) {\n    naviSideBar\n        .openSideMenu(BROKERS);\n    brokersList\n        .waitUntilScreenReady()\n        .openBroker(brokerId);\n    brokersDetails\n        .waitUntilScreenReady();\n  }\n\n  @Step\n  protected void navigateToTopics() {\n    naviSideBar\n        .openSideMenu(TOPICS);\n    topicsList\n        .waitUntilScreenReady()\n        .setShowInternalRadioButton(false);\n  }\n\n  @Step\n  protected void navigateToTopicsAndOpenDetails(String topicName) {\n    navigateToTopics();\n    topicsList\n        .openTopic(topicName);\n    topicDetails\n        .waitUntilScreenReady();\n  }\n\n  @Step\n  protected void navigateToConsumers() {\n    naviSideBar\n        .openSideMenu(CONSUMERS);\n    consumersList\n        .waitUntilScreenReady();\n  }\n\n  @Step\n  protected void navigateToSchemaRegistry() {\n    naviSideBar\n        .openSideMenu(SCHEMA_REGISTRY);\n    schemaRegistryList\n        .waitUntilScreenReady();\n  }\n\n  @Step\n  protected void navigateToSchemaRegistryAndOpenDetails(String schemaName) {\n    navigateToSchemaRegistry();\n    schemaRegistryList\n        .openSchema(schemaName);\n    schemaDetails\n        .waitUntilScreenReady();\n  }\n\n  @Step\n  protected void navigateToConnectors() {\n    naviSideBar\n        .openSideMenu(KAFKA_CONNECT);\n    kafkaConnectList\n        .waitUntilScreenReady();\n  }\n\n  @Step\n  protected void navigateToConnectorsAndOpenDetails(String connectorName) {\n    navigateToConnectors();\n    kafkaConnectList\n        .openConnector(connectorName);\n    connectorDetails\n        .waitUntilScreenReady();\n  }\n\n  @Step\n  protected void navigateToKsqlDb() {\n    naviSideBar\n        .openSideMenu(KSQL_DB);\n    ksqlDbList\n        .waitUntilScreenReady();\n  }\n\n  @Step\n  protected void verifyElementsCondition(List<SelenideElement> elementList, Condition expectedCondition) {\n    SoftAssert softly = new SoftAssert();\n    elementList.forEach(element -> softly.assertTrue(element.is(expectedCondition),\n        element.getSearchCriteria() + \" is \" + expectedCondition));\n    softly.assertAll();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/Facade.java",
    "content": "package com.provectus.kafka.ui;\n\nimport com.provectus.kafka.ui.pages.brokers.BrokersConfigTab;\nimport com.provectus.kafka.ui.pages.brokers.BrokersDetails;\nimport com.provectus.kafka.ui.pages.brokers.BrokersList;\nimport com.provectus.kafka.ui.pages.connectors.ConnectorCreateForm;\nimport com.provectus.kafka.ui.pages.connectors.ConnectorDetails;\nimport com.provectus.kafka.ui.pages.connectors.KafkaConnectList;\nimport com.provectus.kafka.ui.pages.consumers.ConsumersDetails;\nimport com.provectus.kafka.ui.pages.consumers.ConsumersList;\nimport com.provectus.kafka.ui.pages.ksqldb.KsqlDbList;\nimport com.provectus.kafka.ui.pages.ksqldb.KsqlQueryForm;\nimport com.provectus.kafka.ui.pages.panels.NaviSideBar;\nimport com.provectus.kafka.ui.pages.panels.TopPanel;\nimport com.provectus.kafka.ui.pages.schemas.SchemaCreateForm;\nimport com.provectus.kafka.ui.pages.schemas.SchemaDetails;\nimport com.provectus.kafka.ui.pages.schemas.SchemaRegistryList;\nimport com.provectus.kafka.ui.pages.topics.ProduceMessagePanel;\nimport com.provectus.kafka.ui.pages.topics.TopicCreateEditForm;\nimport com.provectus.kafka.ui.pages.topics.TopicDetails;\nimport com.provectus.kafka.ui.pages.topics.TopicSettingsTab;\nimport com.provectus.kafka.ui.pages.topics.TopicsList;\nimport com.provectus.kafka.ui.services.ApiService;\n\npublic abstract class Facade {\n\n  protected ApiService apiService = new ApiService();\n  protected ConnectorCreateForm connectorCreateForm = new ConnectorCreateForm();\n  protected KafkaConnectList kafkaConnectList = new KafkaConnectList();\n  protected ConnectorDetails connectorDetails = new ConnectorDetails();\n  protected SchemaCreateForm schemaCreateForm = new SchemaCreateForm();\n  protected SchemaDetails schemaDetails = new SchemaDetails();\n  protected SchemaRegistryList schemaRegistryList = new SchemaRegistryList();\n  protected ProduceMessagePanel produceMessagePanel = new ProduceMessagePanel();\n  protected TopicCreateEditForm topicCreateEditForm = new TopicCreateEditForm();\n  protected TopicsList topicsList = new TopicsList();\n  protected TopicDetails topicDetails = new TopicDetails();\n  protected ConsumersDetails consumersDetails = new ConsumersDetails();\n  protected ConsumersList consumersList = new ConsumersList();\n  protected NaviSideBar naviSideBar = new NaviSideBar();\n  protected TopPanel topPanel = new TopPanel();\n  protected BrokersList brokersList = new BrokersList();\n  protected BrokersDetails brokersDetails = new BrokersDetails();\n  protected BrokersConfigTab brokersConfigTab = new BrokersConfigTab();\n  protected TopicSettingsTab topicSettingsTab = new TopicSettingsTab();\n  protected KsqlQueryForm ksqlQueryForm = new KsqlQueryForm();\n  protected KsqlDbList ksqlDbList = new KsqlDbList();\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/BaseManualTest.java",
    "content": "package com.provectus.kafka.ui.manualsuite;\n\nimport static com.provectus.kafka.ui.utilities.qase.QaseSetup.qaseIntegrationSetup;\nimport static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED;\nimport static com.provectus.kafka.ui.utilities.qase.enums.State.TO_BE_AUTOMATED;\n\nimport com.provectus.kafka.ui.settings.listeners.QaseResultListener;\nimport com.provectus.kafka.ui.utilities.qase.annotations.Automation;\nimport java.lang.reflect.Method;\nimport org.testng.SkipException;\nimport org.testng.annotations.BeforeMethod;\nimport org.testng.annotations.BeforeSuite;\nimport org.testng.annotations.Listeners;\n\n@Listeners(QaseResultListener.class)\npublic abstract class BaseManualTest {\n\n  @BeforeSuite\n  public void beforeSuite() {\n    qaseIntegrationSetup();\n  }\n\n  @BeforeMethod\n  public void beforeMethod(Method method) {\n    if (method.getAnnotation(Automation.class).state().equals(NOT_AUTOMATED)\n        || method.getAnnotation(Automation.class).state().equals(TO_BE_AUTOMATED)) {\n      throw new SkipException(\"Skip test exception\");\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SanityBacklog.java",
    "content": "package com.provectus.kafka.ui.manualsuite.backlog;\n\nimport com.provectus.kafka.ui.manualsuite.BaseManualTest;\n\npublic class SanityBacklog extends BaseManualTest {\n\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java",
    "content": "package com.provectus.kafka.ui.manualsuite.backlog;\n\nimport static com.provectus.kafka.ui.qasesuite.BaseQaseTest.SCHEMAS_SUITE_ID;\nimport static com.provectus.kafka.ui.qasesuite.BaseQaseTest.TOPICS_PROFILE_SUITE_ID;\nimport static com.provectus.kafka.ui.qasesuite.BaseQaseTest.TOPICS_SUITE_ID;\nimport static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED;\nimport static com.provectus.kafka.ui.utilities.qase.enums.State.TO_BE_AUTOMATED;\n\nimport com.provectus.kafka.ui.manualsuite.BaseManualTest;\nimport com.provectus.kafka.ui.utilities.qase.annotations.Automation;\nimport com.provectus.kafka.ui.utilities.qase.annotations.Suite;\nimport io.qase.api.annotation.QaseId;\nimport org.testng.annotations.Test;\n\npublic class SmokeBacklog extends BaseManualTest {\n\n  @Automation(state = TO_BE_AUTOMATED)\n  @Suite(id = TOPICS_PROFILE_SUITE_ID)\n  @QaseId(335)\n  @Test\n  public void testCaseA() {\n  }\n\n  @Automation(state = TO_BE_AUTOMATED)\n  @Suite(id = TOPICS_PROFILE_SUITE_ID)\n  @QaseId(336)\n  @Test\n  public void testCaseB() {\n  }\n\n  @Automation(state = TO_BE_AUTOMATED)\n  @Suite(id = TOPICS_PROFILE_SUITE_ID)\n  @QaseId(343)\n  @Test\n  public void testCaseC() {\n  }\n\n  @Automation(state = TO_BE_AUTOMATED)\n  @Suite(id = SCHEMAS_SUITE_ID)\n  @QaseId(345)\n  @Test\n  public void testCaseD() {\n  }\n\n  @Automation(state = TO_BE_AUTOMATED)\n  @Suite(id = SCHEMAS_SUITE_ID)\n  @QaseId(346)\n  @Test\n  public void testCaseE() {\n  }\n\n  @Automation(state = TO_BE_AUTOMATED)\n  @Suite(id = TOPICS_PROFILE_SUITE_ID)\n  @QaseId(347)\n  @Test\n  public void testCaseF() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @Suite(id = TOPICS_SUITE_ID)\n  @QaseId(50)\n  @Test\n  public void testCaseG() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @Suite(id = SCHEMAS_SUITE_ID)\n  @QaseId(351)\n  @Test\n  public void testCaseH() {\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/DataMaskingTest.java",
    "content": "package com.provectus.kafka.ui.manualsuite.suite;\n\nimport static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED;\n\nimport com.provectus.kafka.ui.manualsuite.BaseManualTest;\nimport com.provectus.kafka.ui.utilities.qase.annotations.Automation;\nimport io.qase.api.annotation.QaseId;\nimport org.testng.annotations.Test;\n\npublic class DataMaskingTest extends BaseManualTest {\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(262)\n  @Test\n  public void testCaseA() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(264)\n  @Test\n  public void testCaseB() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(265)\n  @Test\n  public void testCaseC() {\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/RbacTest.java",
    "content": "package com.provectus.kafka.ui.manualsuite.suite;\n\nimport static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED;\n\nimport com.provectus.kafka.ui.manualsuite.BaseManualTest;\nimport com.provectus.kafka.ui.utilities.qase.annotations.Automation;\nimport io.qase.api.annotation.QaseId;\nimport org.testng.annotations.Test;\n\npublic class RbacTest extends BaseManualTest {\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(249)\n  @Test\n  public void testCaseA() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(251)\n  @Test\n  public void testCaseB() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(257)\n  @Test\n  public void testCaseC() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(258)\n  @Test\n  public void testCaseD() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(259)\n  @Test\n  public void testCaseE() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(260)\n  @Test\n  public void testCaseF() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(261)\n  @Test\n  public void testCaseG() {\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/TopicsTest.java",
    "content": "package com.provectus.kafka.ui.manualsuite.suite;\n\nimport static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED;\n\nimport com.provectus.kafka.ui.manualsuite.BaseManualTest;\nimport com.provectus.kafka.ui.utilities.qase.annotations.Automation;\nimport io.qase.api.annotation.QaseId;\nimport org.testng.annotations.Test;\n\npublic class TopicsTest extends BaseManualTest {\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(17)\n  @Test\n  public void testCaseA() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(18)\n  @Test\n  public void testCaseB() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(21)\n  @Test()\n  public void testCaseC() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(22)\n  @Test\n  public void testCaseD() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(47)\n  @Test\n  public void testCaseE() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(48)\n  @Test\n  public void testCaseF() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(49)\n  @Test\n  public void testCaseG() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(57)\n  @Test\n  public void testCaseH() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(58)\n  @Test\n  public void testCaseI() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(269)\n  @Test\n  public void testCaseJ() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(270)\n  @Test\n  public void testCaseK() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(271)\n  @Test\n  public void testCaseL() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(272)\n  @Test\n  public void testCaseM() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(337)\n  @Test\n  public void testCaseN() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(339)\n  @Test\n  public void testCaseO() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(341)\n  @Test\n  public void testCaseP() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(342)\n  @Test\n  public void testCaseQ() {\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/WizardTest.java",
    "content": "package com.provectus.kafka.ui.manualsuite.suite;\n\nimport static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED;\n\nimport com.provectus.kafka.ui.manualsuite.BaseManualTest;\nimport com.provectus.kafka.ui.utilities.qase.annotations.Automation;\nimport io.qase.api.annotation.QaseId;\nimport org.testng.annotations.Test;\n\npublic class WizardTest extends BaseManualTest {\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(333)\n  @Test\n  public void testCaseA() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(338)\n  @Test\n  public void testCaseB() {\n  }\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseId(340)\n  @Test\n  public void testCaseC() {\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qasesuite/BaseQaseTest.java",
    "content": "package com.provectus.kafka.ui.qasesuite;\n\nimport static com.provectus.kafka.ui.utilities.qase.QaseSetup.qaseIntegrationSetup;\n\nimport com.provectus.kafka.ui.settings.listeners.QaseCreateListener;\nimport org.testng.annotations.BeforeSuite;\nimport org.testng.annotations.Listeners;\n\n@Listeners(QaseCreateListener.class)\npublic abstract class BaseQaseTest {\n\n  public static final long BROKERS_SUITE_ID = 1;\n  public static final long CONNECTORS_SUITE_ID = 10;\n  public static final long KSQL_DB_SUITE_ID = 8;\n  public static final long SANITY_SUITE_ID = 19;\n  public static final long SCHEMAS_SUITE_ID = 11;\n  public static final long TOPICS_SUITE_ID = 2;\n  public static final long TOPICS_CREATE_SUITE_ID = 4;\n  public static final long TOPICS_PROFILE_SUITE_ID = 5;\n\n  @BeforeSuite\n  public void beforeSuite() {\n    qaseIntegrationSetup();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qasesuite/Template.java",
    "content": "package com.provectus.kafka.ui.qasesuite;\n\nimport static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED;\nimport static com.provectus.kafka.ui.utilities.qase.enums.Status.DRAFT;\n\nimport com.provectus.kafka.ui.utilities.qase.annotations.Automation;\nimport com.provectus.kafka.ui.utilities.qase.annotations.Status;\nimport com.provectus.kafka.ui.utilities.qase.annotations.Suite;\nimport io.qase.api.annotation.QaseTitle;\nimport io.qase.api.annotation.Step;\n\npublic class Template extends BaseQaseTest {\n\n  /**\n   * this class is a kind of placeholder or example, use is as template to create new one\n   * copy Template into kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/qaseSuite/\n   * place it into regarding folder and rename according to test case summary from Qase.io\n   * uncomment @Test and set all annotations according to kafka-ui-e2e-checks/QASE.md\n   */\n\n  @Automation(state = NOT_AUTOMATED)\n  @QaseTitle(\"testCaseA title\")\n  @Status(status = DRAFT)\n  @Suite(id = 0)\n  //  @org.testng.annotations.Test\n  public void testCaseA() {\n    stepA();\n    stepB();\n    stepC();\n    stepD();\n    stepE();\n    stepF();\n  }\n\n  @Step(\"stepA action\")\n  private void stepA() {\n  }\n\n  @Step(\"stepB action\")\n  private void stepB() {\n  }\n\n  @Step(\"stepC action\")\n  private void stepC() {\n  }\n\n  @Step(\"stepD action\")\n  private void stepD() {\n  }\n\n  @Step(\"stepE action\")\n  private void stepE() {\n  }\n\n  @Step(\"stepF action\")\n  private void stepF() {\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/sanitysuite/TopicsTest.java",
    "content": "package com.provectus.kafka.ui.sanitysuite;\n\nimport static com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue.COMPACT;\nimport static com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue.DELETE;\nimport static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;\n\nimport com.provectus.kafka.ui.BaseTest;\nimport com.provectus.kafka.ui.models.Topic;\nimport io.qase.api.annotation.QaseId;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.testng.Assert;\nimport org.testng.annotations.AfterClass;\nimport org.testng.annotations.Test;\n\npublic class TopicsTest extends BaseTest {\n\n  private static final List<Topic> TOPIC_LIST = new ArrayList<>();\n\n  @QaseId(285)\n  @Test()\n  public void verifyClearMessagesMenuStateAfterTopicUpdate() {\n    Topic topic = new Topic()\n        .setName(\"topic-\" + randomAlphabetic(5))\n        .setNumberOfPartitions(1)\n        .setCleanupPolicyValue(DELETE);\n    navigateToTopics();\n    topicsList\n        .clickAddTopicBtn();\n    topicCreateEditForm\n        .waitUntilScreenReady()\n        .setTopicName(topic.getName())\n        .setNumberOfPartitions(topic.getNumberOfPartitions())\n        .selectCleanupPolicy(topic.getCleanupPolicyValue())\n        .clickSaveTopicBtn();\n    topicDetails\n        .waitUntilScreenReady();\n    TOPIC_LIST.add(topic);\n    topicDetails\n        .openDotMenu();\n    Assert.assertTrue(topicDetails.isClearMessagesMenuEnabled(), \"isClearMessagesMenuEnabled\");\n    topic.setCleanupPolicyValue(COMPACT);\n    editCleanUpPolicyAndOpenDotMenu(topic);\n    Assert.assertFalse(topicDetails.isClearMessagesMenuEnabled(), \"isClearMessagesMenuEnabled\");\n    topic.setCleanupPolicyValue(DELETE);\n    editCleanUpPolicyAndOpenDotMenu(topic);\n    Assert.assertTrue(topicDetails.isClearMessagesMenuEnabled(), \"isClearMessagesMenuEnabled\");\n  }\n\n  private void editCleanUpPolicyAndOpenDotMenu(Topic topic) {\n    topicDetails\n        .clickEditSettingsMenu();\n    topicCreateEditForm\n        .waitUntilScreenReady()\n        .selectCleanupPolicy(topic.getCleanupPolicyValue())\n        .clickSaveTopicBtn();\n    topicDetails\n        .waitUntilScreenReady()\n        .openDotMenu();\n  }\n\n  @AfterClass(alwaysRun = true)\n  public void afterClass() {\n    TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName()));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/SmokeTest.java",
    "content": "package com.provectus.kafka.ui.smokesuite;\n\nimport static com.provectus.kafka.ui.pages.panels.enums.MenuItem.BROKERS;\nimport static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KAFKA_CONNECT;\nimport static com.provectus.kafka.ui.pages.panels.enums.MenuItem.SCHEMA_REGISTRY;\nimport static com.provectus.kafka.ui.pages.panels.enums.MenuItem.TOPICS;\nimport static com.provectus.kafka.ui.settings.BaseSource.BASE_HOST;\nimport static com.provectus.kafka.ui.utilities.FileUtils.getResourceAsString;\nimport static com.provectus.kafka.ui.variables.Url.BROKERS_LIST_URL;\nimport static com.provectus.kafka.ui.variables.Url.CONSUMERS_LIST_URL;\nimport static com.provectus.kafka.ui.variables.Url.KAFKA_CONNECT_LIST_URL;\nimport static com.provectus.kafka.ui.variables.Url.KSQL_DB_LIST_URL;\nimport static com.provectus.kafka.ui.variables.Url.SCHEMA_REGISTRY_LIST_URL;\nimport static com.provectus.kafka.ui.variables.Url.TOPICS_LIST_URL;\nimport static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;\n\nimport com.codeborne.selenide.Condition;\nimport com.codeborne.selenide.WebDriverRunner;\nimport com.provectus.kafka.ui.BaseTest;\nimport com.provectus.kafka.ui.models.Connector;\nimport com.provectus.kafka.ui.models.Schema;\nimport com.provectus.kafka.ui.models.Topic;\nimport com.provectus.kafka.ui.pages.panels.enums.MenuItem;\nimport io.qameta.allure.Step;\nimport io.qase.api.annotation.QaseId;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport org.testng.Assert;\nimport org.testng.annotations.AfterClass;\nimport org.testng.annotations.BeforeClass;\nimport org.testng.annotations.Test;\n\npublic class SmokeTest extends BaseTest {\n\n  private static final int BROKER_ID = 1;\n  private static final Schema TEST_SCHEMA = Schema.createSchemaAvro();\n  private static final Topic TEST_TOPIC = new Topic()\n      .setName(\"new-topic-\" + randomAlphabetic(5))\n      .setNumberOfPartitions(1);\n  private static final Connector TEST_CONNECTOR = new Connector()\n      .setName(\"new-connector-\" + randomAlphabetic(5))\n      .setConfig(getResourceAsString(\"testData/connectors/config_for_create_connector_via_api.json\"));\n\n  @BeforeClass(alwaysRun = true)\n  public void beforeClass() {\n    apiService\n        .createTopic(TEST_TOPIC)\n        .createSchema(TEST_SCHEMA)\n        .createConnector(TEST_CONNECTOR);\n  }\n\n  @QaseId(198)\n  @Test\n  public void checkBasePageElements() {\n    verifyElementsCondition(\n        Stream.concat(topPanel.getAllVisibleElements().stream(), naviSideBar.getAllMenuButtons().stream())\n            .collect(Collectors.toList()), Condition.visible);\n    verifyElementsCondition(\n        Stream.concat(topPanel.getAllEnabledElements().stream(), naviSideBar.getAllMenuButtons().stream())\n            .collect(Collectors.toList()), Condition.enabled);\n  }\n\n  @QaseId(45)\n  @Test\n  public void checkUrlWhileNavigating() {\n    navigateToBrokers();\n    verifyCurrentUrl(BROKERS_LIST_URL);\n    navigateToTopics();\n    verifyCurrentUrl(TOPICS_LIST_URL);\n    navigateToConsumers();\n    verifyCurrentUrl(CONSUMERS_LIST_URL);\n    navigateToSchemaRegistry();\n    verifyCurrentUrl(SCHEMA_REGISTRY_LIST_URL);\n    navigateToConnectors();\n    verifyCurrentUrl(KAFKA_CONNECT_LIST_URL);\n    navigateToKsqlDb();\n    verifyCurrentUrl(KSQL_DB_LIST_URL);\n  }\n\n  @QaseId(46)\n  @Test\n  public void checkPathWhileNavigating() {\n    navigateToBrokersAndOpenDetails(BROKER_ID);\n    verifyComponentsPath(BROKERS, String.format(\"Broker %d\", BROKER_ID));\n    navigateToTopicsAndOpenDetails(TEST_TOPIC.getName());\n    verifyComponentsPath(TOPICS, TEST_TOPIC.getName());\n    navigateToSchemaRegistryAndOpenDetails(TEST_SCHEMA.getName());\n    verifyComponentsPath(SCHEMA_REGISTRY, TEST_SCHEMA.getName());\n    navigateToConnectorsAndOpenDetails(TEST_CONNECTOR.getName());\n    verifyComponentsPath(KAFKA_CONNECT, TEST_CONNECTOR.getName());\n  }\n\n  @Step\n  private void verifyCurrentUrl(String expectedUrl) {\n    String urlWithoutParameters = WebDriverRunner.getWebDriver().getCurrentUrl();\n    if (urlWithoutParameters.contains(\"?\")) {\n      urlWithoutParameters = urlWithoutParameters.substring(0, urlWithoutParameters.indexOf(\"?\"));\n    }\n    Assert.assertEquals(urlWithoutParameters, String.format(expectedUrl, BASE_HOST), \"getCurrentUrl()\");\n  }\n\n  @Step\n  private void verifyComponentsPath(MenuItem menuItem, String expectedPath) {\n    Assert.assertEquals(naviSideBar.getPagePath(menuItem), expectedPath,\n        String.format(\"getPagePath() for %s\", menuItem.getPageTitle().toUpperCase()));\n  }\n\n  @AfterClass(alwaysRun = true)\n  public void afterClass() {\n    apiService\n        .deleteTopic(TEST_TOPIC.getName())\n        .deleteSchema(TEST_SCHEMA.getName())\n        .deleteConnector(TEST_CONNECTOR.getName());\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/brokers/BrokersTest.java",
    "content": "package com.provectus.kafka.ui.smokesuite.brokers;\n\nimport static com.provectus.kafka.ui.pages.brokers.BrokersDetails.DetailsTab.CONFIGS;\nimport static com.provectus.kafka.ui.utilities.StringUtils.getMixedCase;\nimport static com.provectus.kafka.ui.variables.Expected.BROKER_SOURCE_INFO_TOOLTIP;\n\nimport com.codeborne.selenide.Condition;\nimport com.provectus.kafka.ui.BaseTest;\nimport com.provectus.kafka.ui.pages.brokers.BrokersConfigTab;\nimport io.qameta.allure.Issue;\nimport io.qase.api.annotation.QaseId;\nimport java.util.List;\nimport org.testng.Assert;\nimport org.testng.annotations.Ignore;\nimport org.testng.annotations.Test;\nimport org.testng.asserts.SoftAssert;\n\npublic class BrokersTest extends BaseTest {\n\n  public static final int DEFAULT_BROKER_ID = 1;\n\n  @QaseId(1)\n  @Test\n  public void checkBrokersOverview() {\n    navigateToBrokers();\n    Assert.assertTrue(brokersList.getAllBrokers().size() > 0, \"getAllBrokers()\");\n    verifyElementsCondition(brokersList.getAllVisibleElements(), Condition.visible);\n    verifyElementsCondition(brokersList.getAllEnabledElements(), Condition.enabled);\n  }\n\n  @QaseId(85)\n  @Test\n  public void checkExistingBrokersInCluster() {\n    navigateToBrokers();\n    Assert.assertTrue(brokersList.getAllBrokers().size() > 0, \"getAllBrokers()\");\n    brokersList\n        .openBroker(DEFAULT_BROKER_ID);\n    brokersDetails\n        .waitUntilScreenReady();\n    verifyElementsCondition(brokersDetails.getAllVisibleElements(), Condition.visible);\n    verifyElementsCondition(brokersDetails.getAllEnabledElements(), Condition.enabled);\n    brokersDetails\n        .openDetailsTab(CONFIGS);\n    brokersConfigTab\n        .waitUntilScreenReady();\n    verifyElementsCondition(brokersConfigTab.getColumnHeaders(), Condition.visible);\n    verifyElementsCondition(brokersConfigTab.getEditButtons(), Condition.enabled);\n    Assert.assertTrue(brokersConfigTab.isSearchByKeyVisible(), \"isSearchByKeyVisible()\");\n  }\n\n  @Ignore\n  @Issue(\"https://github.com/provectus/kafka-ui/issues/3347\")\n  @QaseId(330)\n  @Test\n  public void brokersConfigFirstPageSearchCheck() {\n    navigateToBrokersAndOpenDetails(DEFAULT_BROKER_ID);\n    brokersDetails\n        .openDetailsTab(CONFIGS);\n    String anyConfigKeyFirstPage = brokersConfigTab\n        .getAllConfigs().stream()\n        .findAny().orElseThrow()\n        .getKey();\n    brokersConfigTab\n        .clickNextButton();\n    Assert.assertFalse(brokersConfigTab.getAllConfigs().stream()\n            .map(BrokersConfigTab.BrokersConfigItem::getKey)\n            .toList().contains(anyConfigKeyFirstPage),\n        String.format(\"getAllConfigs().contains(%s)\", anyConfigKeyFirstPage));\n    brokersConfigTab\n        .searchConfig(anyConfigKeyFirstPage);\n    Assert.assertTrue(brokersConfigTab.getAllConfigs().stream()\n            .map(BrokersConfigTab.BrokersConfigItem::getKey)\n            .toList().contains(anyConfigKeyFirstPage),\n        String.format(\"getAllConfigs().contains(%s)\", anyConfigKeyFirstPage));\n  }\n\n  @Ignore\n  @Issue(\"https://github.com/provectus/kafka-ui/issues/3347\")\n  @QaseId(350)\n  @Test\n  public void brokersConfigSecondPageSearchCheck() {\n    navigateToBrokersAndOpenDetails(DEFAULT_BROKER_ID);\n    brokersDetails\n        .openDetailsTab(CONFIGS);\n    brokersConfigTab\n        .clickNextButton();\n    String anyConfigKeySecondPage = brokersConfigTab\n        .getAllConfigs().stream()\n        .findAny().orElseThrow()\n        .getKey();\n    brokersConfigTab\n        .clickPreviousButton();\n    Assert.assertFalse(brokersConfigTab.getAllConfigs().stream()\n            .map(BrokersConfigTab.BrokersConfigItem::getKey)\n            .toList().contains(anyConfigKeySecondPage),\n        String.format(\"getAllConfigs().contains(%s)\", anyConfigKeySecondPage));\n    brokersConfigTab\n        .searchConfig(anyConfigKeySecondPage);\n    Assert.assertTrue(brokersConfigTab.getAllConfigs().stream()\n            .map(BrokersConfigTab.BrokersConfigItem::getKey)\n            .toList().contains(anyConfigKeySecondPage),\n        String.format(\"getAllConfigs().contains(%s)\", anyConfigKeySecondPage));\n  }\n\n  @Ignore\n  @Issue(\"https://github.com/provectus/kafka-ui/issues/3347\")\n  @QaseId(348)\n  @Test\n  public void brokersConfigCaseInsensitiveSearchCheck() {\n    navigateToBrokersAndOpenDetails(DEFAULT_BROKER_ID);\n    brokersDetails\n        .openDetailsTab(CONFIGS);\n    String anyConfigKeyFirstPage = brokersConfigTab\n        .getAllConfigs().stream()\n        .findAny().orElseThrow()\n        .getKey();\n    brokersConfigTab\n        .clickNextButton();\n    Assert.assertFalse(brokersConfigTab.getAllConfigs().stream()\n            .map(BrokersConfigTab.BrokersConfigItem::getKey)\n            .toList().contains(anyConfigKeyFirstPage),\n        String.format(\"getAllConfigs().contains(%s)\", anyConfigKeyFirstPage));\n    SoftAssert softly = new SoftAssert();\n    List.of(anyConfigKeyFirstPage.toLowerCase(), anyConfigKeyFirstPage.toUpperCase(),\n            getMixedCase(anyConfigKeyFirstPage))\n        .forEach(configCase -> {\n          brokersConfigTab\n              .searchConfig(configCase);\n          softly.assertTrue(brokersConfigTab.getAllConfigs().stream()\n                  .map(BrokersConfigTab.BrokersConfigItem::getKey)\n                  .toList().contains(anyConfigKeyFirstPage),\n              String.format(\"getAllConfigs().contains(%s)\", configCase));\n        });\n    softly.assertAll();\n  }\n\n  @QaseId(331)\n  @Test\n  public void brokersSourceInfoCheck() {\n    navigateToBrokersAndOpenDetails(DEFAULT_BROKER_ID);\n    brokersDetails\n        .openDetailsTab(CONFIGS);\n    String sourceInfoTooltip = brokersConfigTab\n        .hoverOnSourceInfoIcon()\n        .getSourceInfoTooltipText();\n    Assert.assertEquals(sourceInfoTooltip, BROKER_SOURCE_INFO_TOOLTIP, \"brokerSourceInfoTooltip\");\n  }\n\n  @QaseId(332)\n  @Test\n  public void brokersConfigEditCheck() {\n    navigateToBrokersAndOpenDetails(DEFAULT_BROKER_ID);\n    brokersDetails\n        .openDetailsTab(CONFIGS);\n    String configKey = \"log.cleaner.min.compaction.lag.ms\";\n    BrokersConfigTab.BrokersConfigItem configItem = brokersConfigTab\n        .searchConfig(configKey)\n        .getConfig(configKey);\n    int defaultValue = Integer.parseInt(configItem.getValue());\n    configItem\n        .clickEditBtn();\n    SoftAssert softly = new SoftAssert();\n    softly.assertTrue(configItem.getSaveBtn().isDisplayed(), \"getSaveBtn().isDisplayed()\");\n    softly.assertTrue(configItem.getCancelBtn().isDisplayed(), \"getCancelBtn().isDisplayed()\");\n    softly.assertTrue(configItem.getValueFld().isEnabled(), \"getValueFld().isEnabled()\");\n    softly.assertAll();\n    int newValue = defaultValue + 1;\n    configItem\n        .setValue(String.valueOf(newValue))\n        .clickCancelBtn();\n    Assert.assertEquals(Integer.parseInt(configItem.getValue()), defaultValue, \"getValue()\");\n    configItem\n        .clickEditBtn()\n        .setValue(String.valueOf(newValue))\n        .clickSaveBtn()\n        .clickConfirm();\n    configItem = brokersConfigTab\n        .searchConfig(configKey)\n        .getConfig(configKey);\n    softly.assertFalse(configItem.getSaveBtn().isDisplayed(), \"getSaveBtn().isDisplayed()\");\n    softly.assertFalse(configItem.getCancelBtn().isDisplayed(), \"getCancelBtn().isDisplayed()\");\n    softly.assertTrue(configItem.getEditBtn().isDisplayed(), \"getEditBtn().isDisplayed()\");\n    softly.assertEquals(Integer.parseInt(configItem.getValue()), newValue, \"getValue()\");\n    softly.assertAll();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/connectors/ConnectorsTest.java",
    "content": "package com.provectus.kafka.ui.smokesuite.connectors;\n\nimport static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS;\nimport static com.provectus.kafka.ui.utilities.FileUtils.getResourceAsString;\nimport static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;\n\nimport com.provectus.kafka.ui.BaseTest;\nimport com.provectus.kafka.ui.models.Connector;\nimport com.provectus.kafka.ui.models.Topic;\nimport io.qase.api.annotation.QaseId;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.testng.Assert;\nimport org.testng.annotations.AfterClass;\nimport org.testng.annotations.BeforeClass;\nimport org.testng.annotations.Test;\n\npublic class ConnectorsTest extends BaseTest {\n\n  private static final List<Topic> TOPIC_LIST = new ArrayList<>();\n  private static final List<Connector> CONNECTOR_LIST = new ArrayList<>();\n  private static final String MESSAGE_CONTENT = \"testData/topics/message_content_create_topic.json\";\n  private static final String MESSAGE_KEY = \" \";\n  private static final Topic TOPIC_FOR_CREATE = new Topic()\n      .setName(\"topic-for-create-connector-\" + randomAlphabetic(5))\n      .setMessageValue(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY);\n  private static final Topic TOPIC_FOR_DELETE = new Topic()\n      .setName(\"topic-for-delete-connector-\" + randomAlphabetic(5))\n      .setMessageValue(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY);\n  private static final Topic TOPIC_FOR_UPDATE = new Topic()\n      .setName(\"topic-for-update-connector-\" + randomAlphabetic(5))\n      .setMessageValue(MESSAGE_CONTENT).setMessageKey(MESSAGE_KEY);\n  private static final Connector CONNECTOR_FOR_DELETE = new Connector()\n      .setName(\"connector-for-delete-\" + randomAlphabetic(5))\n      .setConfig(getResourceAsString(\"testData/connectors/delete_connector_config.json\"));\n  private static final Connector CONNECTOR_FOR_UPDATE = new Connector()\n      .setName(\"connector-for-update-and-delete-\" + randomAlphabetic(5))\n      .setConfig(getResourceAsString(\"testData/connectors/config_for_create_connector_via_api.json\"));\n\n  @BeforeClass(alwaysRun = true)\n  public void beforeClass() {\n    TOPIC_LIST.addAll(List.of(TOPIC_FOR_CREATE, TOPIC_FOR_DELETE, TOPIC_FOR_UPDATE));\n    TOPIC_LIST.forEach(topic -> apiService\n        .createTopic(topic)\n        .sendMessage(topic)\n    );\n    CONNECTOR_LIST.addAll(List.of(CONNECTOR_FOR_DELETE, CONNECTOR_FOR_UPDATE));\n    CONNECTOR_LIST.forEach(connector -> apiService.createConnector(connector));\n  }\n\n  @QaseId(42)\n  @Test\n  public void createConnector() {\n    Connector connectorForCreate = new Connector()\n        .setName(\"connector-for-create-\" + randomAlphabetic(5))\n        .setConfig(getResourceAsString(\"testData/connectors/config_for_create_connector.json\"));\n    navigateToConnectors();\n    kafkaConnectList\n        .clickCreateConnectorBtn();\n    connectorCreateForm\n        .waitUntilScreenReady()\n        .setConnectorDetails(connectorForCreate.getName(), connectorForCreate.getConfig())\n        .clickSubmitButton();\n    connectorDetails\n        .waitUntilScreenReady();\n    navigateToConnectorsAndOpenDetails(connectorForCreate.getName());\n    Assert.assertTrue(connectorDetails.isConnectorHeaderVisible(connectorForCreate.getName()),\n        \"isConnectorTitleVisible()\");\n    navigateToConnectors();\n    Assert.assertTrue(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_DELETE.getName()), \"isConnectorVisible()\");\n    CONNECTOR_LIST.add(connectorForCreate);\n  }\n\n  @QaseId(196)\n  @Test\n  public void updateConnector() {\n    navigateToConnectorsAndOpenDetails(CONNECTOR_FOR_UPDATE.getName());\n    connectorDetails\n        .openConfigTab()\n        .setConfig(CONNECTOR_FOR_UPDATE.getConfig())\n        .clickSubmitButton();\n    Assert.assertTrue(connectorDetails.isAlertWithMessageVisible(SUCCESS, \"Config successfully updated.\"),\n        \"isAlertWithMessageVisible()\");\n    navigateToConnectors();\n    Assert.assertTrue(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_UPDATE.getName()), \"isConnectorVisible()\");\n  }\n\n  @QaseId(195)\n  @Test\n  public void deleteConnector() {\n    navigateToConnectorsAndOpenDetails(CONNECTOR_FOR_DELETE.getName());\n    connectorDetails\n        .openDotMenu()\n        .clickDeleteBtn()\n        .clickConfirmBtn();\n    navigateToConnectors();\n    Assert.assertFalse(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_DELETE.getName()), \"isConnectorVisible()\");\n    CONNECTOR_LIST.remove(CONNECTOR_FOR_DELETE);\n  }\n\n  @AfterClass(alwaysRun = true)\n  public void afterClass() {\n    CONNECTOR_LIST.forEach(connector ->\n        apiService.deleteConnector(connector.getName()));\n    TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName()));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/ksqldb/KsqlDbTest.java",
    "content": "package com.provectus.kafka.ui.smokesuite.ksqldb;\n\nimport static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlMenuTabs.STREAMS;\nimport static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SELECT_ALL_FROM;\nimport static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SHOW_STREAMS;\nimport static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SHOW_TABLES;\nimport static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;\n\nimport com.provectus.kafka.ui.BaseTest;\nimport com.provectus.kafka.ui.pages.ksqldb.models.Stream;\nimport com.provectus.kafka.ui.pages.ksqldb.models.Table;\nimport io.qameta.allure.Step;\nimport io.qase.api.annotation.QaseId;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.testng.Assert;\nimport org.testng.annotations.AfterClass;\nimport org.testng.annotations.BeforeClass;\nimport org.testng.annotations.Test;\nimport org.testng.asserts.SoftAssert;\n\npublic class KsqlDbTest extends BaseTest {\n\n  private static final Stream DEFAULT_STREAM = new Stream()\n      .setName(\"DEFAULT_STREAM_\" + randomAlphabetic(4).toUpperCase())\n      .setTopicName(\"DEFAULT_TOPIC_\" + randomAlphabetic(4).toUpperCase());\n  private static final Table FIRST_TABLE = new Table()\n      .setName(\"FIRST_TABLE_\" + randomAlphabetic(4).toUpperCase())\n      .setStreamName(DEFAULT_STREAM.getName());\n  private static final Table SECOND_TABLE = new Table()\n      .setName(\"SECOND_TABLE_\" + randomAlphabetic(4).toUpperCase())\n      .setStreamName(DEFAULT_STREAM.getName());\n  private static final List<String> TOPIC_NAMES_LIST = new ArrayList<>();\n\n  @BeforeClass(alwaysRun = true)\n  public void beforeClass() {\n    apiService\n        .createStream(DEFAULT_STREAM)\n        .createTables(FIRST_TABLE, SECOND_TABLE);\n    TOPIC_NAMES_LIST.addAll(List.of(DEFAULT_STREAM.getTopicName(),\n        FIRST_TABLE.getName(), SECOND_TABLE.getName()));\n  }\n\n  @QaseId(284)\n  @Test(priority = 1)\n  public void streamsAndTablesVisibilityCheck() {\n    navigateToKsqlDb();\n    SoftAssert softly = new SoftAssert();\n    softly.assertTrue(ksqlDbList.getTableByName(FIRST_TABLE.getName()).isVisible(), \"getTableByName()\");\n    softly.assertTrue(ksqlDbList.getTableByName(SECOND_TABLE.getName()).isVisible(), \"getTableByName()\");\n    softly.assertAll();\n    ksqlDbList\n        .openDetailsTab(STREAMS)\n        .waitUntilScreenReady();\n    Assert.assertTrue(ksqlDbList.getStreamByName(DEFAULT_STREAM.getName()).isVisible(), \"getStreamByName()\");\n  }\n\n  @QaseId(276)\n  @Test(priority = 2)\n  public void clearEnteredQueryCheck() {\n    navigateToKsqlDbAndExecuteRequest(SHOW_TABLES.getQuery());\n    Assert.assertFalse(ksqlQueryForm.getEnteredQuery().isEmpty(), \"getEnteredQuery()\");\n    ksqlQueryForm\n        .clickClearBtn();\n    Assert.assertTrue(ksqlQueryForm.getEnteredQuery().isEmpty(), \"getEnteredQuery()\");\n  }\n\n  @QaseId(344)\n  @Test(priority = 3)\n  public void clearResultsButtonCheck() {\n    String notValidQuery = \"some not valid request\";\n    navigateToKsqlDb();\n    ksqlDbList\n        .clickExecuteKsqlRequestBtn();\n    ksqlQueryForm\n        .waitUntilScreenReady()\n        .setQuery(notValidQuery);\n    Assert.assertFalse(ksqlQueryForm.isClearResultsBtnEnabled(), \"isClearResultsBtnEnabled()\");\n    ksqlQueryForm\n        .clickExecuteBtn(notValidQuery);\n    Assert.assertFalse(ksqlQueryForm.isClearResultsBtnEnabled(), \"isClearResultsBtnEnabled()\");\n  }\n\n  @QaseId(41)\n  @Test(priority = 4)\n  public void checkShowTablesRequestExecution() {\n    navigateToKsqlDbAndExecuteRequest(SHOW_TABLES.getQuery());\n    SoftAssert softly = new SoftAssert();\n    softly.assertTrue(ksqlQueryForm.areResultsVisible(), \"areResultsVisible()\");\n    softly.assertTrue(ksqlQueryForm.getItemByName(FIRST_TABLE.getName()).isVisible(),\n        String.format(\"getItemByName(%s)\", FIRST_TABLE.getName()));\n    softly.assertTrue(ksqlQueryForm.getItemByName(SECOND_TABLE.getName()).isVisible(),\n        String.format(\"getItemByName(%s)\", SECOND_TABLE.getName()));\n    softly.assertAll();\n  }\n\n  @QaseId(278)\n  @Test(priority = 5)\n  public void checkShowStreamsRequestExecution() {\n    navigateToKsqlDbAndExecuteRequest(SHOW_STREAMS.getQuery());\n    SoftAssert softly = new SoftAssert();\n    softly.assertTrue(ksqlQueryForm.areResultsVisible(), \"areResultsVisible()\");\n    softly.assertTrue(ksqlQueryForm.getItemByName(DEFAULT_STREAM.getName()).isVisible(),\n        String.format(\"getItemByName(%s)\", FIRST_TABLE.getName()));\n    softly.assertAll();\n  }\n\n  @QaseId(86)\n  @Test(priority = 6)\n  public void clearResultsForExecutedRequest() {\n    navigateToKsqlDbAndExecuteRequest(SHOW_TABLES.getQuery());\n    SoftAssert softly = new SoftAssert();\n    softly.assertTrue(ksqlQueryForm.areResultsVisible(), \"areResultsVisible()\");\n    softly.assertAll();\n    ksqlQueryForm\n        .clickClearResultsBtn();\n    softly.assertFalse(ksqlQueryForm.areResultsVisible(), \"areResultsVisible()\");\n    softly.assertAll();\n  }\n\n  @QaseId(277)\n  @Test(priority = 7)\n  public void stopQueryFunctionalCheck() {\n    navigateToKsqlDbAndExecuteRequest(String.format(SELECT_ALL_FROM.getQuery(), FIRST_TABLE.getName()));\n    Assert.assertTrue(ksqlQueryForm.isAbortBtnVisible(), \"isAbortBtnVisible()\");\n    ksqlQueryForm\n        .clickAbortBtn();\n    Assert.assertTrue(ksqlQueryForm.isCancelledAlertVisible(), \"isCancelledAlertVisible()\");\n  }\n\n  @AfterClass(alwaysRun = true)\n  public void afterClass() {\n    TOPIC_NAMES_LIST.forEach(topicName -> apiService.deleteTopic(topicName));\n  }\n\n  @Step\n  private void navigateToKsqlDbAndExecuteRequest(String query) {\n    navigateToKsqlDb();\n    ksqlDbList\n        .clickExecuteKsqlRequestBtn();\n    ksqlQueryForm\n        .waitUntilScreenReady()\n        .setQuery(query)\n        .clickExecuteBtn(query);\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/schemas/SchemasTest.java",
    "content": "package com.provectus.kafka.ui.smokesuite.schemas;\n\nimport static com.provectus.kafka.ui.utilities.FileUtils.fileToString;\n\nimport com.codeborne.selenide.Condition;\nimport com.provectus.kafka.ui.BaseTest;\nimport com.provectus.kafka.ui.api.model.CompatibilityLevel;\nimport com.provectus.kafka.ui.models.Schema;\nimport io.qase.api.annotation.QaseId;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.testng.Assert;\nimport org.testng.annotations.AfterClass;\nimport org.testng.annotations.BeforeClass;\nimport org.testng.annotations.Test;\nimport org.testng.asserts.SoftAssert;\n\npublic class SchemasTest extends BaseTest {\n\n  private static final List<Schema> SCHEMA_LIST = new ArrayList<>();\n  private static final Schema AVRO_API = Schema.createSchemaAvro();\n  private static final Schema JSON_API = Schema.createSchemaJson();\n  private static final Schema PROTOBUF_API = Schema.createSchemaProtobuf();\n\n  @BeforeClass(alwaysRun = true)\n  public void beforeClass() {\n    SCHEMA_LIST.addAll(List.of(AVRO_API, JSON_API, PROTOBUF_API));\n    SCHEMA_LIST.forEach(schema -> apiService.createSchema(schema));\n  }\n\n  @QaseId(43)\n  @Test(priority = 1)\n  public void createSchemaAvro() {\n    Schema schemaAvro = Schema.createSchemaAvro();\n    navigateToSchemaRegistry();\n    schemaRegistryList\n        .clickCreateSchema();\n    schemaCreateForm\n        .setSubjectName(schemaAvro.getName())\n        .setSchemaField(fileToString(schemaAvro.getValuePath()))\n        .selectSchemaTypeFromDropdown(schemaAvro.getType())\n        .clickSubmitButton();\n    schemaDetails\n        .waitUntilScreenReady();\n    SoftAssert softly = new SoftAssert();\n    softly.assertTrue(schemaDetails.isSchemaHeaderVisible(schemaAvro.getName()), \"isSchemaHeaderVisible()\");\n    softly.assertEquals(schemaDetails.getSchemaType(), schemaAvro.getType().getValue(), \"getSchemaType()\");\n    softly.assertEquals(schemaDetails.getCompatibility(), CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue(),\n        \"getCompatibility()\");\n    softly.assertAll();\n    navigateToSchemaRegistry();\n    Assert.assertTrue(schemaRegistryList.isSchemaVisible(AVRO_API.getName()), \"isSchemaVisible()\");\n    SCHEMA_LIST.add(schemaAvro);\n  }\n\n  @QaseId(186)\n  @Test(priority = 2)\n  public void updateSchemaAvro() {\n    AVRO_API.setValuePath(\n        System.getProperty(\"user.dir\") + \"/src/main/resources/testData/schemas/schema_avro_for_update.json\");\n    navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName());\n    schemaDetails\n        .openEditSchema();\n    schemaCreateForm\n        .waitUntilScreenReady();\n    verifyElementsCondition(schemaCreateForm.getAllDetailsPageElements(), Condition.visible);\n    SoftAssert softly = new SoftAssert();\n    softly.assertFalse(schemaCreateForm.isSubmitBtnEnabled(), \"isSubmitBtnEnabled()\");\n    softly.assertFalse(schemaCreateForm.isSchemaDropDownEnabled(), \"isSchemaDropDownEnabled()\");\n    softly.assertAll();\n    schemaCreateForm\n        .selectCompatibilityLevelFromDropdown(CompatibilityLevel.CompatibilityEnum.NONE)\n        .setNewSchemaValue(fileToString(AVRO_API.getValuePath()))\n        .clickSubmitButton();\n    schemaDetails\n        .waitUntilScreenReady();\n    Assert.assertEquals(schemaDetails.getCompatibility(), CompatibilityLevel.CompatibilityEnum.NONE.toString(),\n        \"getCompatibility()\");\n  }\n\n  @QaseId(44)\n  @Test(priority = 3)\n  public void compareVersionsOperation() {\n    navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName());\n    int latestVersion = schemaDetails\n        .waitUntilScreenReady()\n        .getLatestVersion();\n    schemaDetails\n        .openCompareVersionMenu();\n    int versionsNumberFromDdl = schemaCreateForm\n        .waitUntilScreenReady()\n        .openLeftVersionDdl()\n        .getVersionsNumberFromList();\n    Assert.assertEquals(versionsNumberFromDdl, latestVersion, \"Versions number is not matched\");\n    schemaCreateForm\n        .selectVersionFromDropDown(1);\n    Assert.assertEquals(schemaCreateForm.getMarkedLinesNumber(), 42, \"getAllMarkedLines()\");\n  }\n\n  @QaseId(187)\n  @Test(priority = 4)\n  public void deleteSchemaAvro() {\n    navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName());\n    schemaDetails\n        .removeSchema();\n    schemaRegistryList\n        .waitUntilScreenReady();\n    Assert.assertFalse(schemaRegistryList.isSchemaVisible(AVRO_API.getName()), \"isSchemaVisible()\");\n    SCHEMA_LIST.remove(AVRO_API);\n  }\n\n  @QaseId(89)\n  @Test(priority = 5)\n  public void createSchemaJson() {\n    Schema schemaJson = Schema.createSchemaJson();\n    navigateToSchemaRegistry();\n    schemaRegistryList\n        .clickCreateSchema();\n    schemaCreateForm\n        .setSubjectName(schemaJson.getName())\n        .setSchemaField(fileToString(schemaJson.getValuePath()))\n        .selectSchemaTypeFromDropdown(schemaJson.getType())\n        .clickSubmitButton();\n    schemaDetails\n        .waitUntilScreenReady();\n    SoftAssert softly = new SoftAssert();\n    softly.assertTrue(schemaDetails.isSchemaHeaderVisible(schemaJson.getName()), \"isSchemaHeaderVisible()\");\n    softly.assertEquals(schemaDetails.getSchemaType(), schemaJson.getType().getValue(), \"getSchemaType()\");\n    softly.assertEquals(schemaDetails.getCompatibility(), CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue(),\n        \"getCompatibility()\");\n    softly.assertAll();\n    navigateToSchemaRegistry();\n    Assert.assertTrue(schemaRegistryList.isSchemaVisible(JSON_API.getName()), \"isSchemaVisible()\");\n    SCHEMA_LIST.add(schemaJson);\n  }\n\n  @QaseId(189)\n  @Test(priority = 6)\n  public void deleteSchemaJson() {\n    navigateToSchemaRegistryAndOpenDetails(JSON_API.getName());\n    schemaDetails\n        .removeSchema();\n    schemaRegistryList\n        .waitUntilScreenReady();\n    Assert.assertFalse(schemaRegistryList.isSchemaVisible(JSON_API.getName()), \"isSchemaVisible()\");\n    SCHEMA_LIST.remove(JSON_API);\n  }\n\n  @QaseId(91)\n  @Test(priority = 7)\n  public void createSchemaProtobuf() {\n    Schema schemaProtobuf = Schema.createSchemaProtobuf();\n    navigateToSchemaRegistry();\n    schemaRegistryList\n        .clickCreateSchema();\n    schemaCreateForm\n        .setSubjectName(schemaProtobuf.getName())\n        .setSchemaField(fileToString(schemaProtobuf.getValuePath()))\n        .selectSchemaTypeFromDropdown(schemaProtobuf.getType())\n        .clickSubmitButton();\n    schemaDetails\n        .waitUntilScreenReady();\n    SoftAssert softly = new SoftAssert();\n    softly.assertTrue(schemaDetails.isSchemaHeaderVisible(schemaProtobuf.getName()), \"isSchemaHeaderVisible()\");\n    softly.assertEquals(schemaDetails.getSchemaType(), schemaProtobuf.getType().getValue(), \"getSchemaType()\");\n    softly.assertEquals(schemaDetails.getCompatibility(), CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue(),\n        \"getCompatibility()\");\n    softly.assertAll();\n    navigateToSchemaRegistry();\n    Assert.assertTrue(schemaRegistryList.isSchemaVisible(PROTOBUF_API.getName()), \"isSchemaVisible()\");\n    SCHEMA_LIST.add(schemaProtobuf);\n  }\n\n  @QaseId(223)\n  @Test(priority = 8)\n  public void deleteSchemaProtobuf() {\n    navigateToSchemaRegistryAndOpenDetails(PROTOBUF_API.getName());\n    schemaDetails\n        .removeSchema();\n    schemaRegistryList\n        .waitUntilScreenReady();\n    Assert.assertFalse(schemaRegistryList.isSchemaVisible(PROTOBUF_API.getName()), \"isSchemaVisible()\");\n    SCHEMA_LIST.remove(PROTOBUF_API);\n  }\n\n  @AfterClass(alwaysRun = true)\n  public void afterClass() {\n    SCHEMA_LIST.forEach(schema -> apiService.deleteSchema(schema.getName()));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/MessagesTest.java",
    "content": "package com.provectus.kafka.ui.smokesuite.topics;\n\nimport static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS;\nimport static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.MESSAGES;\nimport static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.OVERVIEW;\nimport static com.provectus.kafka.ui.utilities.TimeUtils.waitUntilNewMinuteStarted;\nimport static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;\n\nimport com.provectus.kafka.ui.BaseTest;\nimport com.provectus.kafka.ui.models.Topic;\nimport io.qameta.allure.Issue;\nimport io.qameta.allure.Step;\nimport io.qase.api.annotation.QaseId;\nimport java.time.LocalDateTime;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.IntStream;\nimport org.testng.Assert;\nimport org.testng.annotations.AfterClass;\nimport org.testng.annotations.BeforeClass;\nimport org.testng.annotations.Ignore;\nimport org.testng.annotations.Test;\nimport org.testng.asserts.SoftAssert;\n\npublic class MessagesTest extends BaseTest {\n\n  private static final Topic TOPIC_FOR_MESSAGES = new Topic()\n      .setName(\"topic-with-clean-message-attribute-\" + randomAlphabetic(5))\n      .setMessageKey(randomAlphabetic(5))\n      .setMessageValue(randomAlphabetic(10));\n  private static final Topic TOPIC_TO_CLEAR_AND_PURGE_MESSAGES = new Topic()\n      .setName(\"topic-to-clear-and-purge-messages-\" + randomAlphabetic(5))\n      .setMessageKey(randomAlphabetic(5))\n      .setMessageValue(randomAlphabetic(10));\n  private static final Topic TOPIC_FOR_CHECK_FILTERS = new Topic()\n      .setName(\"topic-for-check-filters-\" + randomAlphabetic(5))\n      .setMessageKey(randomAlphabetic(5))\n      .setMessageValue(randomAlphabetic(10));\n  private static final Topic TOPIC_TO_RECREATE = new Topic()\n      .setName(\"topic-to-recreate-attribute-\" + randomAlphabetic(5))\n      .setMessageKey(randomAlphabetic(5))\n      .setMessageValue(randomAlphabetic(10));\n  private static final Topic TOPIC_FOR_CHECK_MESSAGES_COUNT = new Topic()\n      .setName(\"topic-for-check-messages-count\" + randomAlphabetic(5))\n      .setMessageKey(randomAlphabetic(5))\n      .setMessageValue(randomAlphabetic(10));\n  private static final List<Topic> TOPIC_LIST = new ArrayList<>();\n\n  @BeforeClass(alwaysRun = true)\n  public void beforeClass() {\n    TOPIC_LIST.addAll(List.of(TOPIC_FOR_MESSAGES, TOPIC_FOR_CHECK_FILTERS, TOPIC_TO_CLEAR_AND_PURGE_MESSAGES,\n        TOPIC_TO_RECREATE, TOPIC_FOR_CHECK_MESSAGES_COUNT));\n    TOPIC_LIST.forEach(topic -> apiService.createTopic(topic));\n    IntStream.range(1, 3).forEach(i -> apiService.sendMessage(TOPIC_FOR_CHECK_FILTERS));\n    waitUntilNewMinuteStarted();\n    IntStream.range(1, 3).forEach(i -> apiService.sendMessage(TOPIC_FOR_CHECK_FILTERS));\n    IntStream.range(1, 110).forEach(i -> apiService.sendMessage(TOPIC_FOR_CHECK_MESSAGES_COUNT));\n  }\n\n  @QaseId(222)\n  @Test(priority = 1)\n  public void produceMessageCheck() {\n    navigateToTopicsAndOpenDetails(TOPIC_FOR_MESSAGES.getName());\n    topicDetails\n        .openDetailsTab(MESSAGES);\n    produceMessage(TOPIC_FOR_MESSAGES);\n    Assert.assertEquals(topicDetails.getMessageByKey(TOPIC_FOR_MESSAGES.getMessageKey()).getValue(),\n        TOPIC_FOR_MESSAGES.getMessageValue(), \"message.getValue()\");\n  }\n\n  @QaseId(19)\n  @Test(priority = 2)\n  public void clearMessageCheck() {\n    navigateToTopicsAndOpenDetails(TOPIC_FOR_MESSAGES.getName());\n    topicDetails\n        .openDetailsTab(OVERVIEW);\n    int messageAmount = topicDetails.getMessageCountAmount();\n    produceMessage(TOPIC_FOR_MESSAGES);\n    Assert.assertEquals(topicDetails.getMessageCountAmount(), messageAmount + 1, \"getMessageCountAmount()\");\n    topicDetails\n        .openDotMenu()\n        .clickClearMessagesMenu()\n        .clickConfirmBtnMdl()\n        .waitUntilScreenReady();\n    Assert.assertEquals(topicDetails.getMessageCountAmount(), 0, \"getMessageCountAmount()\");\n  }\n\n  @QaseId(239)\n  @Test(priority = 3)\n  public void checkClearTopicMessage() {\n    navigateToTopicsAndOpenDetails(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName());\n    topicDetails\n        .openDetailsTab(OVERVIEW);\n    produceMessage(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES);\n    navigateToTopics();\n    Assert.assertEquals(topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(), 1,\n        \"getNumberOfMessages()\");\n    topicsList\n        .openDotMenuByTopicName(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName())\n        .clickClearMessagesBtn()\n        .clickConfirmBtnMdl();\n    SoftAssert softly = new SoftAssert();\n    softly.assertTrue(topicsList.isAlertWithMessageVisible(SUCCESS,\n            String.format(\"%s messages have been successfully cleared!\", TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName())),\n        \"isAlertWithMessageVisible()\");\n    softly.assertEquals(topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(), 0,\n        \"getNumberOfMessages()\");\n    softly.assertAll();\n  }\n\n  @QaseId(10)\n  @Test(priority = 4)\n  public void checkPurgeMessagePossibility() {\n    navigateToTopics();\n    int messageAmount = topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages();\n    topicsList\n        .openTopic(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName());\n    topicDetails\n        .openDetailsTab(OVERVIEW);\n    produceMessage(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES);\n    navigateToTopics();\n    Assert.assertEquals(topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(),\n        messageAmount + 1, \"getNumberOfMessages()\");\n    topicsList\n        .getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName())\n        .selectItem(true)\n        .clickPurgeMessagesOfSelectedTopicsBtn();\n    Assert.assertTrue(topicsList.isConfirmationMdlVisible(), \"isConfirmationMdlVisible()\");\n    topicsList\n        .clickCancelBtnMdl()\n        .clickPurgeMessagesOfSelectedTopicsBtn()\n        .clickConfirmBtnMdl();\n    SoftAssert softly = new SoftAssert();\n    softly.assertTrue(topicsList.isAlertWithMessageVisible(SUCCESS,\n            String.format(\"%s messages have been successfully cleared!\", TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName())),\n        \"isAlertWithMessageVisible()\");\n    softly.assertEquals(topicsList.getTopicItem(TOPIC_TO_CLEAR_AND_PURGE_MESSAGES.getName()).getNumberOfMessages(), 0,\n        \"getNumberOfMessages()\");\n    softly.assertAll();\n  }\n\n  @QaseId(15)\n  @Test(priority = 6)\n  public void checkMessageFilteringByOffset() {\n    navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName());\n    int nextOffset = topicDetails\n        .openDetailsTab(MESSAGES)\n        .getAllMessages().stream()\n        .findFirst().orElseThrow().getOffset() + 1;\n    topicDetails\n        .selectSeekTypeDdlMessagesTab(\"Offset\")\n        .setSeekTypeValueFldMessagesTab(String.valueOf(nextOffset))\n        .clickSubmitFiltersBtnMessagesTab();\n    SoftAssert softly = new SoftAssert();\n    topicDetails.getAllMessages().forEach(message ->\n        softly.assertTrue(message.getOffset() >= nextOffset,\n            String.format(\"Expected offset not less: %s, but found: %s\", nextOffset, message.getOffset())));\n    softly.assertAll();\n  }\n\n  @Ignore\n  @Issue(\"https://github.com/provectus/kafka-ui/issues/3215\")\n  @Issue(\"https://github.com/provectus/kafka-ui/issues/2345\")\n  @QaseId(16)\n  @Test(priority = 7)\n  public void checkMessageFilteringByTimestamp() {\n    navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName());\n    LocalDateTime firstTimestamp = topicDetails\n        .openDetailsTab(MESSAGES)\n        .getMessageByOffset(0).getTimestamp();\n    LocalDateTime nextTimestamp = topicDetails.getAllMessages().stream()\n        .filter(message -> message.getTimestamp().getMinute() != firstTimestamp.getMinute())\n        .findFirst().orElseThrow().getTimestamp();\n    topicDetails\n        .selectSeekTypeDdlMessagesTab(\"Timestamp\")\n        .openCalendarSeekType()\n        .selectDateAndTimeByCalendar(nextTimestamp)\n        .clickSubmitFiltersBtnMessagesTab();\n    SoftAssert softly = new SoftAssert();\n    topicDetails.getAllMessages().forEach(message ->\n        softly.assertFalse(message.getTimestamp().isBefore(nextTimestamp),\n            String.format(\"Expected that %s is not before %s.\", message.getTimestamp(), nextTimestamp)));\n    softly.assertAll();\n  }\n\n  @QaseId(246)\n  @Test(priority = 8)\n  public void checkClearTopicMessageFromOverviewTab() {\n    navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName());\n    topicDetails\n        .openDetailsTab(OVERVIEW)\n        .openDotMenu()\n        .clickClearMessagesMenu()\n        .clickConfirmBtnMdl();\n    SoftAssert softly = new SoftAssert();\n    softly.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS,\n            String.format(\"%s messages have been successfully cleared!\", TOPIC_FOR_CHECK_FILTERS.getName())),\n        \"isAlertWithMessageVisible()\");\n    softly.assertEquals(topicDetails.getMessageCountAmount(), 0,\n        \"getMessageCountAmount()= \" + topicDetails.getMessageCountAmount());\n    softly.assertAll();\n  }\n\n  @QaseId(240)\n  @Test(priority = 9)\n  public void checkRecreateTopic() {\n    navigateToTopicsAndOpenDetails(TOPIC_TO_RECREATE.getName());\n    topicDetails\n        .openDetailsTab(OVERVIEW);\n    produceMessage(TOPIC_TO_RECREATE);\n    navigateToTopics();\n    Assert.assertEquals(topicsList.getTopicItem(TOPIC_TO_RECREATE.getName()).getNumberOfMessages(), 1,\n        \"getNumberOfMessages()\");\n    topicsList\n        .openDotMenuByTopicName(TOPIC_TO_RECREATE.getName())\n        .clickRecreateTopicBtn()\n        .clickConfirmBtnMdl();\n    SoftAssert softly = new SoftAssert();\n    softly.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS,\n            String.format(\"Topic %s successfully recreated!\", TOPIC_TO_RECREATE.getName())),\n        \"isAlertWithMessageVisible()\");\n    softly.assertEquals(topicsList.getTopicItem(TOPIC_TO_RECREATE.getName()).getNumberOfMessages(), 0,\n        \"getNumberOfMessages()\");\n    softly.assertAll();\n  }\n\n  @Ignore\n  @Issue(\"https://github.com/provectus/kafka-ui/issues/3129\")\n  @QaseId(267)\n  @Test(priority = 10)\n  public void checkMessagesCountPerPageWithinTopic() {\n    navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_MESSAGES_COUNT.getName());\n    topicDetails\n        .openDetailsTab(MESSAGES);\n    int messagesPerPage = topicDetails.getAllMessages().size();\n    SoftAssert softly = new SoftAssert();\n    softly.assertEquals(messagesPerPage, 100, \"getAllMessages()\");\n    softly.assertFalse(topicDetails.isBackButtonEnabled(), \"isBackButtonEnabled()\");\n    softly.assertTrue(topicDetails.isNextButtonEnabled(), \"isNextButtonEnabled()\");\n    softly.assertAll();\n    int lastOffsetOnPage = topicDetails.getAllMessages()\n        .get(messagesPerPage - 1).getOffset();\n    topicDetails\n        .clickNextButton();\n    softly.assertEquals(topicDetails.getAllMessages().stream().findFirst().orElseThrow().getOffset(),\n        lastOffsetOnPage + 1, \"findFirst().getOffset()\");\n    softly.assertTrue(topicDetails.isBackButtonEnabled(), \"isBackButtonEnabled()\");\n    softly.assertFalse(topicDetails.isNextButtonEnabled(), \"isNextButtonEnabled()\");\n    softly.assertAll();\n  }\n\n  @Step\n  private void produceMessage(Topic topic) {\n    topicDetails\n        .clickProduceMessageBtn();\n    produceMessagePanel\n        .waitUntilScreenReady()\n        .setKeyField(topic.getMessageKey())\n        .setValueFiled(topic.getMessageValue())\n        .submitProduceMessage();\n    topicDetails\n        .waitUntilScreenReady();\n  }\n\n  @AfterClass(alwaysRun = true)\n  public void afterClass() {\n    TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName()));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/TopicsTest.java",
    "content": "package com.provectus.kafka.ui.smokesuite.topics;\n\nimport static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS;\nimport static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.CONSUMERS;\nimport static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.MESSAGES;\nimport static com.provectus.kafka.ui.pages.topics.TopicDetails.TopicMenu.SETTINGS;\nimport static com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue.COMPACT;\nimport static com.provectus.kafka.ui.pages.topics.enums.CleanupPolicyValue.DELETE;\nimport static com.provectus.kafka.ui.pages.topics.enums.CustomParameterType.COMPRESSION_TYPE;\nimport static com.provectus.kafka.ui.pages.topics.enums.MaxSizeOnDisk.NOT_SET;\nimport static com.provectus.kafka.ui.pages.topics.enums.MaxSizeOnDisk.SIZE_1_GB;\nimport static com.provectus.kafka.ui.pages.topics.enums.MaxSizeOnDisk.SIZE_50_GB;\nimport static com.provectus.kafka.ui.pages.topics.enums.TimeToRetain.BTN_2_DAYS;\nimport static com.provectus.kafka.ui.pages.topics.enums.TimeToRetain.BTN_7_DAYS;\nimport static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;\nimport static org.apache.commons.lang3.RandomUtils.nextInt;\n\nimport com.codeborne.selenide.Condition;\nimport com.provectus.kafka.ui.BaseTest;\nimport com.provectus.kafka.ui.models.Topic;\nimport io.qameta.allure.Issue;\nimport io.qase.api.annotation.QaseId;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.testng.Assert;\nimport org.testng.annotations.AfterClass;\nimport org.testng.annotations.BeforeClass;\nimport org.testng.annotations.Ignore;\nimport org.testng.annotations.Test;\nimport org.testng.asserts.SoftAssert;\n\npublic class TopicsTest extends BaseTest {\n\n  private static final Topic TOPIC_TO_CREATE = new Topic()\n      .setName(\"new-topic-\" + randomAlphabetic(5))\n      .setNumberOfPartitions(1)\n      .setCustomParameterType(COMPRESSION_TYPE)\n      .setCustomParameterValue(\"producer\")\n      .setCleanupPolicyValue(DELETE);\n  private static final Topic TOPIC_TO_UPDATE_AND_DELETE = new Topic()\n      .setName(\"topic-to-update-and-delete-\" + randomAlphabetic(5))\n      .setNumberOfPartitions(1)\n      .setCleanupPolicyValue(DELETE)\n      .setTimeToRetain(BTN_7_DAYS)\n      .setMaxSizeOnDisk(NOT_SET)\n      .setMaxMessageBytes(\"1048588\")\n      .setMessageKey(randomAlphabetic(5))\n      .setMessageValue(randomAlphabetic(10));\n  private static final Topic TOPIC_TO_CHECK_SETTINGS = new Topic()\n      .setName(\"new-topic-\" + randomAlphabetic(5))\n      .setNumberOfPartitions(1)\n      .setMaxMessageBytes(\"1000012\")\n      .setMaxSizeOnDisk(NOT_SET);\n  private static final Topic TOPIC_FOR_CHECK_FILTERS = new Topic()\n      .setName(\"topic-for-check-filters-\" + randomAlphabetic(5));\n  private static final Topic TOPIC_FOR_DELETE = new Topic()\n      .setName(\"topic-to-delete-\" + randomAlphabetic(5));\n  private static final List<Topic> TOPIC_LIST = new ArrayList<>();\n\n  @BeforeClass(alwaysRun = true)\n  public void beforeClass() {\n    TOPIC_LIST.addAll(List.of(TOPIC_TO_UPDATE_AND_DELETE, TOPIC_FOR_DELETE, TOPIC_FOR_CHECK_FILTERS));\n    TOPIC_LIST.forEach(topic -> apiService.createTopic(topic));\n  }\n\n  @QaseId(199)\n  @Test(priority = 1)\n  public void createTopic() {\n    navigateToTopics();\n    topicsList\n        .clickAddTopicBtn();\n    topicCreateEditForm\n        .waitUntilScreenReady()\n        .setTopicName(TOPIC_TO_CREATE.getName())\n        .setNumberOfPartitions(TOPIC_TO_CREATE.getNumberOfPartitions())\n        .selectCleanupPolicy(TOPIC_TO_CREATE.getCleanupPolicyValue())\n        .clickSaveTopicBtn();\n    navigateToTopicsAndOpenDetails(TOPIC_TO_CREATE.getName());\n    SoftAssert softly = new SoftAssert();\n    softly.assertTrue(topicDetails.isTopicHeaderVisible(TOPIC_TO_CREATE.getName()), \"isTopicHeaderVisible()\");\n    softly.assertEquals(topicDetails.getCleanUpPolicy(), TOPIC_TO_CREATE.getCleanupPolicyValue().toString(),\n        \"getCleanUpPolicy()\");\n    softly.assertEquals(topicDetails.getPartitions(), TOPIC_TO_CREATE.getNumberOfPartitions(), \"getPartitions()\");\n    softly.assertAll();\n    navigateToTopics();\n    Assert.assertTrue(topicsList.isTopicVisible(TOPIC_TO_CREATE.getName()), \"isTopicVisible()\");\n    TOPIC_LIST.add(TOPIC_TO_CREATE);\n  }\n\n  @QaseId(7)\n  @Test(priority = 2)\n  void checkAvailableOperations() {\n    navigateToTopics();\n    topicsList\n        .getTopicItem(TOPIC_TO_UPDATE_AND_DELETE.getName())\n        .selectItem(true);\n    verifyElementsCondition(topicsList.getActionButtons(), Condition.enabled);\n    topicsList\n        .getTopicItem(TOPIC_FOR_CHECK_FILTERS.getName())\n        .selectItem(true);\n    Assert.assertFalse(topicsList.isCopySelectedTopicBtnEnabled(), \"isCopySelectedTopicBtnEnabled()\");\n  }\n\n  @Ignore\n  @Issue(\"https://github.com/provectus/kafka-ui/issues/3071\")\n  @QaseId(268)\n  @Test(priority = 3)\n  public void checkCustomParametersWithinEditExistingTopic() {\n    navigateToTopicsAndOpenDetails(TOPIC_TO_UPDATE_AND_DELETE.getName());\n    topicDetails\n        .openDotMenu()\n        .clickEditSettingsMenu();\n    SoftAssert softly = new SoftAssert();\n    topicCreateEditForm\n        .waitUntilScreenReady()\n        .clickAddCustomParameterTypeButton()\n        .openCustomParameterTypeDdl()\n        .getAllDdlOptions()\n        .forEach(option ->\n            softly.assertTrue(!option.is(Condition.attribute(\"disabled\")),\n                option.getText() + \" is enabled:\"));\n    softly.assertAll();\n  }\n\n  @QaseId(197)\n  @Test(priority = 4)\n  public void updateTopic() {\n    navigateToTopicsAndOpenDetails(TOPIC_TO_UPDATE_AND_DELETE.getName());\n    topicDetails\n        .openDotMenu()\n        .clickEditSettingsMenu();\n    topicCreateEditForm\n        .waitUntilScreenReady();\n    SoftAssert softly = new SoftAssert();\n    softly.assertEquals(topicCreateEditForm.getCleanupPolicy(),\n        TOPIC_TO_UPDATE_AND_DELETE.getCleanupPolicyValue().getVisibleText(), \"getCleanupPolicy()\");\n    softly.assertEquals(topicCreateEditForm.getTimeToRetain(),\n        TOPIC_TO_UPDATE_AND_DELETE.getTimeToRetain().getValue(), \"getTimeToRetain()\");\n    softly.assertEquals(topicCreateEditForm.getMaxSizeOnDisk(),\n        TOPIC_TO_UPDATE_AND_DELETE.getMaxSizeOnDisk().getVisibleText(), \"getMaxSizeOnDisk()\");\n    softly.assertEquals(topicCreateEditForm.getMaxMessageBytes(),\n        TOPIC_TO_UPDATE_AND_DELETE.getMaxMessageBytes(), \"getMaxMessageBytes()\");\n    softly.assertAll();\n    TOPIC_TO_UPDATE_AND_DELETE\n        .setCleanupPolicyValue(COMPACT)\n        .setTimeToRetain(BTN_2_DAYS)\n        .setMaxSizeOnDisk(SIZE_50_GB).setMaxMessageBytes(\"1048589\");\n    topicCreateEditForm\n        .selectCleanupPolicy((TOPIC_TO_UPDATE_AND_DELETE.getCleanupPolicyValue()))\n        .setTimeToRetainDataByButtons(TOPIC_TO_UPDATE_AND_DELETE.getTimeToRetain())\n        .setMaxSizeOnDiskInGB(TOPIC_TO_UPDATE_AND_DELETE.getMaxSizeOnDisk())\n        .setMaxMessageBytes(TOPIC_TO_UPDATE_AND_DELETE.getMaxMessageBytes())\n        .clickSaveTopicBtn();\n    softly.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, \"Topic successfully updated.\"),\n        \"isAlertWithMessageVisible()\");\n    softly.assertTrue(topicDetails.isTopicHeaderVisible(TOPIC_TO_UPDATE_AND_DELETE.getName()),\n        \"isTopicHeaderVisible()\");\n    softly.assertAll();\n    topicDetails\n        .waitUntilScreenReady();\n    navigateToTopicsAndOpenDetails(TOPIC_TO_UPDATE_AND_DELETE.getName());\n    topicDetails\n        .openDotMenu()\n        .clickEditSettingsMenu();\n    softly.assertFalse(topicCreateEditForm.isNameFieldEnabled(), \"isNameFieldEnabled()\");\n    softly.assertEquals(topicCreateEditForm.getCleanupPolicy(),\n        TOPIC_TO_UPDATE_AND_DELETE.getCleanupPolicyValue().getVisibleText(), \"getCleanupPolicy()\");\n    softly.assertEquals(topicCreateEditForm.getTimeToRetain(),\n        TOPIC_TO_UPDATE_AND_DELETE.getTimeToRetain().getValue(), \"getTimeToRetain()\");\n    softly.assertEquals(topicCreateEditForm.getMaxSizeOnDisk(),\n        TOPIC_TO_UPDATE_AND_DELETE.getMaxSizeOnDisk().getVisibleText(), \"getMaxSizeOnDisk()\");\n    softly.assertEquals(topicCreateEditForm.getMaxMessageBytes(),\n        TOPIC_TO_UPDATE_AND_DELETE.getMaxMessageBytes(), \"getMaxMessageBytes()\");\n    softly.assertAll();\n  }\n\n  @QaseId(242)\n  @Test(priority = 5)\n  public void removeTopicFromTopicList() {\n    navigateToTopics();\n    topicsList\n        .openDotMenuByTopicName(TOPIC_TO_UPDATE_AND_DELETE.getName())\n        .clickRemoveTopicBtn()\n        .clickConfirmBtnMdl();\n    Assert.assertTrue(topicsList.isAlertWithMessageVisible(SUCCESS,\n            String.format(\"Topic %s successfully deleted!\", TOPIC_TO_UPDATE_AND_DELETE.getName())),\n        \"isAlertWithMessageVisible()\");\n    TOPIC_LIST.remove(TOPIC_TO_UPDATE_AND_DELETE);\n  }\n\n  @QaseId(207)\n  @Test(priority = 6)\n  public void deleteTopic() {\n    navigateToTopicsAndOpenDetails(TOPIC_FOR_DELETE.getName());\n    topicDetails\n        .openDotMenu()\n        .clickDeleteTopicMenu()\n        .clickConfirmBtnMdl();\n    navigateToTopics();\n    Assert.assertFalse(topicsList.isTopicVisible(TOPIC_FOR_DELETE.getName()), \"isTopicVisible\");\n    TOPIC_LIST.remove(TOPIC_FOR_DELETE);\n  }\n\n  @QaseId(20)\n  @Test(priority = 7)\n  public void redirectToConsumerFromTopic() {\n    String topicName = \"source-activities\";\n    String consumerGroupId = \"connect-sink_postgres_activities\";\n    navigateToTopicsAndOpenDetails(topicName);\n    topicDetails\n        .openDetailsTab(CONSUMERS)\n        .openConsumerGroup(consumerGroupId);\n    consumersDetails\n        .waitUntilScreenReady();\n    SoftAssert softly = new SoftAssert();\n    softly.assertTrue(consumersDetails.isRedirectedConsumerTitleVisible(consumerGroupId),\n        \"isRedirectedConsumerTitleVisible()\");\n    softly.assertTrue(consumersDetails.isTopicInConsumersDetailsVisible(topicName),\n        \"isTopicInConsumersDetailsVisible()\");\n    softly.assertAll();\n  }\n\n  @QaseId(4)\n  @Test(priority = 8)\n  public void checkTopicCreatePossibility() {\n    navigateToTopics();\n    topicsList\n        .clickAddTopicBtn();\n    topicCreateEditForm\n        .waitUntilScreenReady();\n    Assert.assertFalse(topicCreateEditForm.isCreateTopicButtonEnabled(), \"isCreateTopicButtonEnabled()\");\n    topicCreateEditForm\n        .setTopicName(\"testName\");\n    Assert.assertFalse(topicCreateEditForm.isCreateTopicButtonEnabled(), \"isCreateTopicButtonEnabled()\");\n    topicCreateEditForm\n        .setTopicName(null)\n        .setNumberOfPartitions(nextInt(1, 10));\n    Assert.assertFalse(topicCreateEditForm.isCreateTopicButtonEnabled(), \"isCreateTopicButtonEnabled()\");\n    topicCreateEditForm\n        .setTopicName(\"testName\");\n    Assert.assertTrue(topicCreateEditForm.isCreateTopicButtonEnabled(), \"isCreateTopicButtonEnabled()\");\n  }\n\n  @QaseId(266)\n  @Test(priority = 9)\n  public void checkTimeToRetainDataCustomValueWithEditingTopic() {\n    Topic topicToRetainData = new Topic()\n        .setName(\"topic-to-retain-data-\" + randomAlphabetic(5))\n        .setTimeToRetainData(\"86400000\");\n    navigateToTopics();\n    topicsList\n        .clickAddTopicBtn();\n    topicCreateEditForm\n        .waitUntilScreenReady()\n        .setTopicName(topicToRetainData.getName())\n        .setNumberOfPartitions(1)\n        .setTimeToRetainDataInMs(\"604800000\");\n    Assert.assertEquals(topicCreateEditForm.getTimeToRetain(), \"604800000\", \"getTimeToRetain()\");\n    topicCreateEditForm\n        .setTimeToRetainDataInMs(topicToRetainData.getTimeToRetainData())\n        .clickSaveTopicBtn();\n    topicDetails\n        .waitUntilScreenReady()\n        .openDotMenu()\n        .clickEditSettingsMenu();\n    Assert.assertEquals(topicCreateEditForm.getTimeToRetain(), topicToRetainData.getTimeToRetainData(),\n        \"getTimeToRetain()\");\n    topicDetails\n        .openDetailsTab(SETTINGS);\n    Assert.assertEquals(topicDetails.getSettingsGridValueByKey(\"retention.ms\"), topicToRetainData.getTimeToRetainData(),\n        \"getSettingsGridValueByKey()\");\n    TOPIC_LIST.add(topicToRetainData);\n  }\n\n  @QaseId(6)\n  @Test(priority = 10)\n  public void checkCustomParametersWithinCreateNewTopic() {\n    navigateToTopics();\n    topicsList\n        .clickAddTopicBtn();\n    topicCreateEditForm\n        .waitUntilScreenReady()\n        .setTopicName(TOPIC_TO_CREATE.getName())\n        .clickAddCustomParameterTypeButton()\n        .setCustomParameterType(TOPIC_TO_CREATE.getCustomParameterType());\n    Assert.assertTrue(topicCreateEditForm.isDeleteCustomParameterButtonEnabled(),\n        \"isDeleteCustomParameterButtonEnabled()\");\n    topicCreateEditForm\n        .clearCustomParameterValue();\n    Assert.assertTrue(topicCreateEditForm.isValidationMessageCustomParameterValueVisible(),\n        \"isValidationMessageCustomParameterValueVisible()\");\n  }\n\n  @QaseId(2)\n  @Test(priority = 11)\n  public void checkTopicListElements() {\n    navigateToTopics();\n    verifyElementsCondition(topicsList.getAllVisibleElements(), Condition.visible);\n    verifyElementsCondition(topicsList.getAllEnabledElements(), Condition.enabled);\n  }\n\n  @QaseId(12)\n  @Test(priority = 12)\n  public void addNewFilterWithinTopic() {\n    String filterName = randomAlphabetic(5);\n    navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName());\n    topicDetails\n        .openDetailsTab(MESSAGES)\n        .clickMessagesAddFiltersBtn()\n        .waitUntilAddFiltersMdlVisible();\n    verifyElementsCondition(topicDetails.getAllAddFilterModalVisibleElements(), Condition.visible);\n    verifyElementsCondition(topicDetails.getAllAddFilterModalEnabledElements(), Condition.enabled);\n    verifyElementsCondition(topicDetails.getAllAddFilterModalDisabledElements(), Condition.disabled);\n    Assert.assertFalse(topicDetails.isSaveThisFilterCheckBoxSelected(), \"isSaveThisFilterCheckBoxSelected()\");\n    topicDetails\n        .setFilterCodeFldAddFilterMdl(filterName);\n    Assert.assertTrue(topicDetails.isAddFilterBtnAddFilterMdlEnabled(), \"isAddFilterBtnAddFilterMdlEnabled()\");\n    topicDetails.clickAddFilterBtnAndCloseMdl(true);\n    Assert.assertTrue(topicDetails.isActiveFilterVisible(filterName), \"isActiveFilterVisible()\");\n  }\n\n  @QaseId(352)\n  @Test(priority = 13)\n  public void editActiveSmartFilterCheck() {\n    String filterName = randomAlphabetic(5);\n    String filterCode = randomAlphabetic(5);\n    navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName());\n    topicDetails\n        .openDetailsTab(MESSAGES)\n        .clickMessagesAddFiltersBtn()\n        .waitUntilAddFiltersMdlVisible()\n        .setFilterCodeFldAddFilterMdl(filterCode)\n        .setDisplayNameFldAddFilterMdl(filterName)\n        .clickAddFilterBtnAndCloseMdl(true)\n        .clickEditActiveFilterBtn(filterName)\n        .waitUntilAddFiltersMdlVisible();\n    SoftAssert softly = new SoftAssert();\n    softly.assertEquals(topicDetails.getFilterCodeValue(), filterCode, \"getFilterCodeValue()\");\n    softly.assertEquals(topicDetails.getFilterNameValue(), filterName, \"getFilterNameValue()\");\n    softly.assertAll();\n    String newFilterName = randomAlphabetic(5);\n    String newFilterCode = randomAlphabetic(5);\n    topicDetails\n        .setFilterCodeFldAddFilterMdl(newFilterCode)\n        .setDisplayNameFldAddFilterMdl(newFilterName)\n        .clickSaveFilterBtnAndCloseMdl(true);\n    softly.assertTrue(topicDetails.isActiveFilterVisible(newFilterName), \"isActiveFilterVisible()\");\n    softly.assertEquals(topicDetails.getSearchFieldValue(), newFilterCode, \"getSearchFieldValue()\");\n    softly.assertAll();\n  }\n\n  @QaseId(13)\n  @Test(priority = 14)\n  public void checkFilterSavingWithinSavedFilters() {\n    String displayName = randomAlphabetic(5);\n    navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName());\n    topicDetails\n        .openDetailsTab(MESSAGES)\n        .clickMessagesAddFiltersBtn()\n        .waitUntilAddFiltersMdlVisible()\n        .setFilterCodeFldAddFilterMdl(randomAlphabetic(4))\n        .selectSaveThisFilterCheckboxMdl(true)\n        .setDisplayNameFldAddFilterMdl(displayName);\n    Assert.assertTrue(topicDetails.isAddFilterBtnAddFilterMdlEnabled(),\n        \"isAddFilterBtnAddFilterMdlEnabled()\");\n    topicDetails\n        .clickAddFilterBtnAndCloseMdl(false)\n        .openSavedFiltersListMdl();\n    Assert.assertTrue(topicDetails.isFilterVisibleAtSavedFiltersMdl(displayName),\n        \"isFilterVisibleAtSavedFiltersMdl()\");\n  }\n\n  @QaseId(14)\n  @Test(priority = 15)\n  public void checkApplyingSavedFilterWithinTopicMessages() {\n    String displayName = randomAlphabetic(5);\n    navigateToTopicsAndOpenDetails(TOPIC_FOR_CHECK_FILTERS.getName());\n    topicDetails\n        .openDetailsTab(MESSAGES)\n        .clickMessagesAddFiltersBtn()\n        .waitUntilAddFiltersMdlVisible()\n        .setFilterCodeFldAddFilterMdl(randomAlphabetic(4))\n        .selectSaveThisFilterCheckboxMdl(true)\n        .setDisplayNameFldAddFilterMdl(displayName)\n        .clickAddFilterBtnAndCloseMdl(false)\n        .openSavedFiltersListMdl()\n        .selectFilterAtSavedFiltersMdl(displayName)\n        .clickSelectFilterBtnAtSavedFiltersMdl();\n    Assert.assertTrue(topicDetails.isActiveFilterVisible(displayName), \"isActiveFilterVisible()\");\n  }\n\n  @QaseId(11)\n  @Test(priority = 16)\n  public void checkShowInternalTopicsButton() {\n    navigateToTopics();\n    topicsList\n        .setShowInternalRadioButton(true);\n    Assert.assertTrue(topicsList.getInternalTopics().size() > 0, \"getInternalTopics()\");\n    topicsList\n        .goToLastPage();\n    Assert.assertTrue(topicsList.getNonInternalTopics().size() > 0, \"getNonInternalTopics()\");\n    topicsList\n        .setShowInternalRadioButton(false);\n    SoftAssert softly = new SoftAssert();\n    softly.assertEquals(topicsList.getInternalTopics().size(), 0, \"getInternalTopics()\");\n    softly.assertTrue(topicsList.getNonInternalTopics().size() > 0, \"getNonInternalTopics()\");\n    softly.assertAll();\n  }\n\n  @QaseId(334)\n  @Test(priority = 17)\n  public void checkInternalTopicsNaming() {\n    navigateToTopics();\n    SoftAssert softly = new SoftAssert();\n    topicsList\n        .setShowInternalRadioButton(true)\n        .getInternalTopics()\n        .forEach(topic -> softly.assertTrue(topic.getName().startsWith(\"_\"),\n            String.format(\"'%s' starts with '_'\", topic.getName())));\n    softly.assertAll();\n  }\n\n  @QaseId(56)\n  @Test(priority = 18)\n  public void checkRetentionBytesAccordingToMaxSizeOnDisk() {\n    navigateToTopics();\n    topicsList\n        .clickAddTopicBtn();\n    topicCreateEditForm\n        .waitUntilScreenReady()\n        .setTopicName(TOPIC_TO_CHECK_SETTINGS.getName())\n        .setNumberOfPartitions(TOPIC_TO_CHECK_SETTINGS.getNumberOfPartitions())\n        .setMaxMessageBytes(TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes())\n        .clickSaveTopicBtn();\n    topicDetails\n        .waitUntilScreenReady();\n    TOPIC_LIST.add(TOPIC_TO_CHECK_SETTINGS);\n    topicDetails\n        .openDetailsTab(SETTINGS);\n    topicSettingsTab\n        .waitUntilScreenReady();\n    SoftAssert softly = new SoftAssert();\n    softly.assertEquals(topicSettingsTab.getValueByKey(\"retention.bytes\"),\n        TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk().getOptionValue(), \"getValueOfKey(retention.bytes)\");\n    softly.assertEquals(topicSettingsTab.getValueByKey(\"max.message.bytes\"),\n        TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes(), \"getValueOfKey(max.message.bytes)\");\n    softly.assertAll();\n    TOPIC_TO_CHECK_SETTINGS\n        .setMaxSizeOnDisk(SIZE_1_GB)\n        .setMaxMessageBytes(\"1000056\");\n    topicDetails\n        .openDotMenu()\n        .clickEditSettingsMenu();\n    topicCreateEditForm\n        .waitUntilScreenReady()\n        .setMaxSizeOnDiskInGB(TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk())\n        .setMaxMessageBytes(TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes())\n        .clickSaveTopicBtn();\n    topicDetails\n        .waitUntilScreenReady()\n        .openDetailsTab(SETTINGS);\n    topicSettingsTab\n        .waitUntilScreenReady();\n    softly.assertEquals(topicSettingsTab.getValueByKey(\"retention.bytes\"),\n        TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk().getOptionValue(), \"getValueOfKey(retention.bytes)\");\n    softly.assertEquals(topicSettingsTab.getValueByKey(\"max.message.bytes\"),\n        TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes(), \"getValueOfKey(max.message.bytes)\");\n    softly.assertAll();\n  }\n\n  @QaseId(247)\n  @Test(priority = 19)\n  public void recreateTopicFromTopicProfile() {\n    Topic topicToRecreate = new Topic()\n        .setName(\"topic-to-recreate-\" + randomAlphabetic(5))\n        .setNumberOfPartitions(1);\n    navigateToTopics();\n    topicsList\n        .clickAddTopicBtn();\n    topicCreateEditForm\n        .waitUntilScreenReady()\n        .setTopicName(topicToRecreate.getName())\n        .setNumberOfPartitions(topicToRecreate.getNumberOfPartitions())\n        .clickSaveTopicBtn();\n    topicDetails\n        .waitUntilScreenReady();\n    TOPIC_LIST.add(topicToRecreate);\n    topicDetails\n        .openDotMenu()\n        .clickRecreateTopicMenu();\n    Assert.assertTrue(topicDetails.isConfirmationMdlVisible(), \"isConfirmationMdlVisible()\");\n    topicDetails\n        .clickConfirmBtnMdl();\n    Assert.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS,\n            String.format(\"Topic %s successfully recreated!\", topicToRecreate.getName())),\n        \"isAlertWithMessageVisible()\");\n  }\n\n  @QaseId(8)\n  @Test(priority = 20)\n  public void checkCopyTopicPossibility() {\n    Topic topicToCopy = new Topic()\n        .setName(\"topic-to-copy-\" + randomAlphabetic(5))\n        .setNumberOfPartitions(1);\n    navigateToTopics();\n    topicsList\n        .getAnyNonInternalTopic()\n        .selectItem(true)\n        .clickCopySelectedTopicBtn();\n    topicCreateEditForm\n        .waitUntilScreenReady();\n    Assert.assertFalse(topicCreateEditForm.isCreateTopicButtonEnabled(), \"isCreateTopicButtonEnabled()\");\n    topicCreateEditForm\n        .setTopicName(topicToCopy.getName())\n        .setNumberOfPartitions(topicToCopy.getNumberOfPartitions())\n        .clickSaveTopicBtn();\n    topicDetails\n        .waitUntilScreenReady();\n    TOPIC_LIST.add(topicToCopy);\n    Assert.assertTrue(topicDetails.isTopicHeaderVisible(topicToCopy.getName()), \"isTopicHeaderVisible()\");\n  }\n\n  @AfterClass(alwaysRun = true)\n  public void afterClass() {\n    TOPIC_LIST.forEach(topic -> apiService.deleteTopic(topic.getName()));\n  }\n}\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/resources/manual.xml",
    "content": "<!DOCTYPE suite SYSTEM \"https://testng.org/testng-1.0.dtd\">\n<suite name=\"ManualSuite\">\n    <test name=\"ManualTest\" enabled=\"true\" parallel=\"classes\" thread-count=\"1\">\n        <packages>\n            <package name=\"com.provectus.kafka.ui.manualsuite.*\"/>\n        </packages>\n    </test>\n</suite>\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/resources/qase.xml",
    "content": "<!DOCTYPE suite SYSTEM \"https://testng.org/testng-1.0.dtd\">\n<suite name=\"QaseSuite\">\n    <test name=\"QaseTest\" enabled=\"true\" parallel=\"classes\" thread-count=\"1\">\n        <packages>\n            <package name=\"com.provectus.kafka.ui.qasesuite.*\"/>\n        </packages>\n    </test>\n</suite>\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/resources/regression.xml",
    "content": "<!DOCTYPE suite SYSTEM \"https://testng.org/testng-1.0.dtd\">\n<suite name=\"RegressionSuite\">\n    <test name=\"RegressionTest\" enabled=\"true\" parallel=\"classes\" thread-count=\"2\">\n        <packages>\n            <package name=\"com.provectus.kafka.ui.smokesuite.*\"/>\n            <package name=\"com.provectus.kafka.ui.sanitysuite.*\"/>\n            <package name=\"com.provectus.kafka.ui.manualsuite.*\"/>\n        </packages>\n    </test>\n</suite>\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/resources/sanity.xml",
    "content": "<!DOCTYPE suite SYSTEM \"https://testng.org/testng-1.0.dtd\">\n<suite name=\"SanitySuite\">\n    <test name=\"SanityTest\" enabled=\"true\" parallel=\"classes\" thread-count=\"2\">\n        <packages>\n            <package name=\"com.provectus.kafka.ui.sanitysuite.*\"/>\n        </packages>\n    </test>\n</suite>\n"
  },
  {
    "path": "kafka-ui-e2e-checks/src/test/resources/smoke.xml",
    "content": "<!DOCTYPE suite SYSTEM \"https://testng.org/testng-1.0.dtd\">\n<suite name=\"SmokeSuite\">\n    <test name=\"SmokeTest\" enabled=\"true\" parallel=\"classes\" thread-count=\"2\">\n        <packages>\n            <package name=\"com.provectus.kafka.ui.smokesuite.*\"/>\n        </packages>\n    </test>\n</suite>\n"
  },
  {
    "path": "kafka-ui-react-app/.editorconfig",
    "content": "root = true\n\n[*]\nend_of_line = lf\nindent_style = space\nindent_size = 2\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n"
  },
  {
    "path": "kafka-ui-react-app/.eslintignore",
    "content": "/src/generated-sources/**\n"
  },
  {
    "path": "kafka-ui-react-app/.eslintrc.json",
    "content": "{\n  \"env\": {\n    \"browser\": true,\n    \"es6\": true,\n    \"jest\": true\n  },\n  \"globals\": {\n    \"Atomics\": \"readonly\",\n    \"SharedArrayBuffer\": \"readonly\"\n  },\n  \"parser\": \"@typescript-eslint/parser\",\n  \"parserOptions\": {\n    \"ecmaFeatures\": {\n      \"jsx\": true\n    },\n    \"ecmaVersion\": 2018,\n    \"sourceType\": \"module\",\n    \"project\": [\n      \"./tsconfig.json\",\n      \"./src/setupTests.ts\"\n    ]\n  },\n  \"plugins\": [\n    \"react\",\n    \"@typescript-eslint\",\n    \"prettier\",\n    \"react-hooks\"\n  ],\n  \"extends\": [\n    \"airbnb\",\n    \"airbnb-typescript\",\n    \"plugin:@typescript-eslint/recommended\",\n    \"plugin:jest-dom/recommended\",\n    \"plugin:prettier/recommended\",\n    \"eslint:recommended\",\n    \"plugin:react/recommended\",\n    \"prettier\"\n  ],\n  \"rules\": {\n    \"react/no-unused-prop-types\": \"off\",\n    \"react/require-default-props\": \"off\",\n    \"prettier/prettier\": \"warn\",\n    \"@typescript-eslint/explicit-module-boundary-types\": \"off\",\n    \"jsx-a11y/label-has-associated-control\": \"off\",\n    \"import/prefer-default-export\": \"off\",\n    \"@typescript-eslint/no-explicit-any\": \"error\",\n    \"react-hooks/rules-of-hooks\": \"error\", // Checks rules of Hooks\n    // breaks builds as we still have those warns\n    \"react-hooks/exhaustive-deps\": \"off\", // Checks effect dependencies\n    \"import/no-extraneous-dependencies\": [\n      \"error\",\n      {\n        \"devDependencies\": true\n      }\n    ],\n    \"import/no-cycle\": \"error\",\n    \"import/order\": [\n      \"error\",\n      {\n        \"groups\": [\n          \"builtin\",\n          \"external\",\n          \"parent\",\n          \"sibling\",\n          \"index\"\n        ],\n        \"newlines-between\": \"always\"\n      }\n    ],\n    \"import/no-relative-parent-imports\": \"error\",\n    \"no-debugger\": \"warn\",\n    \"react/jsx-props-no-spreading\": \"off\",\n    \"no-param-reassign\": [\n      \"error\",\n      {\n        \"props\": true,\n        \"ignorePropertyModificationsFor\": [\n          \"state\"\n        ]\n      }\n    ],\n    \"react/function-component-definition\": [\n      2,\n      {\n        \"namedComponents\": \"arrow-function\",\n        \"unnamedComponents\": \"arrow-function\"\n      }\n    ],\n    \"react/jsx-no-constructed-context-values\": \"off\",\n    \"react/display-name\": \"off\"\n  },\n  \"overrides\": [\n    {\n      \"files\": [\n        \"**/*.tsx\"\n      ],\n      \"rules\": {\n        \"react/prop-types\": \"off\"\n      }\n    },\n    {\n      \"files\": [\n        \"*.spec.tsx\"\n      ],\n      \"rules\": {\n        \"react/jsx-props-no-spreading\": \"off\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "kafka-ui-react-app/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\nnode_modules\n.pnp\n.pnp.js\nnode\n\npackage-lock.json\n\n# testing\ncoverage\n\n# production\nbuild\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\npnpm-debug.log*\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n.idea\n\n# generated sources\nsrc/generated-sources\n\n.eslintcache\n"
  },
  {
    "path": "kafka-ui-react-app/.jest/cssTransform.js",
    "content": "'use strict';\n\n// This is a custom Jest transformer turning style imports into empty objects.\n// http://facebook.github.io/jest/docs/en/webpack.html\n\nmodule.exports = {\n  process() {\n    return {\n      code: 'module.exports = {};',\n    };\n  },\n  getCacheKey() {\n    // The output is always the same.\n    return 'cssTransform';\n  },\n};\n"
  },
  {
    "path": "kafka-ui-react-app/.jest/resolver.js",
    "content": "module.exports = (path, options) => {\n  // Call the defaultResolver, so we leverage its cache, error handling, etc.\n  return options.defaultResolver(path, {\n    ...options,\n    // Use packageFilter to process parsed `package.json` before\n    // the resolution (see https://www.npmjs.com/package/resolve#resolveid-opts-cb)\n    packageFilter: (pkg) => {\n      // jest-environment-jsdom 28+ tries to use browser exports instead of default exports,\n      // but @hookform/resolvers only offers an ESM browser export and not a CommonJS one. Jest does not yet\n      // support ESM modules natively, so this causes a Jest error related to trying to parse\n      // \"export\" syntax.\n      //\n      // This workaround prevents Jest from considering @hookform/resolvers module-based exports at all;\n      // it falls back to CommonJS+node \"main\" property.\n      if (pkg.name === '@hookform/resolvers') {\n        delete pkg['exports'];\n        delete pkg['module'];\n      }\n      if (pkg.name === 'jsonpath-plus') {\n        delete pkg['exports'];\n        delete pkg['module'];\n      }\n      return pkg;\n    },\n  });\n};\n"
  },
  {
    "path": "kafka-ui-react-app/.nvmrc",
    "content": "v18.17.1\n"
  },
  {
    "path": "kafka-ui-react-app/.prettierrc",
    "content": "{\n  \"trailingComma\": \"es5\",\n  \"semi\": true,\n  \"singleQuote\": true,\n  \"quoteProps\": \"as-needed\",\n  \"jsxSingleQuote\": false,\n  \"bracketSpacing\": true,\n  \"bracketSameLine\": false,\n  \"arrowParens\": \"always\"\n}\n"
  },
  {
    "path": "kafka-ui-react-app/README.md",
    "content": "# UI for Apache Kafka\nUI for Apache Kafka management\n\n[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=com.provectus%3Akafka-ui_frontend&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=com.provectus%3Akafka-ui_frontend)\n[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=com.provectus%3Akafka-ui_frontend&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=com.provectus%3Akafka-ui_frontend)\n[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=com.provectus%3Akafka-ui_frontend&metric=coverage)](https://sonarcloud.io/summary/new_code?id=com.provectus%3Akafka-ui_frontend)\n\n## Table of contents\n- [Requirements](#requirements)\n- [Getting started](#getting-started)\n- [Links](#links)\n\n## Requirements\n- [docker](https://www.docker.com/get-started) (required to run [Initialize application](#initialize-application))\n- [nvm](https://github.com/nvm-sh/nvm) with installed [Node.js](https://nodejs.org/en/) of expected version (check `.nvmrc`)\n\n## Getting started\n\nGo to react app folder\n```sh\ncd ./kafka-ui-react-app\n```\n\nInstall [pnpm](https://pnpm.io/installation)\n```\nnpm install -g pnpm\n```\n\nInstall dependencies\n```\npnpm install\n```\n\nGenerate API clients from OpenAPI document\n```sh\npnpm gen:sources\n```\n\n## Start application\n### Proxying API Requests in Development\n\nCreate or update existing `.env.local` file with\n```\nVITE_DEV_PROXY= https://api.server # your API server\n```\n\nRun the application\n```sh\npnpm dev\n```\n\n### Docker way\n\nHave to be run from root directory.\n\nStart UI for Apache Kafka with your Kafka clusters:\n```sh\ndocker-compose -f ./documentation/compose/kafka-ui.yaml up\n```\n\nMake sure that none of the `.env*` files contain `DEV_PROXY` variable\n\nRun the application\n```sh\npnpm dev\n```\n## Links\n\n* [Vite](https://github.com/vitejs/vite)\n"
  },
  {
    "path": "kafka-ui-react-app/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\n    <!-- Favicons -->\n    <link rel=\"icon\" href=\"<%= PUBLIC_PATH %>/favicon/favicon.ico\" sizes=\"any\" />\n    <link rel=\"icon\" href=\"<%= PUBLIC_PATH %>/favicon/icon.svg\" type=\"image/svg+xml\" />\n    <link rel=\"apple-touch-icon\" href=\"<%= PUBLIC_PATH %>/favicon/apple-touch-icon.png\" />\n    <link rel=\"manifest\" href=\"<%= PUBLIC_PATH %>/manifest.json\" />\n\n    <title>UI for Apache Kafka</title>\n    <script type=\"text/javascript\">\n      window.basePath = '<%= PUBLIC_PATH %>';\n\n      window.__assetsPathBuilder = function (importer) {\n        return window.basePath+ \"/\" + importer;\n      };\n    </script>\n    <style>\n      @font-face {\n        font-family: 'Inter';\n        src: url('<%= PUBLIC_PATH %>/fonts/Inter-Medium.ttf') format('truetype');\n        font-weight: 500;\n        font-display: swap;\n      }\n\n      @font-face {\n        font-family: 'Inter';\n        src: url('<%= PUBLIC_PATH %>/fonts/Inter-Regular.ttf') format('truetype');\n        font-weight: 400;\n        font-display: swap;\n      }\n\n      @font-face {\n        font-family: 'Roboto Mono';\n        src: url('<%= PUBLIC_PATH %>/fonts/RobotoMono-Medium.ttf') format('truetype');\n        font-weight: 500;\n        font-display: swap;\n      }\n\n      @font-face {\n        font-family: 'Roboto Mono';\n        src: url('<%= PUBLIC_PATH %>/fonts/RobotoMono-Regular.ttf') format('truetype');\n        font-weight: 400;\n        font-display: swap;\n      }\n    </style>\n  </head>\n\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/index.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "kafka-ui-react-app/jest.config.ts",
    "content": "import type { Config } from '@jest/types';\n\nexport default {\n  roots: ['<rootDir>/src'],\n  collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'],\n  coveragePathIgnorePatterns: [\n    '/node_modules/',\n    '<rootDir>/src/generated-sources/',\n    '<rootDir>/src/lib/fixtures/',\n    '<rootDir>/vite.config.ts',\n    '<rootDir>/src/index.tsx',\n    '<rootDir>/src/serviceWorker.ts',\n  ],\n  coverageReporters: ['json', 'lcov', 'text', 'clover'],\n  resolver: '<rootDir>/.jest/resolver.js',\n  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],\n  testMatch: [\n    '<rootDir>/src/**/__{test,tests}__/**/*.{spec,test}.{js,jsx,ts,tsx}',\n  ],\n  testEnvironment: 'jsdom',\n  transform: {\n    '\\\\.[jt]sx?$': '@swc/jest',\n    '^.+\\\\.css$': '<rootDir>/.jest/cssTransform.js',\n  },\n  transformIgnorePatterns: [\n    '[/\\\\\\\\]node_modules[/\\\\\\\\].+\\\\.(js|jsx|mjs|cjs|ts|tsx)$',\n    '^.+\\\\.module\\\\.(css|sass|scss)$',\n  ],\n  modulePaths: ['<rootDir>/src'],\n  watchPlugins: [\n    'jest-watch-typeahead/filename',\n    'jest-watch-typeahead/testname',\n  ],\n  resetMocks: true,\n  reporters: ['default', 'github-actions'],\n} as Config.InitialOptions;\n"
  },
  {
    "path": "kafka-ui-react-app/openapitools.json",
    "content": "{\n  \"$schema\": \"node_modules/@openapitools/openapi-generator-cli/config.schema.json\",\n  \"spaces\": 2,\n  \"generator-cli\": {\n    \"version\": \"5.3.0\",\n    \"generators\": {\n      \"fetch\": {\n        \"generatorName\": \"typescript-fetch\",\n        \"output\": \"src/generated-sources\",\n        \"glob\": \"../kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml\",\n        \"additionalProperties\": {\n          \"enumPropertyNaming\": \"UPPERCASE\",\n          \"typescriptThreePlus\": true,\n          \"supportsES6\": true,\n          \"nullSafeAdditionalProps\": true,\n          \"withInterfaces\": true\n        },\n        \"typeMappings\": {\n          \"object\": \"any\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "kafka-ui-react-app/package.json",
    "content": "{\n  \"name\": \"kafka-ui\",\n  \"version\": \"0.4.0\",\n  \"homepage\": \"./\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@floating-ui/react\": \"^0.19.2\",\n    \"@hookform/error-message\": \"^2.0.0\",\n    \"@hookform/resolvers\": \"^2.7.1\",\n    \"@microsoft/fetch-event-source\": \"^2.0.1\",\n    \"@reduxjs/toolkit\": \"^1.8.3\",\n    \"@szhsin/react-menu\": \"^3.5.3\",\n    \"@tanstack/react-query\": \"^4.0.5\",\n    \"@tanstack/react-table\": \"^8.5.10\",\n    \"@testing-library/react\": \"^14.0.0\",\n    \"@types/testing-library__jest-dom\": \"^5.14.5\",\n    \"ace-builds\": \"^1.7.1\",\n    \"ajv\": \"^8.6.3\",\n    \"ajv-formats\": \"^2.1.1\",\n    \"classnames\": \"^2.2.6\",\n    \"fetch-mock\": \"^9.11.0\",\n    \"jest\": \"^29.4.3\",\n    \"jest-watch-typeahead\": \"^2.2.2\",\n    \"json-schema-faker\": \"^0.5.0-rcv.44\",\n    \"jsonpath-plus\": \"^7.2.0\",\n    \"lodash\": \"^4.17.21\",\n    \"lossless-json\": \"^2.0.8\",\n    \"pretty-ms\": \"7.0.1\",\n    \"react\": \"^18.1.0\",\n    \"react-ace\": \"^10.1.0\",\n    \"react-datepicker\": \"^4.10.0\",\n    \"react-dom\": \"^18.1.0\",\n    \"react-error-boundary\": \"^3.1.4\",\n    \"react-hook-form\": \"7.43.1\",\n    \"react-hot-toast\": \"^2.4.0\",\n    \"react-is\": \"^18.2.0\",\n    \"react-multi-select-component\": \"^4.3.3\",\n    \"react-redux\": \"^8.0.2\",\n    \"react-router-dom\": \"^6.3.0\",\n    \"redux\": \"^4.2.0\",\n    \"sass\": \"^1.52.3\",\n    \"styled-components\": \"^5.3.1\",\n    \"use-debounce\": \"^9.0.3\",\n    \"vite\": \"^4.0.0\",\n    \"vite-tsconfig-paths\": \"^4.0.2\",\n    \"whatwg-fetch\": \"^3.6.2\",\n    \"yup\": \"^1.0.0\",\n    \"zustand\": \"^4.1.1\"\n  },\n  \"scripts\": {\n    \"start\": \"vite\",\n    \"dev\": \"vite\",\n    \"gen:sources\": \"rimraf src/generated-sources && openapi-generator-cli generate\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview\",\n    \"lint\": \"eslint --ext .tsx,.ts src/\",\n    \"lint:fix\": \"eslint --ext .tsx,.ts src/ --fix\",\n    \"lint:CI\": \"eslint --ext .tsx,.ts src/ --max-warnings=0\",\n    \"test\": \"jest --watch\",\n    \"test:coverage\": \"jest --watchAll --coverage\",\n    \"test:CI\": \"CI=true pnpm test:coverage --ci --testResultsProcessor=\\\"jest-sonar-reporter\\\" --watchAll=false\",\n    \"tsc\": \"tsc --pretty --noEmit\",\n    \"deadcode\": \"ts-prune -i src/generated-sources\"\n  },\n  \"devDependencies\": {\n    \"@jest/types\": \"^29.4.3\",\n    \"@openapitools/openapi-generator-cli\": \"^2.5.2\",\n    \"@swc/core\": \"^1.3.36\",\n    \"@swc/jest\": \"^0.2.24\",\n    \"@testing-library/dom\": \"^9.0.0\",\n    \"@testing-library/jest-dom\": \"^5.16.5\",\n    \"@testing-library/user-event\": \"^14.4.3\",\n    \"@types/eventsource\": \"^1.1.8\",\n    \"@types/lodash\": \"^4.14.172\",\n    \"@types/lossless-json\": \"^1.0.1\",\n    \"@types/node\": \"^16.4.13\",\n    \"@types/react\": \"^18.0.9\",\n    \"@types/react-datepicker\": \"^4.8.0\",\n    \"@types/react-dom\": \"^18.0.3\",\n    \"@types/react-router-dom\": \"^5.3.3\",\n    \"@types/styled-components\": \"^5.1.13\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.29.0\",\n    \"@typescript-eslint/parser\": \"^5.29.0\",\n    \"@vitejs/plugin-react-swc\": \"^3.0.0\",\n    \"dotenv\": \"^16.0.1\",\n    \"eslint\": \"^8.3.0\",\n    \"eslint-config-airbnb\": \"^19.0.4\",\n    \"eslint-config-airbnb-typescript\": \"^17.0.0\",\n    \"eslint-config-prettier\": \"^9.0.0\",\n    \"eslint-import-resolver-node\": \"^0.3.6\",\n    \"eslint-import-resolver-typescript\": \"^3.2.7\",\n    \"eslint-plugin-import\": \"^2.26.0\",\n    \"eslint-plugin-jest-dom\": \"^4.0.3\",\n    \"eslint-plugin-jsx-a11y\": \"^6.5.1\",\n    \"eslint-plugin-prettier\": \"^4.0.0\",\n    \"eslint-plugin-react\": \"^7.30.1\",\n    \"eslint-plugin-react-hooks\": \"^4.5.0\",\n    \"jest-environment-jsdom\": \"^29.4.3\",\n    \"jest-sonar-reporter\": \"^2.0.0\",\n    \"jest-styled-components\": \"^7.1.1\",\n    \"prettier\": \"^2.8.4\",\n    \"rimraf\": \"^4.1.2\",\n    \"ts-node\": \"^10.9.1\",\n    \"ts-prune\": \"^0.10.3\",\n    \"typescript\": \"^4.7.4\",\n    \"vite-plugin-ejs\": \"^1.6.4\"\n  },\n  \"engines\": {\n    \"node\": \"v18.17.1\",\n    \"pnpm\": \"^8.6.12\"\n  }\n}\n"
  },
  {
    "path": "kafka-ui-react-app/public/manifest.json",
    "content": "{\n  \"name\": \"UI for Apache Kafka\",\n  \"icons\": [\n    {\n      \"src\": \"/favicon/icon-192.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"192x192\"\n    },\n    {\n      \"src\": \"/favicon/icon-512.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"512x512\"\n    }\n  ]\n}\n"
  },
  {
    "path": "kafka-ui-react-app/public/robots.txt",
    "content": "User-agent: *\nDisallow: /\n"
  },
  {
    "path": "kafka-ui-react-app/sonar-project.properties",
    "content": "sonar.projectKey=com.provectus:kafka-ui_frontend\nsonar.organization=provectus\n\nsonar.sources=.\nsonar.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/**\n\nsonar.typescript.lcov.reportPaths=./coverage/lcov.info\nsonar.testExecutionReportPaths=./test-report.xml\n\nsonar.sourceEncoding=UTF-8\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ACLPage/ACLPage.tsx",
    "content": "import React from 'react';\nimport { Routes, Route } from 'react-router-dom';\nimport ACList from 'components/ACLPage/List/List';\n\nconst ACLPage = () => {\n  return (\n    <Routes>\n      <Route index element={<ACList />} />\n    </Routes>\n  );\n};\n\nexport default ACLPage;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ACLPage/List/List.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const EnumCell = styled.div`\n  text-transform: capitalize;\n`;\n\nexport const DeleteCell = styled.div`\n  svg {\n    cursor: pointer;\n  }\n`;\n\nexport const Chip = styled.div<{\n  chipType?: 'default' | 'success' | 'danger' | 'secondary' | string;\n}>`\n  width: fit-content;\n  text-transform: capitalize;\n  padding: 2px 8px;\n  font-size: 12px;\n  line-height: 16px;\n  border-radius: 16px;\n  color: ${({ theme }) => theme.tag.color};\n  background-color: ${({ theme, chipType }) => {\n    switch (chipType) {\n      case 'success':\n        return theme.tag.backgroundColor.green;\n      case 'danger':\n        return theme.tag.backgroundColor.red;\n      case 'secondary':\n        return theme.tag.backgroundColor.secondary;\n      default:\n        return theme.tag.backgroundColor.gray;\n    }\n  }};\n`;\n\nexport const PatternCell = styled.div`\n  display: flex;\n  align-items: center;\n\n  ${Chip} {\n    margin-left: 4px;\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ACLPage/List/List.tsx",
    "content": "import React from 'react';\nimport { ColumnDef } from '@tanstack/react-table';\nimport { useTheme } from 'styled-components';\nimport PageHeading from 'components/common/PageHeading/PageHeading';\nimport Table from 'components/common/NewTable';\nimport DeleteIcon from 'components/common/Icons/DeleteIcon';\nimport { useConfirm } from 'lib/hooks/useConfirm';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { useAcls, useDeleteAcl } from 'lib/hooks/api/acl';\nimport { ClusterName } from 'redux/interfaces';\nimport {\n  KafkaAcl,\n  KafkaAclNamePatternType,\n  KafkaAclPermissionEnum,\n} from 'generated-sources';\n\nimport * as S from './List.styled';\n\nconst ACList: React.FC = () => {\n  const { clusterName } = useAppParams<{ clusterName: ClusterName }>();\n  const theme = useTheme();\n  const { data: aclList } = useAcls(clusterName);\n  const { deleteResource } = useDeleteAcl(clusterName);\n  const modal = useConfirm(true);\n\n  const [rowId, setRowId] = React.useState('');\n\n  const onDeleteClick = (acl: KafkaAcl | null) => {\n    if (acl) {\n      modal('Are you sure want to delete this ACL record?', () =>\n        deleteResource(acl)\n      );\n    }\n  };\n\n  const columns = React.useMemo<ColumnDef<KafkaAcl>[]>(\n    () => [\n      {\n        header: 'Principal',\n        accessorKey: 'principal',\n        size: 257,\n      },\n      {\n        header: 'Resource',\n        accessorKey: 'resourceType',\n        // eslint-disable-next-line react/no-unstable-nested-components\n        cell: ({ getValue }) => (\n          <S.EnumCell>{getValue<string>().toLowerCase()}</S.EnumCell>\n        ),\n        size: 145,\n      },\n      {\n        header: 'Pattern',\n        accessorKey: 'resourceName',\n        // eslint-disable-next-line react/no-unstable-nested-components\n        cell: ({ getValue, row }) => {\n          let chipType;\n          if (\n            row.original.namePatternType === KafkaAclNamePatternType.PREFIXED\n          ) {\n            chipType = 'default';\n          }\n\n          if (\n            row.original.namePatternType === KafkaAclNamePatternType.LITERAL\n          ) {\n            chipType = 'secondary';\n          }\n          return (\n            <S.PatternCell>\n              {getValue<string>()}\n              {chipType ? (\n                <S.Chip chipType={chipType}>\n                  {row.original.namePatternType.toLowerCase()}\n                </S.Chip>\n              ) : null}\n            </S.PatternCell>\n          );\n        },\n        size: 257,\n      },\n      {\n        header: 'Host',\n        accessorKey: 'host',\n        size: 257,\n      },\n      {\n        header: 'Operation',\n        accessorKey: 'operation',\n        // eslint-disable-next-line react/no-unstable-nested-components\n        cell: ({ getValue }) => (\n          <S.EnumCell>{getValue<string>().toLowerCase()}</S.EnumCell>\n        ),\n        size: 121,\n      },\n      {\n        header: 'Permission',\n        accessorKey: 'permission',\n        // eslint-disable-next-line react/no-unstable-nested-components\n        cell: ({ getValue }) => (\n          <S.Chip\n            chipType={\n              getValue<string>() === KafkaAclPermissionEnum.ALLOW\n                ? 'success'\n                : 'danger'\n            }\n          >\n            {getValue<string>().toLowerCase()}\n          </S.Chip>\n        ),\n        size: 111,\n      },\n      {\n        id: 'delete',\n        // eslint-disable-next-line react/no-unstable-nested-components\n        cell: ({ row }) => {\n          return (\n            <S.DeleteCell onClick={() => onDeleteClick(row.original)}>\n              <DeleteIcon\n                fill={\n                  rowId === row.id ? theme.acl.table.deleteIcon : 'transparent'\n                }\n              />\n            </S.DeleteCell>\n          );\n        },\n        size: 76,\n      },\n    ],\n    [rowId]\n  );\n\n  const onRowHover = (value: unknown) => {\n    if (value && typeof value === 'object' && 'id' in value) {\n      setRowId(value.id as string);\n    }\n  };\n\n  return (\n    <>\n      <PageHeading text=\"Access Control List\" />\n      <Table\n        columns={columns}\n        data={aclList ?? []}\n        emptyMessage=\"No ACL items found\"\n        onRowHover={onRowHover}\n        onMouseLeave={() => setRowId('')}\n      />\n    </>\n  );\n};\n\nexport default ACList;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ACLPage/List/__test__/List.spec.tsx",
    "content": "import React from 'react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { screen } from '@testing-library/dom';\nimport userEvent from '@testing-library/user-event';\nimport { clusterACLPath } from 'lib/paths';\nimport ACList from 'components/ACLPage/List/List';\nimport { useAcls, useDeleteAcl } from 'lib/hooks/api/acl';\nimport { aclPayload } from 'lib/fixtures/acls';\n\njest.mock('lib/hooks/api/acl', () => ({\n  useAcls: jest.fn(),\n  useDeleteAcl: jest.fn(),\n}));\n\ndescribe('ACLList Component', () => {\n  const clusterName = 'local';\n  const renderComponent = () =>\n    render(\n      <WithRoute path={clusterACLPath()}>\n        <ACList />\n      </WithRoute>,\n      {\n        initialEntries: [clusterACLPath(clusterName)],\n      }\n    );\n\n  describe('ACLList', () => {\n    describe('when the acls are loaded', () => {\n      beforeEach(() => {\n        (useAcls as jest.Mock).mockImplementation(() => ({\n          data: aclPayload,\n        }));\n        (useDeleteAcl as jest.Mock).mockImplementation(() => ({\n          deleteResource: jest.fn(),\n        }));\n      });\n\n      it('renders ACLList with records', async () => {\n        renderComponent();\n        expect(screen.getByRole('table')).toBeInTheDocument();\n        expect(screen.getAllByRole('row').length).toEqual(4);\n      });\n\n      it('shows delete icon on hover', async () => {\n        const { container } = renderComponent();\n        const [trElement] = screen.getAllByRole('row');\n        await userEvent.hover(trElement);\n        const deleteElement = container.querySelector('svg');\n        expect(deleteElement).not.toHaveStyle({\n          fill: 'transparent',\n        });\n      });\n    });\n\n    describe('when it has no acls', () => {\n      beforeEach(() => {\n        (useAcls as jest.Mock).mockImplementation(() => ({\n          data: [],\n        }));\n        (useDeleteAcl as jest.Mock).mockImplementation(() => ({\n          deleteResource: jest.fn(),\n        }));\n      });\n\n      it('renders empty ACLList with message', async () => {\n        renderComponent();\n        expect(screen.getByRole('table')).toBeInTheDocument();\n        expect(\n          screen.getByRole('row', { name: 'No ACL items found' })\n        ).toBeInTheDocument();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/App.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const Layout = styled.div`\n  min-width: 1200px;\n\n  @media screen and (max-width: 1023px) {\n    min-width: initial;\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/App.tsx",
    "content": "import React, { Suspense, useContext } from 'react';\nimport { Routes, Route, Navigate } from 'react-router-dom';\nimport {\n  accessErrorPage,\n  clusterPath,\n  errorPage,\n  getNonExactPath,\n  clusterNewConfigPath,\n} from 'lib/paths';\nimport PageLoader from 'components/common/PageLoader/PageLoader';\nimport Dashboard from 'components/Dashboard/Dashboard';\nimport ClusterPage from 'components/ClusterPage/ClusterPage';\nimport { ThemeProvider } from 'styled-components';\nimport { theme, darkTheme } from 'theme/theme';\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { showServerError } from 'lib/errorHandling';\nimport { Toaster } from 'react-hot-toast';\nimport GlobalCSS from 'components/globalCss';\nimport * as S from 'components/App.styled';\nimport ClusterConfigForm from 'widgets/ClusterConfigForm';\nimport { ThemeModeContext } from 'components/contexts/ThemeModeContext';\n\nimport ConfirmationModal from './common/ConfirmationModal/ConfirmationModal';\nimport { ConfirmContextProvider } from './contexts/ConfirmContext';\nimport { GlobalSettingsProvider } from './contexts/GlobalSettingsContext';\nimport ErrorPage from './ErrorPage/ErrorPage';\nimport { UserInfoRolesAccessProvider } from './contexts/UserInfoRolesAccessContext';\nimport PageContainer from './PageContainer/PageContainer';\n\nconst queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      suspense: true,\n      networkMode: 'offlineFirst',\n      onError(error) {\n        showServerError(error as Response);\n      },\n    },\n    mutations: {\n      onError(error) {\n        showServerError(error as Response);\n      },\n    },\n  },\n});\nconst App: React.FC = () => {\n  const { isDarkMode } = useContext(ThemeModeContext);\n\n  return (\n    <QueryClientProvider client={queryClient}>\n      <GlobalSettingsProvider>\n        <ThemeProvider theme={isDarkMode ? darkTheme : theme}>\n          <Suspense fallback={<PageLoader />}>\n            <UserInfoRolesAccessProvider>\n              <ConfirmContextProvider>\n                <GlobalCSS />\n                <S.Layout>\n                  <PageContainer>\n                    <Routes>\n                      {['/', '/ui', '/ui/clusters'].map((path) => (\n                        <Route\n                          key=\"Home\" // optional: avoid full re-renders on route changes\n                          path={path}\n                          element={<Dashboard />}\n                        />\n                      ))}\n                      <Route\n                        path={getNonExactPath(clusterNewConfigPath)}\n                        element={<ClusterConfigForm />}\n                      />\n                      <Route\n                        path={getNonExactPath(clusterPath())}\n                        element={<ClusterPage />}\n                      />\n                      <Route\n                        path={accessErrorPage}\n                        element={\n                          <ErrorPage status={403} text=\"Access is Denied\" />\n                        }\n                      />\n                      <Route path={errorPage} element={<ErrorPage />} />\n                      <Route\n                        path=\"*\"\n                        element={<Navigate to={errorPage} replace />}\n                      />\n                    </Routes>\n                  </PageContainer>\n                  <Toaster position=\"bottom-right\" />\n                </S.Layout>\n                <ConfirmationModal />\n              </ConfirmContextProvider>\n            </UserInfoRolesAccessProvider>\n          </Suspense>\n        </ThemeProvider>\n      </GlobalSettingsProvider>\n    </QueryClientProvider>\n  );\n};\n\nexport default App;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Brokers/Broker/Broker.tsx",
    "content": "import React, { Suspense } from 'react';\nimport PageHeading from 'components/common/PageHeading/PageHeading';\nimport * as Metrics from 'components/common/Metrics';\nimport BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport {\n  clusterBrokerMetricsPath,\n  clusterBrokerMetricsRelativePath,\n  clusterBrokerConfigsPath,\n  ClusterBrokerParam,\n  clusterBrokerPath,\n  clusterBrokersPath,\n  clusterBrokerConfigsRelativePath,\n} from 'lib/paths';\nimport { useClusterStats } from 'lib/hooks/api/clusters';\nimport { useBrokers } from 'lib/hooks/api/brokers';\nimport { NavLink, Route, Routes } from 'react-router-dom';\nimport BrokerLogdir from 'components/Brokers/Broker/BrokerLogdir/BrokerLogdir';\nimport BrokerMetrics from 'components/Brokers/Broker/BrokerMetrics/BrokerMetrics';\nimport Navbar from 'components/common/Navigation/Navbar.styled';\nimport PageLoader from 'components/common/PageLoader/PageLoader';\nimport { ActionNavLink } from 'components/common/ActionComponent';\nimport { Action, ResourceType } from 'generated-sources';\n\nimport Configs from './Configs/Configs';\n\nconst Broker: React.FC = () => {\n  const { clusterName, brokerId } = useAppParams<ClusterBrokerParam>();\n\n  const { data: clusterStats } = useClusterStats(clusterName);\n  const { data: brokers } = useBrokers(clusterName);\n\n  if (!clusterStats) return null;\n\n  const brokerItem = brokers?.find(({ id }) => id === Number(brokerId));\n  const brokerDiskUsage = clusterStats.diskUsage?.find(\n    (item) => item.brokerId === Number(brokerId)\n  );\n  return (\n    <>\n      <PageHeading\n        text={`Broker ${brokerId}`}\n        backTo={clusterBrokersPath(clusterName)}\n        backText=\"Brokers\"\n      />\n      <Metrics.Wrapper>\n        <Metrics.Section>\n          <Metrics.Indicator label=\"Segment Size\">\n            <BytesFormatted\n              value={brokerDiskUsage?.segmentSize}\n              precision={2}\n            />\n          </Metrics.Indicator>\n          <Metrics.Indicator label=\"Segment Count\">\n            {brokerDiskUsage?.segmentCount}\n          </Metrics.Indicator>\n          <Metrics.Indicator label=\"Port\">{brokerItem?.port}</Metrics.Indicator>\n          <Metrics.Indicator label=\"Host\">{brokerItem?.host}</Metrics.Indicator>\n        </Metrics.Section>\n      </Metrics.Wrapper>\n\n      <Navbar role=\"navigation\">\n        <NavLink\n          to={clusterBrokerPath(clusterName, brokerId)}\n          className={({ isActive }) => (isActive ? 'is-active' : '')}\n          end\n        >\n          Log directories\n        </NavLink>\n        <NavLink\n          to={clusterBrokerConfigsPath(clusterName, brokerId)}\n          className={({ isActive }) => (isActive ? 'is-active' : '')}\n        >\n          Configs\n        </NavLink>\n        <ActionNavLink\n          to={clusterBrokerMetricsPath(clusterName, brokerId)}\n          className={({ isActive }) => (isActive ? 'is-active' : '')}\n          permission={{\n            resource: ResourceType.CLUSTERCONFIG,\n            action: Action.VIEW,\n          }}\n        >\n          Metrics\n        </ActionNavLink>\n      </Navbar>\n      <Suspense fallback={<PageLoader />}>\n        <Routes>\n          <Route index element={<BrokerLogdir />} />\n          <Route\n            path={clusterBrokerConfigsRelativePath}\n            element={<Configs />}\n          />\n          <Route\n            path={clusterBrokerMetricsRelativePath}\n            element={<BrokerMetrics />}\n          />\n        </Routes>\n      </Suspense>\n    </>\n  );\n};\n\nexport default Broker;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Brokers/Broker/BrokerLogdir/BrokerLogdir.tsx",
    "content": "import React from 'react';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { ClusterBrokerParam } from 'lib/paths';\nimport { useBrokerLogDirs } from 'lib/hooks/api/brokers';\nimport Table from 'components/common/NewTable';\nimport { ColumnDef } from '@tanstack/react-table';\nimport { BrokersLogdirs } from 'generated-sources';\n\nconst BrokerLogdir: React.FC = () => {\n  const { clusterName, brokerId } = useAppParams<ClusterBrokerParam>();\n  const { data } = useBrokerLogDirs(clusterName, Number(brokerId));\n\n  const columns = React.useMemo<ColumnDef<BrokersLogdirs>[]>(\n    () => [\n      { header: 'Name', accessorKey: 'name' },\n      { header: 'Error', accessorKey: 'error' },\n      {\n        header: 'Topics',\n        accessorKey: 'topics',\n        cell: ({ getValue }) =>\n          getValue<BrokersLogdirs['topics']>()?.length || 0,\n        enableSorting: false,\n      },\n      {\n        id: 'partitions',\n        header: 'Partitions',\n        accessorKey: 'topics',\n        cell: ({ getValue }) => {\n          const topics = getValue<BrokersLogdirs['topics']>();\n          if (!topics) {\n            return 0;\n          }\n          return topics.reduce(\n            (acc, topic) => acc + (topic.partitions?.length || 0),\n            0\n          );\n        },\n        enableSorting: false,\n      },\n    ],\n    []\n  );\n\n  return (\n    <Table\n      data={data || []}\n      columns={columns}\n      emptyMessage=\"Log dir data not available\"\n      enableSorting\n    />\n  );\n};\n\nexport default BrokerLogdir;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Brokers/Broker/BrokerLogdir/__test__/BrokerLogdir.spec.tsx",
    "content": "import React from 'react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { screen } from '@testing-library/dom';\nimport { clusterBrokerPath } from 'lib/paths';\nimport { brokerLogDirsPayload } from 'lib/fixtures/brokers';\nimport { useBrokerLogDirs } from 'lib/hooks/api/brokers';\nimport { BrokerLogdirs } from 'generated-sources';\nimport BrokerLogdir from 'components/Brokers/Broker/BrokerLogdir/BrokerLogdir';\n\njest.mock('lib/hooks/api/brokers', () => ({\n  useBrokerLogDirs: jest.fn(),\n}));\n\nconst clusterName = 'local';\nconst brokerId = 1;\n\ndescribe('BrokerLogdir Component', () => {\n  const renderComponent = async (payload?: BrokerLogdirs[]) => {\n    (useBrokerLogDirs as jest.Mock).mockImplementation(() => ({\n      data: payload,\n    }));\n    await render(\n      <WithRoute path={clusterBrokerPath()}>\n        <BrokerLogdir />\n      </WithRoute>,\n      {\n        initialEntries: [clusterBrokerPath(clusterName, brokerId)],\n      }\n    );\n  };\n\n  it('shows warning when server returns undefined logDirs response', async () => {\n    await renderComponent();\n    expect(\n      screen.getByRole('row', { name: 'Log dir data not available' })\n    ).toBeInTheDocument();\n  });\n\n  it('shows warning when server returns empty logDirs response', async () => {\n    await renderComponent([]);\n    expect(\n      screen.getByRole('row', { name: 'Log dir data not available' })\n    ).toBeInTheDocument();\n  });\n\n  it('shows brokers', async () => {\n    await renderComponent(brokerLogDirsPayload);\n    expect(\n      screen.queryByRole('row', { name: 'Log dir data not available' })\n    ).not.toBeInTheDocument();\n\n    expect(\n      screen.getByRole('row', {\n        name: '/opt/kafka/data-0/logs NONE 3 4',\n      })\n    ).toBeInTheDocument();\n    expect(\n      screen.getByRole('row', {\n        name: '/opt/kafka/data-1/logs NONE 0 0',\n      })\n    ).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Brokers/Broker/BrokerMetrics/BrokerMetrics.tsx",
    "content": "import React from 'react';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { ClusterBrokerParam } from 'lib/paths';\nimport { useBrokerMetrics } from 'lib/hooks/api/brokers';\nimport { SchemaType } from 'generated-sources';\nimport EditorViewer from 'components/common/EditorViewer/EditorViewer';\nimport { getEditorText } from 'components/Brokers/utils/getEditorText';\n\nconst BrokerMetrics: React.FC = () => {\n  const { clusterName, brokerId } = useAppParams<ClusterBrokerParam>();\n  const { data: metrics } = useBrokerMetrics(clusterName, Number(brokerId));\n\n  return (\n    <EditorViewer schemaType={SchemaType.JSON} data={getEditorText(metrics)} />\n  );\n};\n\nexport default BrokerMetrics;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Brokers/Broker/BrokerMetrics/__test__/BrokerMetrics.spec.tsx",
    "content": "import React from 'react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { screen } from '@testing-library/dom';\nimport { clusterBrokerMetricsPath } from 'lib/paths';\nimport BrokerMetrics from 'components/Brokers/Broker/BrokerMetrics/BrokerMetrics';\nimport { useBrokerMetrics } from 'lib/hooks/api/brokers';\n\njest.mock('lib/hooks/api/brokers', () => ({\n  useBrokerMetrics: jest.fn(),\n}));\n\nconst clusterName = 'local';\nconst brokerId = 1;\n\ndescribe('BrokerMetrics Component', () => {\n  it(\"shows warning when server doesn't return metrics response\", async () => {\n    (useBrokerMetrics as jest.Mock).mockImplementation(() => ({\n      data: {},\n    }));\n\n    render(\n      <WithRoute path={clusterBrokerMetricsPath()}>\n        <BrokerMetrics />\n      </WithRoute>,\n      {\n        initialEntries: [clusterBrokerMetricsPath(clusterName, brokerId)],\n      }\n    );\n    expect(screen.getAllByRole('textbox').length).toEqual(1);\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Brokers/Broker/Configs/Configs.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const ValueWrapper = styled.div`\n  display: flex;\n  justify-content: space-between;\n  button {\n    margin: 0 10px;\n  }\n`;\n\nexport const Value = styled.span`\n  line-height: 24px;\n  margin-right: 10px;\n  text-overflow: ellipsis;\n  max-width: 400px;\n  overflow: hidden;\n  white-space: nowrap;\n`;\n\nexport const ButtonsWrapper = styled.div`\n  display: flex;\n`;\nexport const SearchWrapper = styled.div`\n  margin: 10px;\n  width: 21%;\n`;\n\nexport const Source = styled.div`\n  display: flex;\n  align-content: center;\n  svg {\n    margin-left: 10px;\n    vertical-align: middle;\n    cursor: pointer;\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Brokers/Broker/Configs/Configs.tsx",
    "content": "import React from 'react';\nimport { CellContext, ColumnDef } from '@tanstack/react-table';\nimport { ClusterBrokerParam } from 'lib/paths';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport {\n  useBrokerConfig,\n  useUpdateBrokerConfigByName,\n} from 'lib/hooks/api/brokers';\nimport Table from 'components/common/NewTable';\nimport { BrokerConfig, ConfigSource } from 'generated-sources';\nimport Search from 'components/common/Search/Search';\nimport Tooltip from 'components/common/Tooltip/Tooltip';\nimport InfoIcon from 'components/common/Icons/InfoIcon';\n\nimport InputCell from './InputCell';\nimport * as S from './Configs.styled';\n\nconst tooltipContent = `DYNAMIC_TOPIC_CONFIG = dynamic topic config that is configured for a specific topic\nDYNAMIC_BROKER_LOGGER_CONFIG = dynamic broker logger config that is configured for a specific broker\nDYNAMIC_BROKER_CONFIG = dynamic broker config that is configured for a specific broker\nDYNAMIC_DEFAULT_BROKER_CONFIG = dynamic broker config that is configured as default for all brokers in the cluster\nSTATIC_BROKER_CONFIG = static broker config provided as broker properties at start up (e.g. server.properties file)\nDEFAULT_CONFIG = built-in default configuration for configs that have a default value\nUNKNOWN = source unknown e.g. in the ConfigEntry used for alter requests where source is not set`;\n\nconst Configs: React.FC = () => {\n  const [keyword, setKeyword] = React.useState('');\n  const { clusterName, brokerId } = useAppParams<ClusterBrokerParam>();\n  const { data = [] } = useBrokerConfig(clusterName, Number(brokerId));\n  const stateMutation = useUpdateBrokerConfigByName(\n    clusterName,\n    Number(brokerId)\n  );\n\n  const getData = () => {\n    return data\n      .filter((item) => {\n        const nameMatch = item.name\n          .toLocaleLowerCase()\n          .includes(keyword.toLocaleLowerCase());\n        return nameMatch\n          ? true\n          : item.value &&\n              item.value\n                .toLocaleLowerCase()\n                .includes(keyword.toLocaleLowerCase()); // try to match the keyword on any of the item.value elements when nameMatch fails but item.value exists\n      })\n      .sort((a, b) => {\n        if (a.source === b.source) return 0;\n        return a.source === ConfigSource.DYNAMIC_BROKER_CONFIG ? -1 : 1;\n      });\n  };\n\n  const dataSource = React.useMemo(() => getData(), [data, keyword]);\n\n  const renderCell = (props: CellContext<BrokerConfig, unknown>) => (\n    <InputCell\n      {...props}\n      onUpdate={(name, value) => {\n        stateMutation.mutateAsync({\n          name,\n          brokerConfigItem: {\n            value,\n          },\n        });\n      }}\n    />\n  );\n\n  const columns = React.useMemo<ColumnDef<BrokerConfig>[]>(\n    () => [\n      { header: 'Key', accessorKey: 'name' },\n      {\n        header: 'Value',\n        accessorKey: 'value',\n        cell: renderCell,\n      },\n      {\n        // eslint-disable-next-line react/no-unstable-nested-components\n        header: () => {\n          return (\n            <S.Source>\n              Source\n              <Tooltip\n                value={<InfoIcon />}\n                content={tooltipContent}\n                placement=\"top-end\"\n              />\n            </S.Source>\n          );\n        },\n        accessorKey: 'source',\n      },\n    ],\n    []\n  );\n\n  return (\n    <>\n      <S.SearchWrapper>\n        <Search\n          onChange={setKeyword}\n          placeholder=\"Search by Key or Value\"\n          value={keyword}\n        />\n      </S.SearchWrapper>\n      <Table columns={columns} data={dataSource} />\n    </>\n  );\n};\n\nexport default Configs;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Brokers/Broker/Configs/InputCell.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { CellContext } from '@tanstack/react-table';\nimport CheckmarkIcon from 'components/common/Icons/CheckmarkIcon';\nimport EditIcon from 'components/common/Icons/EditIcon';\nimport CancelIcon from 'components/common/Icons/CancelIcon';\nimport { useConfirm } from 'lib/hooks/useConfirm';\nimport { Action, BrokerConfig, ResourceType } from 'generated-sources';\nimport { Button } from 'components/common/Button/Button';\nimport Input from 'components/common/Input/Input';\nimport { ActionButton } from 'components/common/ActionComponent';\n\nimport * as S from './Configs.styled';\n\ninterface InputCellProps extends CellContext<BrokerConfig, unknown> {\n  onUpdate: (name: string, value?: string) => void;\n}\n\nconst InputCell: React.FC<InputCellProps> = ({ row, getValue, onUpdate }) => {\n  const initialValue = `${getValue<string | number>()}`;\n  const [isEdit, setIsEdit] = React.useState(false);\n  const [value, setValue] = React.useState(initialValue);\n\n  const confirm = useConfirm();\n\n  const onSave = () => {\n    if (value !== initialValue) {\n      confirm('Are you sure you want to change the value?', async () => {\n        onUpdate(row?.original?.name, value);\n      });\n    }\n    setIsEdit(false);\n  };\n\n  useEffect(() => {\n    setValue(initialValue);\n  }, [initialValue]);\n\n  return isEdit ? (\n    <S.ValueWrapper>\n      <Input\n        type=\"text\"\n        inputSize=\"S\"\n        value={value}\n        aria-label=\"inputValue\"\n        onChange={({ target }) => setValue(target?.value)}\n      />\n      <S.ButtonsWrapper>\n        <Button\n          buttonType=\"primary\"\n          buttonSize=\"S\"\n          aria-label=\"confirmAction\"\n          onClick={onSave}\n        >\n          <CheckmarkIcon /> Save\n        </Button>\n        <Button\n          buttonType=\"primary\"\n          buttonSize=\"S\"\n          aria-label=\"cancelAction\"\n          onClick={() => setIsEdit(false)}\n        >\n          <CancelIcon /> Cancel\n        </Button>\n      </S.ButtonsWrapper>\n    </S.ValueWrapper>\n  ) : (\n    <S.ValueWrapper\n      style={\n        row?.original?.source === 'DYNAMIC_BROKER_CONFIG'\n          ? { fontWeight: 600 }\n          : { fontWeight: 400 }\n      }\n    >\n      <S.Value title={initialValue}>{initialValue}</S.Value>\n      <ActionButton\n        buttonType=\"primary\"\n        buttonSize=\"S\"\n        aria-label=\"editAction\"\n        onClick={() => setIsEdit(true)}\n        permission={{\n          resource: ResourceType.CLUSTERCONFIG,\n          action: Action.EDIT,\n        }}\n      >\n        <EditIcon /> Edit\n      </ActionButton>\n    </S.ValueWrapper>\n  );\n};\n\nexport default InputCell;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Brokers/Broker/Configs/__test__/Configs.spec.tsx",
    "content": "import React from 'react';\nimport { screen } from '@testing-library/dom';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { clusterBrokerConfigsPath } from 'lib/paths';\nimport { useBrokerConfig } from 'lib/hooks/api/brokers';\nimport { brokerConfigPayload } from 'lib/fixtures/brokers';\nimport Configs from 'components/Brokers/Broker/Configs/Configs';\nimport userEvent from '@testing-library/user-event';\n\nconst clusterName = 'Cluster_Name';\nconst brokerId = 'Broker_Id';\n\njest.mock('lib/hooks/api/brokers', () => ({\n  useBrokerConfig: jest.fn(),\n  useUpdateBrokerConfigByName: jest.fn(),\n}));\n\ndescribe('Configs', () => {\n  const renderComponent = () => {\n    const path = clusterBrokerConfigsPath(clusterName, brokerId);\n    return render(\n      <WithRoute path={clusterBrokerConfigsPath()}>\n        <Configs />\n      </WithRoute>,\n      { initialEntries: [path] }\n    );\n  };\n\n  beforeEach(() => {\n    (useBrokerConfig as jest.Mock).mockImplementation(() => ({\n      data: brokerConfigPayload,\n    }));\n    renderComponent();\n  });\n\n  it('renders configs table', async () => {\n    expect(screen.getByRole('table')).toBeInTheDocument();\n    expect(screen.getAllByRole('row').length).toEqual(\n      brokerConfigPayload.length + 1\n    );\n  });\n\n  it('updates textbox value', async () => {\n    await userEvent.click(screen.getAllByLabelText('editAction')[0]);\n\n    const textbox = screen.getByLabelText('inputValue');\n    expect(textbox).toBeInTheDocument();\n    expect(textbox).toHaveValue('producer');\n\n    await userEvent.type(textbox, 'new value');\n\n    expect(\n      screen.getByRole('button', { name: 'confirmAction' })\n    ).toBeInTheDocument();\n    expect(\n      screen.getByRole('button', { name: 'cancelAction' })\n    ).toBeInTheDocument();\n\n    await userEvent.click(\n      screen.getByRole('button', { name: 'confirmAction' })\n    );\n\n    expect(\n      screen.getByText('Are you sure you want to change the value?')\n    ).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Brokers/Broker/__test__/Broker.spec.tsx",
    "content": "import React from 'react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { screen } from '@testing-library/dom';\nimport {\n  clusterBrokerMetricsPath,\n  clusterBrokerPath,\n  getNonExactPath,\n} from 'lib/paths';\nimport Broker from 'components/Brokers/Broker/Broker';\nimport { useBrokers } from 'lib/hooks/api/brokers';\nimport { useClusterStats } from 'lib/hooks/api/clusters';\nimport { brokersPayload } from 'lib/fixtures/brokers';\nimport { clusterStatsPayload } from 'lib/fixtures/clusters';\n\nconst clusterName = 'local';\nconst brokerId = 200;\nconst activeClassName = 'is-active';\nconst brokerLogdir = {\n  pageText: 'brokerLogdir',\n  navigationName: 'Log directories',\n};\nconst brokerMetrics = {\n  pageText: 'brokerMetrics',\n  navigationName: 'Metrics',\n};\n\njest.mock('components/Brokers/Broker/BrokerLogdir/BrokerLogdir', () => () => (\n  <div>{brokerLogdir.pageText}</div>\n));\njest.mock('components/Brokers/Broker/BrokerMetrics/BrokerMetrics', () => () => (\n  <div>{brokerMetrics.pageText}</div>\n));\njest.mock('lib/hooks/api/brokers', () => ({\n  useBrokers: jest.fn(),\n}));\njest.mock('lib/hooks/api/clusters', () => ({\n  useClusterStats: jest.fn(),\n}));\n\ndescribe('Broker Component', () => {\n  beforeEach(() => {\n    (useBrokers as jest.Mock).mockImplementation(() => ({\n      data: brokersPayload,\n    }));\n    (useClusterStats as jest.Mock).mockImplementation(() => ({\n      data: clusterStatsPayload,\n    }));\n  });\n  const renderComponent = (path = clusterBrokerPath(clusterName, brokerId)) =>\n    render(\n      <WithRoute path={getNonExactPath(clusterBrokerPath())}>\n        <Broker />\n      </WithRoute>,\n      {\n        initialEntries: [path],\n      }\n    );\n\n  it('shows broker found', async () => {\n    await renderComponent();\n    const brokerInfo = brokersPayload.find((broker) => broker.id === brokerId);\n    const brokerDiskUsage = clusterStatsPayload.diskUsage.find(\n      (disk) => disk.brokerId === brokerId\n    );\n\n    expect(\n      screen.getByText(brokerDiskUsage?.segmentCount || '')\n    ).toBeInTheDocument();\n    expect(screen.getByText('11.77 MB')).toBeInTheDocument();\n\n    expect(screen.getByText('Segment Count')).toBeInTheDocument();\n    expect(\n      screen.getByText(brokerDiskUsage?.segmentCount || '')\n    ).toBeInTheDocument();\n\n    expect(screen.getByText('Port')).toBeInTheDocument();\n    expect(screen.getByText(brokerInfo?.port || '')).toBeInTheDocument();\n\n    expect(screen.getByText('Host')).toBeInTheDocument();\n    expect(screen.getByText(brokerInfo?.host || '')).toBeInTheDocument();\n  });\n\n  it('renders Broker Logdir', async () => {\n    await renderComponent();\n\n    const logdirLink = screen.getByRole('link', {\n      name: brokerLogdir.navigationName,\n    });\n    expect(logdirLink).toBeInTheDocument();\n    expect(logdirLink).toHaveClass(activeClassName);\n\n    expect(screen.getByText(brokerLogdir.pageText)).toBeInTheDocument();\n  });\n\n  it('renders Broker Metrics', async () => {\n    await renderComponent(clusterBrokerMetricsPath(clusterName, brokerId));\n\n    const metricsLink = screen.getByRole('link', {\n      name: brokerMetrics.navigationName,\n    });\n    expect(metricsLink).toBeInTheDocument();\n    expect(metricsLink).toHaveClass(activeClassName);\n\n    expect(screen.getByText(brokerMetrics.pageText)).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Brokers/Brokers.tsx",
    "content": "import React from 'react';\nimport { Route, Routes } from 'react-router-dom';\nimport { getNonExactPath, RouteParams } from 'lib/paths';\nimport BrokersList from 'components/Brokers/BrokersList/BrokersList';\nimport Broker from 'components/Brokers/Broker/Broker';\nimport SuspenseQueryComponent from 'components/common/SuspenseQueryComponent/SuspenseQueryComponent';\n\nconst Brokers: React.FC = () => (\n  <Routes>\n    <Route index element={<BrokersList />} />\n    <Route\n      path={getNonExactPath(RouteParams.brokerId)}\n      element={\n        <SuspenseQueryComponent>\n          <Broker />\n        </SuspenseQueryComponent>\n      }\n    />\n  </Routes>\n);\n\nexport default Brokers;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Brokers/BrokersList/BrokersList.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const RowCell = styled.div`\n  display: flex;\n  width: 100%;\n  align-items: center;\n\n  svg {\n    width: 20px;\n    padding-left: 6px;\n  }\n`;\n\nexport const DangerText = styled.span`\n  color: ${({ theme }) => theme.circularAlert.color.error};\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Brokers/BrokersList/BrokersList.tsx",
    "content": "import React from 'react';\nimport { ClusterName } from 'redux/interfaces';\nimport { useNavigate } from 'react-router-dom';\nimport PageHeading from 'components/common/PageHeading/PageHeading';\nimport * as Metrics from 'components/common/Metrics';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { useBrokers } from 'lib/hooks/api/brokers';\nimport { useClusterStats } from 'lib/hooks/api/clusters';\nimport Table, { LinkCell, SizeCell } from 'components/common/NewTable';\nimport CheckMarkRoundIcon from 'components/common/Icons/CheckMarkRoundIcon';\nimport { ColumnDef } from '@tanstack/react-table';\nimport { clusterBrokerPath } from 'lib/paths';\nimport Tooltip from 'components/common/Tooltip/Tooltip';\nimport ColoredCell from 'components/common/NewTable/ColoredCell';\n\nimport SkewHeader from './SkewHeader/SkewHeader';\nimport * as S from './BrokersList.styled';\n\nconst NA = 'N/A';\n\nconst BrokersList: React.FC = () => {\n  const navigate = useNavigate();\n  const { clusterName } = useAppParams<{ clusterName: ClusterName }>();\n  const { data: clusterStats = {} } = useClusterStats(clusterName);\n  const { data: brokers } = useBrokers(clusterName);\n\n  const {\n    brokerCount,\n    activeControllers,\n    onlinePartitionCount,\n    offlinePartitionCount,\n    inSyncReplicasCount,\n    outOfSyncReplicasCount,\n    underReplicatedPartitionCount,\n    diskUsage,\n    version,\n  } = clusterStats;\n\n  const rows = React.useMemo(() => {\n    let brokersResource;\n    if (!diskUsage || !diskUsage?.length) {\n      brokersResource =\n        brokers?.map((broker) => {\n          return {\n            brokerId: broker.id,\n            segmentSize: NA,\n            segmentCount: NA,\n          };\n        }) || [];\n    } else {\n      brokersResource = diskUsage;\n    }\n\n    return brokersResource.map(({ brokerId, segmentSize, segmentCount }) => {\n      const broker = brokers?.find(({ id }) => id === brokerId);\n      return {\n        brokerId,\n        size: segmentSize || NA,\n        count: segmentCount || NA,\n        port: broker?.port,\n        host: broker?.host,\n        partitionsLeader: broker?.partitionsLeader,\n        partitionsSkew: broker?.partitionsSkew,\n        leadersSkew: broker?.leadersSkew,\n        inSyncPartitions: broker?.inSyncPartitions,\n      };\n    });\n  }, [diskUsage, brokers]);\n\n  const columns = React.useMemo<ColumnDef<(typeof rows)[number]>[]>(\n    () => [\n      {\n        header: 'Broker ID',\n        accessorKey: 'brokerId',\n        // eslint-disable-next-line react/no-unstable-nested-components\n        cell: ({ getValue }) => (\n          <S.RowCell>\n            <LinkCell\n              value={`${getValue<string | number>()}`}\n              to={encodeURIComponent(`${getValue<string | number>()}`)}\n            />\n            {getValue<string | number>() === activeControllers && (\n              <Tooltip\n                value={<CheckMarkRoundIcon />}\n                content=\"Active Controller\"\n                placement=\"right\"\n              />\n            )}\n          </S.RowCell>\n        ),\n      },\n      {\n        header: 'Disk usage',\n        accessorKey: 'size',\n        // eslint-disable-next-line react/no-unstable-nested-components\n        cell: ({ getValue, table, cell, column, renderValue, row }) =>\n          getValue() === NA ? (\n            NA\n          ) : (\n            <SizeCell\n              table={table}\n              column={column}\n              row={row}\n              cell={cell}\n              getValue={getValue}\n              renderValue={renderValue}\n              renderSegments\n              precision={2}\n            />\n          ),\n      },\n      {\n        // eslint-disable-next-line react/no-unstable-nested-components\n        header: () => <SkewHeader />,\n        accessorKey: 'partitionsSkew',\n        // eslint-disable-next-line react/no-unstable-nested-components\n        cell: ({ getValue }) => {\n          const value = getValue<number>();\n          return (\n            <ColoredCell\n              value={value ? `${value.toFixed(2)}%` : '-'}\n              warn={value >= 10 && value < 20}\n              attention={value >= 20}\n            />\n          );\n        },\n      },\n      { header: 'Leaders', accessorKey: 'partitionsLeader' },\n      {\n        header: 'Leader skew',\n        accessorKey: 'leadersSkew',\n        // eslint-disable-next-line react/no-unstable-nested-components\n        cell: ({ getValue }) => {\n          const value = getValue<number>();\n          return (\n            <ColoredCell\n              value={value ? `${value.toFixed(2)}%` : '-'}\n              warn={value >= 10 && value < 20}\n              attention={value >= 20}\n            />\n          );\n        },\n      },\n      {\n        header: 'Online partitions',\n        accessorKey: 'inSyncPartitions',\n        // eslint-disable-next-line react/no-unstable-nested-components\n        cell: ({ getValue, row }) => {\n          const value = getValue<number>();\n          return (\n            <ColoredCell\n              value={value}\n              attention={value !== row.original.count}\n            />\n          );\n        },\n      },\n      { header: 'Port', accessorKey: 'port' },\n      {\n        header: 'Host',\n        accessorKey: 'host',\n      },\n    ],\n    []\n  );\n\n  const replicas = (inSyncReplicasCount ?? 0) + (outOfSyncReplicasCount ?? 0);\n  const areAllInSync = inSyncReplicasCount && replicas === inSyncReplicasCount;\n  const partitionIsOffline = offlinePartitionCount && offlinePartitionCount > 0;\n\n  const isActiveControllerUnKnown = typeof activeControllers === 'undefined';\n\n  return (\n    <>\n      <PageHeading text=\"Brokers\" />\n      <Metrics.Wrapper>\n        <Metrics.Section title=\"Uptime\">\n          <Metrics.Indicator label=\"Broker Count\">\n            {brokerCount}\n          </Metrics.Indicator>\n          <Metrics.Indicator\n            label=\"Active Controller\"\n            isAlert={isActiveControllerUnKnown}\n          >\n            {isActiveControllerUnKnown ? (\n              <S.DangerText>No Active Controller</S.DangerText>\n            ) : (\n              activeControllers\n            )}\n          </Metrics.Indicator>\n          <Metrics.Indicator label=\"Version\">{version}</Metrics.Indicator>\n        </Metrics.Section>\n        <Metrics.Section title=\"Partitions\">\n          <Metrics.Indicator\n            label=\"Online\"\n            isAlert\n            alertType={partitionIsOffline ? 'error' : 'success'}\n          >\n            {partitionIsOffline ? (\n              <Metrics.RedText>{onlinePartitionCount}</Metrics.RedText>\n            ) : (\n              onlinePartitionCount\n            )}\n            <Metrics.LightText>\n              {` of ${\n                (onlinePartitionCount || 0) + (offlinePartitionCount || 0)\n              }\n              `}\n            </Metrics.LightText>\n          </Metrics.Indicator>\n          <Metrics.Indicator\n            label=\"URP\"\n            title=\"Under replicated partitions\"\n            isAlert\n            alertType={!underReplicatedPartitionCount ? 'success' : 'error'}\n          >\n            {!underReplicatedPartitionCount ? (\n              <Metrics.LightText>\n                {underReplicatedPartitionCount}\n              </Metrics.LightText>\n            ) : (\n              <Metrics.RedText>{underReplicatedPartitionCount}</Metrics.RedText>\n            )}\n          </Metrics.Indicator>\n          <Metrics.Indicator\n            label=\"In Sync Replicas\"\n            isAlert\n            alertType={areAllInSync ? 'success' : 'error'}\n          >\n            {areAllInSync ? (\n              replicas\n            ) : (\n              <Metrics.RedText>{inSyncReplicasCount}</Metrics.RedText>\n            )}\n            <Metrics.LightText> of {replicas}</Metrics.LightText>\n          </Metrics.Indicator>\n          <Metrics.Indicator label=\"Out Of Sync Replicas\">\n            {outOfSyncReplicasCount}\n          </Metrics.Indicator>\n        </Metrics.Section>\n      </Metrics.Wrapper>\n      <Table\n        columns={columns}\n        data={rows}\n        enableSorting\n        onRowClick={({ original: { brokerId } }) =>\n          navigate(clusterBrokerPath(clusterName, brokerId))\n        }\n        emptyMessage=\"No clusters are online\"\n      />\n    </>\n  );\n};\n\nexport default BrokersList;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Brokers/BrokersList/SkewHeader/SkewHeader.styled.ts",
    "content": "import styled from 'styled-components';\nimport { MessageTooltip } from 'components/common/Tooltip/Tooltip.styled';\n\nexport const CellWrapper = styled.div`\n  display: flex;\n  gap: 10px;\n\n  ${MessageTooltip} {\n    max-height: unset;\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Brokers/BrokersList/SkewHeader/SkewHeader.tsx",
    "content": "import React from 'react';\nimport Tooltip from 'components/common/Tooltip/Tooltip';\nimport InfoIcon from 'components/common/Icons/InfoIcon';\n\nimport * as S from './SkewHeader.styled';\n\nconst SkewHeader: React.FC = () => (\n  <S.CellWrapper>\n    Partitions skew\n    <Tooltip\n      value={<InfoIcon />}\n      content=\"The divergence from the average brokers' value\"\n    />\n  </S.CellWrapper>\n);\n\nexport default SkewHeader;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Brokers/BrokersList/__test__/BrokersList.spec.tsx",
    "content": "import React from 'react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { screen, waitFor } from '@testing-library/dom';\nimport { clusterBrokerPath, clusterBrokersPath } from 'lib/paths';\nimport BrokersList from 'components/Brokers/BrokersList/BrokersList';\nimport userEvent from '@testing-library/user-event';\nimport { useBrokers } from 'lib/hooks/api/brokers';\nimport { useClusterStats } from 'lib/hooks/api/clusters';\nimport { brokersPayload } from 'lib/fixtures/brokers';\nimport { clusterStatsPayload } from 'lib/fixtures/clusters';\n\nconst mockedUsedNavigate = jest.fn();\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useNavigate: () => mockedUsedNavigate,\n}));\n\njest.mock('lib/hooks/api/brokers', () => ({\n  useBrokers: jest.fn(),\n}));\njest.mock('lib/hooks/api/clusters', () => ({\n  useClusterStats: jest.fn(),\n}));\n\ndescribe('BrokersList Component', () => {\n  const clusterName = 'local';\n\n  const testInSyncReplicasCount = 798;\n  const testOutOfSyncReplicasCount = 1;\n\n  const renderComponent = () =>\n    render(\n      <WithRoute path={clusterBrokersPath()}>\n        <BrokersList />\n      </WithRoute>,\n      {\n        initialEntries: [clusterBrokersPath(clusterName)],\n      }\n    );\n\n  describe('BrokersList', () => {\n    describe('when the brokers are loaded', () => {\n      beforeEach(() => {\n        (useBrokers as jest.Mock).mockImplementation(() => ({\n          data: brokersPayload,\n        }));\n        (useClusterStats as jest.Mock).mockImplementation(() => ({\n          data: clusterStatsPayload,\n        }));\n      });\n      it('renders', async () => {\n        renderComponent();\n        expect(screen.getByRole('table')).toBeInTheDocument();\n        expect(screen.getAllByRole('row').length).toEqual(3);\n      });\n      it('opens broker when row clicked', async () => {\n        renderComponent();\n        await userEvent.click(screen.getByRole('cell', { name: '100' }));\n\n        await waitFor(() =>\n          expect(mockedUsedNavigate).toBeCalledWith(\n            clusterBrokerPath(clusterName, '100')\n          )\n        );\n      });\n      it('shows warning when offlinePartitionCount > 0', async () => {\n        (useClusterStats as jest.Mock).mockImplementation(() => ({\n          data: {\n            ...clusterStatsPayload,\n            offlinePartitionCount: 1345,\n          },\n        }));\n        renderComponent();\n        const onlineWidget = screen.getByText(\n          clusterStatsPayload.onlinePartitionCount\n        );\n        expect(onlineWidget).toBeInTheDocument();\n        expect(onlineWidget).toHaveStyle({ color: '#E51A1A' });\n      });\n      it('shows right count when offlinePartitionCount > 0', async () => {\n        (useClusterStats as jest.Mock).mockImplementation(() => ({\n          data: {\n            ...clusterStatsPayload,\n            inSyncReplicasCount: testInSyncReplicasCount,\n            outOfSyncReplicasCount: testOutOfSyncReplicasCount,\n          },\n        }));\n        renderComponent();\n        const onlineWidgetDef = screen.getByText(testInSyncReplicasCount);\n        const onlineWidget = screen.getByText(\n          `of ${testInSyncReplicasCount + testOutOfSyncReplicasCount}`\n        );\n        expect(onlineWidgetDef).toBeInTheDocument();\n        expect(onlineWidget).toBeInTheDocument();\n      });\n      it('shows right count when inSyncReplicasCount: undefined && outOfSyncReplicasCount: 1', async () => {\n        (useClusterStats as jest.Mock).mockImplementation(() => ({\n          data: {\n            ...clusterStatsPayload,\n            inSyncReplicasCount: undefined,\n            outOfSyncReplicasCount: testOutOfSyncReplicasCount,\n          },\n        }));\n        renderComponent();\n        const onlineWidget = screen.getByText(\n          `of ${testOutOfSyncReplicasCount}`\n        );\n        expect(onlineWidget).toBeInTheDocument();\n      });\n      it(`shows right count when inSyncReplicasCount: ${testInSyncReplicasCount} outOfSyncReplicasCount: undefined`, async () => {\n        (useClusterStats as jest.Mock).mockImplementation(() => ({\n          data: {\n            ...clusterStatsPayload,\n            inSyncReplicasCount: testInSyncReplicasCount,\n            outOfSyncReplicasCount: undefined,\n          },\n        }));\n        renderComponent();\n        const onlineWidgetDef = screen.getByText(testInSyncReplicasCount);\n        const onlineWidget = screen.getByText(`of ${testInSyncReplicasCount}`);\n        expect(onlineWidgetDef).toBeInTheDocument();\n        expect(onlineWidget).toBeInTheDocument();\n      });\n    });\n\n    describe('BrokersList', () => {\n      describe('when the brokers are loaded', () => {\n        const testActiveControllers = 0;\n        beforeEach(() => {\n          (useBrokers as jest.Mock).mockImplementation(() => ({\n            data: brokersPayload,\n          }));\n          (useClusterStats as jest.Mock).mockImplementation(() => ({\n            data: clusterStatsPayload,\n          }));\n        });\n\n        it(`Indicates correct active cluster`, async () => {\n          renderComponent();\n          await waitFor(() =>\n            expect(screen.getByRole('tooltip')).toBeInTheDocument()\n          );\n        });\n        it(`Correct display even if there is no active cluster: ${testActiveControllers} `, async () => {\n          (useClusterStats as jest.Mock).mockImplementation(() => ({\n            data: {\n              ...clusterStatsPayload,\n              activeControllers: testActiveControllers,\n            },\n          }));\n          renderComponent();\n          await waitFor(() =>\n            expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()\n          );\n        });\n      });\n    });\n\n    describe('when diskUsage is empty', () => {\n      beforeEach(() => {\n        (useBrokers as jest.Mock).mockImplementation(() => ({\n          data: brokersPayload,\n        }));\n        (useClusterStats as jest.Mock).mockImplementation(() => ({\n          data: { ...clusterStatsPayload, diskUsage: undefined },\n        }));\n      });\n\n      describe('when it has no brokers', () => {\n        beforeEach(() => {\n          (useBrokers as jest.Mock).mockImplementation(() => ({\n            data: [],\n          }));\n        });\n\n        it('renders empty table', async () => {\n          renderComponent();\n          expect(screen.getByRole('table')).toBeInTheDocument();\n          expect(\n            screen.getByRole('row', { name: 'No clusters are online' })\n          ).toBeInTheDocument();\n        });\n      });\n\n      it('renders list of all brokers', async () => {\n        renderComponent();\n        expect(screen.getByRole('table')).toBeInTheDocument();\n        expect(screen.getAllByRole('row').length).toEqual(3);\n      });\n      it('opens broker when row clicked', async () => {\n        renderComponent();\n        await userEvent.click(screen.getByRole('cell', { name: '100' }));\n\n        await waitFor(() =>\n          expect(mockedUsedNavigate).toBeCalledWith(\n            clusterBrokerPath(clusterName, '100')\n          )\n        );\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Brokers/__test__/Brokers.spec.tsx",
    "content": "import React from 'react';\nimport { render } from 'lib/testHelpers';\nimport { screen } from '@testing-library/react';\nimport { clusterBrokerPath } from 'lib/paths';\nimport Brokers from 'components/Brokers/Brokers';\n\nconst brokersList = 'brokersList';\nconst broker = 'brokers';\n\njest.mock('components/Brokers/BrokersList/BrokersList', () => () => (\n  <div>{brokersList}</div>\n));\njest.mock('components/Brokers/Broker/Broker', () => () => <div>{broker}</div>);\n\ndescribe('Brokers Component', () => {\n  const clusterName = 'clusterName';\n  const brokerId = '1';\n  const renderComponent = (path?: string) =>\n    render(<Brokers />, {\n      initialEntries: path ? [path] : undefined,\n    });\n\n  it('renders BrokersList', () => {\n    renderComponent();\n    expect(screen.getByText(brokersList)).toBeInTheDocument();\n  });\n\n  it('renders Broker', () => {\n    renderComponent(clusterBrokerPath(clusterName, brokerId));\n    expect(screen.getByText(broker)).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Brokers/utils/__test__/fixtures.ts",
    "content": "import { BrokerMetrics } from 'generated-sources';\n\nexport const brokerMetricsPayload: BrokerMetrics = {\n  segmentSize: 23,\n  segmentCount: 23,\n  metrics: [\n    {\n      name: 'TotalFetchRequestsPerSec',\n      labels: {\n        canonicalName:\n          'kafka.server:name=TotalFetchRequestsPerSec,topic=_connect_status,type=BrokerTopicMetrics',\n      },\n      value: 10,\n    },\n    {\n      name: 'ZooKeeperRequestLatencyMs',\n      value: 11,\n    },\n    {\n      name: 'RequestHandlerAvgIdlePercent',\n    },\n  ],\n};\nexport const transformedBrokerMetricsPayload =\n  '{\"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\"}]}';\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Brokers/utils/__test__/getEditorText.spec.tsx",
    "content": "import { getEditorText } from 'components/Brokers/utils/getEditorText';\n\nimport {\n  brokerMetricsPayload,\n  transformedBrokerMetricsPayload,\n} from './fixtures';\n\ndescribe('Get editor text', () => {\n  it('returns error message when broker metrics is not defined', () => {\n    expect(getEditorText(undefined)).toEqual('Metrics data not available');\n  });\n  it('returns transformed metrics text when broker logdirs metrics', () => {\n    expect(getEditorText(brokerMetricsPayload)).toEqual(\n      transformedBrokerMetricsPayload\n    );\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Brokers/utils/getEditorText.ts",
    "content": "import { BrokerMetrics } from 'generated-sources';\n\nexport const getEditorText = (metrics: BrokerMetrics | undefined): string =>\n  metrics ? JSON.stringify(metrics) : 'Metrics data not available';\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ClusterPage/ClusterConfigPage.tsx",
    "content": "import React from 'react';\nimport { useAppConfig } from 'lib/hooks/api/appConfig';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { ClusterNameRoute } from 'lib/paths';\nimport ClusterConfigForm from 'widgets/ClusterConfigForm';\nimport { getInitialFormData } from 'widgets/ClusterConfigForm/utils/getInitialFormData';\n\nconst ClusterConfigPage: React.FC = () => {\n  const config = useAppConfig();\n  const { clusterName } = useAppParams<ClusterNameRoute>();\n\n  const currentClusterConfig = React.useMemo(() => {\n    if (config.isSuccess && !!config.data.properties?.kafka?.clusters) {\n      const current = config.data.properties?.kafka?.clusters?.find(\n        ({ name }) => name === clusterName\n      );\n      if (current) {\n        return getInitialFormData(current);\n      }\n    }\n    return undefined;\n  }, [clusterName, config]);\n\n  if (!currentClusterConfig) {\n    return null;\n  }\n\n  const hasCustomConfig = Object.values(currentClusterConfig.customAuth).some(\n    (v) => !!v\n  );\n\n  return (\n    <ClusterConfigForm\n      initialValues={currentClusterConfig}\n      hasCustomConfig={hasCustomConfig}\n    />\n  );\n};\n\nexport default ClusterConfigPage;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ClusterPage/ClusterPage.tsx",
    "content": "import React, { Suspense } from 'react';\nimport { Routes, Navigate, Route, Outlet } from 'react-router-dom';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { ClusterFeaturesEnum } from 'generated-sources';\nimport {\n  clusterBrokerRelativePath,\n  clusterConnectorsRelativePath,\n  clusterConnectsRelativePath,\n  clusterConsumerGroupsRelativePath,\n  clusterKsqlDbRelativePath,\n  ClusterNameRoute,\n  clusterSchemasRelativePath,\n  clusterTopicsRelativePath,\n  clusterConfigRelativePath,\n  getNonExactPath,\n  clusterAclRelativePath,\n} from 'lib/paths';\nimport ClusterContext from 'components/contexts/ClusterContext';\nimport PageLoader from 'components/common/PageLoader/PageLoader';\nimport { useClusters } from 'lib/hooks/api/clusters';\nimport { GlobalSettingsContext } from 'components/contexts/GlobalSettingsContext';\n\nconst Brokers = React.lazy(() => import('components/Brokers/Brokers'));\nconst Topics = React.lazy(() => import('components/Topics/Topics'));\nconst Schemas = React.lazy(() => import('components/Schemas/Schemas'));\nconst Connect = React.lazy(() => import('components/Connect/Connect'));\nconst KsqlDb = React.lazy(() => import('components/KsqlDb/KsqlDb'));\nconst ClusterConfigPage = React.lazy(\n  () => import('components/ClusterPage/ClusterConfigPage')\n);\nconst ConsumerGroups = React.lazy(\n  () => import('components/ConsumerGroups/ConsumerGroups')\n);\nconst AclPage = React.lazy(() => import('components/ACLPage/ACLPage'));\n\nconst ClusterPage: React.FC = () => {\n  const { clusterName } = useAppParams<ClusterNameRoute>();\n  const appInfo = React.useContext(GlobalSettingsContext);\n\n  const { data } = useClusters();\n  const contextValue = React.useMemo(() => {\n    const cluster = data?.find(({ name }) => name === clusterName);\n    const features = cluster?.features || [];\n    return {\n      isReadOnly: cluster?.readOnly || false,\n      hasKafkaConnectConfigured: features.includes(\n        ClusterFeaturesEnum.KAFKA_CONNECT\n      ),\n      hasSchemaRegistryConfigured: features.includes(\n        ClusterFeaturesEnum.SCHEMA_REGISTRY\n      ),\n      isTopicDeletionAllowed: features.includes(\n        ClusterFeaturesEnum.TOPIC_DELETION\n      ),\n      hasKsqlDbConfigured: features.includes(ClusterFeaturesEnum.KSQL_DB),\n      hasAclViewConfigured:\n        features.includes(ClusterFeaturesEnum.KAFKA_ACL_VIEW) ||\n        features.includes(ClusterFeaturesEnum.KAFKA_ACL_EDIT),\n    };\n  }, [clusterName, data]);\n\n  return (\n    <Suspense fallback={<PageLoader />}>\n      <ClusterContext.Provider value={contextValue}>\n        <Suspense fallback={<PageLoader />}>\n          <Routes>\n            <Route\n              path={getNonExactPath(clusterBrokerRelativePath)}\n              element={<Brokers />}\n            />\n            <Route\n              path={getNonExactPath(clusterTopicsRelativePath)}\n              element={<Topics />}\n            />\n            <Route\n              path={getNonExactPath(clusterConsumerGroupsRelativePath)}\n              element={<ConsumerGroups />}\n            />\n            {contextValue.hasSchemaRegistryConfigured && (\n              <Route\n                path={getNonExactPath(clusterSchemasRelativePath)}\n                element={<Schemas />}\n              />\n            )}\n            {contextValue.hasKafkaConnectConfigured && (\n              <Route\n                path={getNonExactPath(clusterConnectsRelativePath)}\n                element={<Connect />}\n              />\n            )}\n            {contextValue.hasKafkaConnectConfigured && (\n              <Route\n                path={getNonExactPath(clusterConnectorsRelativePath)}\n                element={<Connect />}\n              />\n            )}\n            {contextValue.hasKsqlDbConfigured && (\n              <Route\n                path={getNonExactPath(clusterKsqlDbRelativePath)}\n                element={<KsqlDb />}\n              />\n            )}\n            {contextValue.hasAclViewConfigured && (\n              <Route\n                path={getNonExactPath(clusterAclRelativePath)}\n                element={<AclPage />}\n              />\n            )}\n            {appInfo.hasDynamicConfig && (\n              <Route\n                path={getNonExactPath(clusterConfigRelativePath)}\n                element={<ClusterConfigPage />}\n              />\n            )}\n            <Route\n              path=\"/\"\n              element={<Navigate to={clusterBrokerRelativePath} replace />}\n            />\n          </Routes>\n          <Outlet />\n        </Suspense>\n      </ClusterContext.Provider>\n    </Suspense>\n  );\n};\n\nexport default ClusterPage;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ClusterPage/__tests__/ClusterPage.spec.tsx",
    "content": "import React from 'react';\nimport { Cluster, ClusterFeaturesEnum } from 'generated-sources';\nimport ClusterPageComponent from 'components/ClusterPage/ClusterPage';\nimport { screen, waitFor } from '@testing-library/react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport {\n  clusterBrokersPath,\n  clusterConnectorsPath,\n  clusterConnectsPath,\n  clusterConsumerGroupsPath,\n  clusterKsqlDbPath,\n  clusterPath,\n  clusterSchemasPath,\n  clusterTopicsPath,\n} from 'lib/paths';\nimport { useClusters } from 'lib/hooks/api/clusters';\nimport { onlineClusterPayload } from 'lib/fixtures/clusters';\n\nconst CLusterCompText = {\n  Topics: 'Topics',\n  Schemas: 'Schemas',\n  Connect: 'Connect',\n  Brokers: 'Brokers',\n  ConsumerGroups: 'ConsumerGroups',\n  KsqlDb: 'KsqlDb',\n};\n\njest.mock('components/Topics/Topics', () => () => (\n  <div>{CLusterCompText.Topics}</div>\n));\njest.mock('components/Schemas/Schemas', () => () => (\n  <div>{CLusterCompText.Schemas}</div>\n));\njest.mock('components/Connect/Connect', () => () => (\n  <div>{CLusterCompText.Connect}</div>\n));\njest.mock('components/Brokers/Brokers', () => () => (\n  <div>{CLusterCompText.Brokers}</div>\n));\njest.mock('components/ConsumerGroups/ConsumerGroups', () => () => (\n  <div>{CLusterCompText.ConsumerGroups}</div>\n));\njest.mock('components/KsqlDb/KsqlDb', () => () => (\n  <div>{CLusterCompText.KsqlDb}</div>\n));\n\njest.mock('lib/hooks/api/clusters', () => ({\n  useClusters: jest.fn(),\n}));\n\ndescribe('ClusterPage', () => {\n  const renderComponent = async (pathname: string, payload: Cluster[] = []) => {\n    (useClusters as jest.Mock).mockImplementation(() => ({\n      data: payload,\n    }));\n    await render(\n      <WithRoute path={`${clusterPath()}/*`}>\n        <ClusterPageComponent />\n      </WithRoute>,\n      { initialEntries: [pathname] }\n    );\n    await waitFor(() => {\n      expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();\n    });\n  };\n\n  it('renders Brokers', async () => {\n    await renderComponent(clusterBrokersPath('second'));\n    expect(screen.getByText(CLusterCompText.Brokers)).toBeInTheDocument();\n  });\n  it('renders Topics', async () => {\n    await renderComponent(clusterTopicsPath('second'));\n    expect(screen.getByText(CLusterCompText.Topics)).toBeInTheDocument();\n  });\n  it('renders ConsumerGroups', async () => {\n    await renderComponent(clusterConsumerGroupsPath('second'));\n    expect(\n      screen.getByText(CLusterCompText.ConsumerGroups)\n    ).toBeInTheDocument();\n  });\n\n  describe('configured features', () => {\n    const itCorrectlyHandlesConfiguredSchema = (\n      feature: ClusterFeaturesEnum,\n      text: string,\n      path: string\n    ) => {\n      it(`renders Schemas if ${feature} is configured`, async () => {\n        await renderComponent(path, [\n          {\n            ...onlineClusterPayload,\n            features: [feature],\n          },\n        ]);\n        expect(screen.getByText(text)).toBeInTheDocument();\n      });\n\n      it(`does not render Schemas if ${feature} is not configured`, async () => {\n        await renderComponent(path, [\n          { ...onlineClusterPayload, features: [] },\n        ]);\n        expect(screen.queryByText(text)).not.toBeInTheDocument();\n      });\n    };\n\n    itCorrectlyHandlesConfiguredSchema(\n      ClusterFeaturesEnum.SCHEMA_REGISTRY,\n      CLusterCompText.Schemas,\n      clusterSchemasPath(onlineClusterPayload.name)\n    );\n    itCorrectlyHandlesConfiguredSchema(\n      ClusterFeaturesEnum.KAFKA_CONNECT,\n      CLusterCompText.Connect,\n      clusterConnectsPath(onlineClusterPayload.name)\n    );\n    itCorrectlyHandlesConfiguredSchema(\n      ClusterFeaturesEnum.KAFKA_CONNECT,\n      CLusterCompText.Connect,\n      clusterConnectorsPath(onlineClusterPayload.name)\n    );\n    itCorrectlyHandlesConfiguredSchema(\n      ClusterFeaturesEnum.KSQL_DB,\n      CLusterCompText.KsqlDb,\n      clusterKsqlDbPath(onlineClusterPayload.name)\n    );\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/Connect.tsx",
    "content": "import React from 'react';\nimport { Navigate, Routes, Route } from 'react-router-dom';\nimport {\n  RouteParams,\n  clusterConnectConnectorRelativePath,\n  clusterConnectConnectorsRelativePath,\n  clusterConnectorNewRelativePath,\n  getNonExactPath,\n  clusterConnectorsPath,\n} from 'lib/paths';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport SuspenseQueryComponent from 'components/common/SuspenseQueryComponent/SuspenseQueryComponent';\n\nimport ListPage from './List/ListPage';\nimport New from './New/New';\nimport DetailsPage from './Details/DetailsPage';\n\nconst Connect: React.FC = () => {\n  const { clusterName } = useAppParams();\n\n  return (\n    <Routes>\n      <Route index element={<ListPage />} />\n      <Route path={clusterConnectorNewRelativePath} element={<New />} />\n      <Route\n        path={getNonExactPath(clusterConnectConnectorRelativePath)}\n        element={\n          <SuspenseQueryComponent>\n            <DetailsPage />\n          </SuspenseQueryComponent>\n        }\n      />\n      <Route\n        path={clusterConnectConnectorsRelativePath}\n        element={<Navigate to={clusterConnectorsPath(clusterName)} replace />}\n      />\n      <Route\n        path={RouteParams.connectName}\n        element={<Navigate to=\"/\" replace />}\n      />\n    </Routes>\n  );\n};\n\nexport default Connect;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/Details/Actions/Action.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const ConnectorActionsWrapperStyled = styled.div`\n  display: flex;\n  flex-wrap: wrap;\n  align-items: center;\n  gap: 8px;\n`;\nexport const ButtonLabel = styled.span`\n  margin-right: 11.5px;\n`;\nexport const RestartButton = styled.div`\n  padding: 0 12px;\n  border: none;\n  border-radius: 4px;\n  display: flex;\n  -webkit-align-items: center;\n  background: ${({ theme }) => theme.button.primary.backgroundColor.normal};\n  color: ${({ theme }) => theme.button.primary.color.normal};\n  font-size: 14px;\n  font-weight: 500;\n  height: 32px;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx",
    "content": "import React from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { useIsMutating } from '@tanstack/react-query';\nimport {\n  Action,\n  ConnectorAction,\n  ConnectorState,\n  ResourceType,\n} from 'generated-sources';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport {\n  useConnector,\n  useDeleteConnector,\n  useUpdateConnectorState,\n} from 'lib/hooks/api/kafkaConnect';\nimport {\n  clusterConnectorsPath,\n  RouterParamsClusterConnectConnector,\n} from 'lib/paths';\nimport { useConfirm } from 'lib/hooks/useConfirm';\nimport { Dropdown } from 'components/common/Dropdown';\nimport { ActionDropdownItem } from 'components/common/ActionComponent';\nimport ChevronDownIcon from 'components/common/Icons/ChevronDownIcon';\n\nimport * as S from './Action.styled';\n\nconst Actions: React.FC = () => {\n  const navigate = useNavigate();\n  const routerProps = useAppParams<RouterParamsClusterConnectConnector>();\n  const mutationsNumber = useIsMutating();\n  const isMutating = mutationsNumber > 0;\n\n  const { data: connector } = useConnector(routerProps);\n  const confirm = useConfirm();\n\n  const deleteConnectorMutation = useDeleteConnector(routerProps);\n  const deleteConnectorHandler = () =>\n    confirm(\n      <>\n        Are you sure you want to remove <b>{routerProps.connectorName}</b>{' '}\n        connector?\n      </>,\n      async () => {\n        try {\n          await deleteConnectorMutation.mutateAsync();\n          navigate(clusterConnectorsPath(routerProps.clusterName));\n        } catch {\n          // do not redirect\n        }\n      }\n    );\n\n  const stateMutation = useUpdateConnectorState(routerProps);\n  const restartConnectorHandler = () =>\n    stateMutation.mutateAsync(ConnectorAction.RESTART);\n  const restartAllTasksHandler = () =>\n    stateMutation.mutateAsync(ConnectorAction.RESTART_ALL_TASKS);\n  const restartFailedTasksHandler = () =>\n    stateMutation.mutateAsync(ConnectorAction.RESTART_FAILED_TASKS);\n  const pauseConnectorHandler = () =>\n    stateMutation.mutateAsync(ConnectorAction.PAUSE);\n  const resumeConnectorHandler = () =>\n    stateMutation.mutateAsync(ConnectorAction.RESUME);\n  return (\n    <S.ConnectorActionsWrapperStyled>\n      <Dropdown\n        label={\n          <S.RestartButton>\n            <S.ButtonLabel>Restart</S.ButtonLabel>\n            <ChevronDownIcon />\n          </S.RestartButton>\n        }\n      >\n        {connector?.status.state === ConnectorState.RUNNING && (\n          <ActionDropdownItem\n            onClick={pauseConnectorHandler}\n            disabled={isMutating}\n            permission={{\n              resource: ResourceType.CONNECT,\n              action: Action.EDIT,\n              value: routerProps.connectorName,\n            }}\n          >\n            Pause\n          </ActionDropdownItem>\n        )}\n        {connector?.status.state === ConnectorState.PAUSED && (\n          <ActionDropdownItem\n            onClick={resumeConnectorHandler}\n            disabled={isMutating}\n            permission={{\n              resource: ResourceType.CONNECT,\n              action: Action.EDIT,\n              value: routerProps.connectorName,\n            }}\n          >\n            Resume\n          </ActionDropdownItem>\n        )}\n        <ActionDropdownItem\n          onClick={restartConnectorHandler}\n          disabled={isMutating}\n          permission={{\n            resource: ResourceType.CONNECT,\n            action: Action.RESTART,\n            value: routerProps.connectorName,\n          }}\n        >\n          Restart Connector\n        </ActionDropdownItem>\n        <ActionDropdownItem\n          onClick={restartAllTasksHandler}\n          disabled={isMutating}\n          permission={{\n            resource: ResourceType.CONNECT,\n            action: Action.RESTART,\n            value: routerProps.connectorName,\n          }}\n        >\n          Restart All Tasks\n        </ActionDropdownItem>\n        <ActionDropdownItem\n          onClick={restartFailedTasksHandler}\n          disabled={isMutating}\n          permission={{\n            resource: ResourceType.CONNECT,\n            action: Action.RESTART,\n            value: routerProps.connectorName,\n          }}\n        >\n          Restart Failed Tasks\n        </ActionDropdownItem>\n      </Dropdown>\n      <Dropdown>\n        <ActionDropdownItem\n          onClick={deleteConnectorHandler}\n          disabled={isMutating}\n          danger\n          permission={{\n            resource: ResourceType.CONNECT,\n            action: Action.DELETE,\n            value: routerProps.connectorName,\n          }}\n        >\n          Delete\n        </ActionDropdownItem>\n      </Dropdown>\n    </S.ConnectorActionsWrapperStyled>\n  );\n};\n\nexport default Actions;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx",
    "content": "import React from 'react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { clusterConnectConnectorPath } from 'lib/paths';\nimport Actions from 'components/Connect/Details/Actions/Actions';\nimport { ConnectorAction, ConnectorState } from 'generated-sources';\nimport { screen, waitFor } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport {\n  useConnector,\n  useUpdateConnectorState,\n} from 'lib/hooks/api/kafkaConnect';\nimport { connector } from 'lib/fixtures/kafkaConnect';\nimport set from 'lodash/set';\n\nconst mockHistoryPush = jest.fn();\nconst deleteConnector = jest.fn();\nconst cancelMock = jest.fn();\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useNavigate: () => mockHistoryPush,\n}));\n\njest.mock('lib/hooks/api/kafkaConnect', () => ({\n  useConnector: jest.fn(),\n  useDeleteConnector: jest.fn(),\n  useUpdateConnectorState: jest.fn(),\n}));\n\nconst expectActionButtonsExists = () => {\n  expect(screen.getByText('Restart Connector')).toBeInTheDocument();\n  expect(screen.getByText('Restart All Tasks')).toBeInTheDocument();\n  expect(screen.getByText('Restart Failed Tasks')).toBeInTheDocument();\n  expect(screen.getByText('Delete')).toBeInTheDocument();\n};\nconst afterClickDropDownButton = async () => {\n  const dropDownButton = screen.getAllByRole('button');\n  await userEvent.click(dropDownButton[1]);\n};\nconst afterClickRestartButton = async () => {\n  const dropDownButton = screen.getByText('Restart');\n  await userEvent.click(dropDownButton);\n};\ndescribe('Actions', () => {\n  afterEach(() => {\n    mockHistoryPush.mockClear();\n    deleteConnector.mockClear();\n    cancelMock.mockClear();\n  });\n\n  describe('view', () => {\n    const route = clusterConnectConnectorPath();\n    const path = clusterConnectConnectorPath(\n      'myCluster',\n      'myConnect',\n      'myConnector'\n    );\n\n    const renderComponent = () =>\n      render(\n        <WithRoute path={route}>\n          <Actions />\n        </WithRoute>,\n        { initialEntries: [path] }\n      );\n\n    it('renders buttons when paused', async () => {\n      (useConnector as jest.Mock).mockImplementation(() => ({\n        data: set({ ...connector }, 'status.state', ConnectorState.PAUSED),\n      }));\n      renderComponent();\n      await afterClickRestartButton();\n      expect(screen.getAllByRole('menuitem').length).toEqual(4);\n      expect(screen.getByText('Resume')).toBeInTheDocument();\n      expect(screen.queryByText('Pause')).not.toBeInTheDocument();\n      expectActionButtonsExists();\n    });\n\n    it('renders buttons when failed', async () => {\n      (useConnector as jest.Mock).mockImplementation(() => ({\n        data: set({ ...connector }, 'status.state', ConnectorState.FAILED),\n      }));\n      renderComponent();\n      await afterClickRestartButton();\n      expect(screen.getAllByRole('menuitem').length).toEqual(3);\n      expect(screen.queryByText('Resume')).not.toBeInTheDocument();\n      expect(screen.queryByText('Pause')).not.toBeInTheDocument();\n      expectActionButtonsExists();\n    });\n\n    it('renders buttons when unassigned', async () => {\n      (useConnector as jest.Mock).mockImplementation(() => ({\n        data: set({ ...connector }, 'status.state', ConnectorState.UNASSIGNED),\n      }));\n      renderComponent();\n      await afterClickRestartButton();\n      expect(screen.getAllByRole('menuitem').length).toEqual(3);\n      expect(screen.queryByText('Resume')).not.toBeInTheDocument();\n      expect(screen.queryByText('Pause')).not.toBeInTheDocument();\n      expectActionButtonsExists();\n    });\n\n    it('renders buttons when running connector action', async () => {\n      (useConnector as jest.Mock).mockImplementation(() => ({\n        data: set({ ...connector }, 'status.state', ConnectorState.RUNNING),\n      }));\n      renderComponent();\n      await afterClickRestartButton();\n      expect(screen.getAllByRole('menuitem').length).toEqual(4);\n      expect(screen.queryByText('Resume')).not.toBeInTheDocument();\n      expect(screen.getByText('Pause')).toBeInTheDocument();\n      expectActionButtonsExists();\n    });\n\n    describe('mutations', () => {\n      beforeEach(() => {\n        (useConnector as jest.Mock).mockImplementation(() => ({\n          data: set({ ...connector }, 'status.state', ConnectorState.RUNNING),\n        }));\n      });\n\n      it('opens confirmation modal when delete button clicked', async () => {\n        renderComponent();\n        await afterClickDropDownButton();\n        await waitFor(async () =>\n          userEvent.click(screen.getByRole('menuitem', { name: 'Delete' }))\n        );\n        expect(screen.getByRole('dialog')).toBeInTheDocument();\n      });\n\n      it('calls restartConnector when restart button clicked', async () => {\n        const restartConnector = jest.fn();\n        (useUpdateConnectorState as jest.Mock).mockImplementation(() => ({\n          mutateAsync: restartConnector,\n        }));\n        renderComponent();\n        await afterClickRestartButton();\n        await userEvent.click(\n          screen.getByRole('menuitem', { name: 'Restart Connector' })\n        );\n        expect(restartConnector).toHaveBeenCalledWith(ConnectorAction.RESTART);\n      });\n\n      it('calls restartAllTasks', async () => {\n        const restartAllTasks = jest.fn();\n        (useUpdateConnectorState as jest.Mock).mockImplementation(() => ({\n          mutateAsync: restartAllTasks,\n        }));\n        renderComponent();\n        await afterClickRestartButton();\n        await userEvent.click(\n          screen.getByRole('menuitem', { name: 'Restart All Tasks' })\n        );\n        expect(restartAllTasks).toHaveBeenCalledWith(\n          ConnectorAction.RESTART_ALL_TASKS\n        );\n      });\n\n      it('calls restartFailedTasks', async () => {\n        const restartFailedTasks = jest.fn();\n        (useUpdateConnectorState as jest.Mock).mockImplementation(() => ({\n          mutateAsync: restartFailedTasks,\n        }));\n        renderComponent();\n        await afterClickRestartButton();\n        await userEvent.click(\n          screen.getByRole('menuitem', { name: 'Restart Failed Tasks' })\n        );\n        expect(restartFailedTasks).toHaveBeenCalledWith(\n          ConnectorAction.RESTART_FAILED_TASKS\n        );\n      });\n\n      it('calls pauseConnector when pause button clicked', async () => {\n        const pauseConnector = jest.fn();\n        (useUpdateConnectorState as jest.Mock).mockImplementation(() => ({\n          mutateAsync: pauseConnector,\n        }));\n        renderComponent();\n        await afterClickRestartButton();\n        await userEvent.click(screen.getByRole('menuitem', { name: 'Pause' }));\n        expect(pauseConnector).toHaveBeenCalledWith(ConnectorAction.PAUSE);\n      });\n\n      it('calls resumeConnector when resume button clicked', async () => {\n        const resumeConnector = jest.fn();\n        (useConnector as jest.Mock).mockImplementation(() => ({\n          data: set({ ...connector }, 'status.state', ConnectorState.PAUSED),\n        }));\n        (useUpdateConnectorState as jest.Mock).mockImplementation(() => ({\n          mutateAsync: resumeConnector,\n        }));\n        renderComponent();\n        await afterClickRestartButton();\n        await userEvent.click(screen.getByRole('menuitem', { name: 'Resume' }));\n        expect(resumeConnector).toHaveBeenCalledWith(ConnectorAction.RESUME);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/Details/Config/Config.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const ConnectEditWrapperStyled = styled.div`\n  margin: 16px;\n\n  & form > *:last-child {\n    margin-top: 16px;\n  }\n`;\n\nexport const ConnectEditWarningMessageStyled = styled.div`\n  height: 48px;\n  display: flex;\n  align-items: center;\n  background-color: ${({ theme }) => theme.connectEditWarning};\n  border-radius: 8px;\n  padding: 8px;\n  margin-bottom: 16px;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx",
    "content": "import React from 'react';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { Controller, useForm } from 'react-hook-form';\nimport { ErrorMessage } from '@hookform/error-message';\nimport { yupResolver } from '@hookform/resolvers/yup';\nimport { RouterParamsClusterConnectConnector } from 'lib/paths';\nimport yup from 'lib/yupExtended';\nimport Editor from 'components/common/Editor/Editor';\nimport { Button } from 'components/common/Button/Button';\nimport {\n  useConnectorConfig,\n  useUpdateConnectorConfig,\n} from 'lib/hooks/api/kafkaConnect';\n\nimport {\n  ConnectEditWarningMessageStyled,\n  ConnectEditWrapperStyled,\n} from './Config.styled';\n\nconst validationSchema = yup.object().shape({\n  config: yup.string().required().isJsonObject(),\n});\n\ninterface FormValues {\n  config: string;\n}\n\nconst Config: React.FC = () => {\n  const routerParams = useAppParams<RouterParamsClusterConnectConnector>();\n  const { data: config } = useConnectorConfig(routerParams);\n  const mutation = useUpdateConnectorConfig(routerParams);\n\n  const {\n    handleSubmit,\n    control,\n    reset,\n    formState: { isDirty, isSubmitting, isValid, errors },\n    setValue,\n  } = useForm<FormValues>({\n    mode: 'onChange',\n    resolver: yupResolver(validationSchema),\n    defaultValues: {\n      config: JSON.stringify(config, null, '\\t'),\n    },\n  });\n\n  React.useEffect(() => {\n    if (config) {\n      setValue('config', JSON.stringify(config, null, '\\t'));\n    }\n  }, [config, setValue]);\n\n  const onSubmit = async (values: FormValues) => {\n    try {\n      const requestBody = JSON.parse(values.config.trim());\n      await mutation.mutateAsync(requestBody);\n      reset(values);\n    } catch (e) {\n      // do nothing\n    }\n  };\n\n  const hasCredentials = JSON.stringify(config, null, '\\t').includes(\n    '\"******\"'\n  );\n  return (\n    <ConnectEditWrapperStyled>\n      {hasCredentials && (\n        <ConnectEditWarningMessageStyled>\n          Please replace ****** with the real credential values to avoid\n          accidentally breaking your connector config!\n        </ConnectEditWarningMessageStyled>\n      )}\n      <form onSubmit={handleSubmit(onSubmit)} aria-label=\"Edit connect form\">\n        <div>\n          <Controller\n            control={control}\n            name=\"config\"\n            render={({ field }) => (\n              <Editor {...field} readOnly={isSubmitting} />\n            )}\n          />\n        </div>\n        <div>\n          <ErrorMessage errors={errors} name=\"config\" />\n        </div>\n        <Button\n          buttonSize=\"M\"\n          buttonType=\"primary\"\n          type=\"submit\"\n          disabled={!isValid || isSubmitting || !isDirty}\n        >\n          Submit\n        </Button>\n      </form>\n    </ConnectEditWrapperStyled>\n  );\n};\n\nexport default Config;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/Details/Config/__tests__/Config.spec.tsx",
    "content": "import React from 'react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { clusterConnectConnectorConfigPath } from 'lib/paths';\nimport Config from 'components/Connect/Details/Config/Config';\nimport { connector } from 'lib/fixtures/kafkaConnect';\nimport { waitFor } from '@testing-library/dom';\nimport { act, fireEvent, screen } from '@testing-library/react';\nimport {\n  useConnectorConfig,\n  useUpdateConnectorConfig,\n} from 'lib/hooks/api/kafkaConnect';\n\njest.mock('components/common/Editor/Editor', () => 'mock-Editor');\n\nconst mockHistoryPush = jest.fn();\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useNavigate: () => mockHistoryPush,\n}));\njest.mock('lib/hooks/api/kafkaConnect', () => ({\n  useConnectorConfig: jest.fn(),\n  useUpdateConnectorConfig: jest.fn(),\n}));\n\nconst [clusterName, connectName, connectorName] = [\n  'my-cluster',\n  'my-connect',\n  'my-connector',\n];\n\ndescribe('Config', () => {\n  const pathname = clusterConnectConnectorConfigPath();\n  const renderComponent = () =>\n    render(\n      <WithRoute path={pathname}>\n        <Config />\n      </WithRoute>,\n      {\n        initialEntries: [\n          clusterConnectConnectorConfigPath(\n            clusterName,\n            connectName,\n            connectorName\n          ),\n        ],\n      }\n    );\n\n  beforeEach(() => {\n    (useConnectorConfig as jest.Mock).mockImplementation(() => ({\n      data: connector.config,\n    }));\n  });\n\n  it('calls updateConfig and redirects to connector config view on successful submit', async () => {\n    const updateConfig = jest.fn(() => {\n      return Promise.resolve(connector);\n    });\n    (useUpdateConnectorConfig as jest.Mock).mockImplementation(() => ({\n      mutateAsync: updateConfig,\n    }));\n\n    renderComponent();\n    fireEvent.submit(screen.getByRole('form'));\n    await waitFor(() => expect(updateConfig).toHaveBeenCalledTimes(1));\n  });\n\n  it('does not redirect to connector config view on unsuccessful submit', async () => {\n    const updateConfig = jest.fn(() => {\n      return Promise.resolve();\n    });\n    (useUpdateConnectorConfig as jest.Mock).mockImplementation(() => ({\n      mutateAsync: updateConfig,\n    }));\n    renderComponent();\n    await act(() => {\n      fireEvent.submit(screen.getByRole('form'));\n    });\n    expect(mockHistoryPush).not.toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/Details/DetailsPage.tsx",
    "content": "import React, { Suspense } from 'react';\nimport { NavLink, Route, Routes } from 'react-router-dom';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport {\n  clusterConnectConnectorConfigPath,\n  clusterConnectConnectorConfigRelativePath,\n  clusterConnectConnectorPath,\n  clusterConnectorsPath,\n  RouterParamsClusterConnectConnector,\n} from 'lib/paths';\nimport Navbar from 'components/common/Navigation/Navbar.styled';\nimport PageHeading from 'components/common/PageHeading/PageHeading';\nimport PageLoader from 'components/common/PageLoader/PageLoader';\n\nimport Overview from './Overview/Overview';\nimport Tasks from './Tasks/Tasks';\nimport Config from './Config/Config';\nimport Actions from './Actions/Actions';\n\nconst DetailsPage: React.FC = () => {\n  const { clusterName, connectName, connectorName } =\n    useAppParams<RouterParamsClusterConnectConnector>();\n\n  return (\n    <div>\n      <PageHeading\n        text={connectorName}\n        backTo={clusterConnectorsPath(clusterName)}\n        backText=\"Connectors\"\n      >\n        <Actions />\n      </PageHeading>\n      <Overview />\n      <Navbar role=\"navigation\">\n        <NavLink\n          to={clusterConnectConnectorPath(\n            clusterName,\n            connectName,\n            connectorName\n          )}\n          className={({ isActive }) => (isActive ? 'is-active' : '')}\n          end\n        >\n          Tasks\n        </NavLink>\n        <NavLink\n          to={clusterConnectConnectorConfigPath(\n            clusterName,\n            connectName,\n            connectorName\n          )}\n          className={({ isActive }) => (isActive ? 'is-active' : '')}\n        >\n          Config\n        </NavLink>\n      </Navbar>\n      <Suspense fallback={<PageLoader />}>\n        <Routes>\n          <Route index element={<Tasks />} />\n          <Route\n            path={clusterConnectConnectorConfigRelativePath}\n            element={<Config />}\n          />\n        </Routes>\n      </Suspense>\n    </div>\n  );\n};\n\nexport default DetailsPage;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/Details/Overview/Overview.tsx",
    "content": "import React from 'react';\nimport * as C from 'components/common/Tag/Tag.styled';\nimport * as Metrics from 'components/common/Metrics';\nimport getTagColor from 'components/common/Tag/getTagColor';\nimport { RouterParamsClusterConnectConnector } from 'lib/paths';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { useConnector, useConnectorTasks } from 'lib/hooks/api/kafkaConnect';\n\nimport getTaskMetrics from './getTaskMetrics';\n\nconst Overview: React.FC = () => {\n  const routerProps = useAppParams<RouterParamsClusterConnectConnector>();\n\n  const { data: connector } = useConnector(routerProps);\n  const { data: tasks } = useConnectorTasks(routerProps);\n\n  if (!connector) {\n    return null;\n  }\n\n  const { running, failed } = getTaskMetrics(tasks);\n\n  return (\n    <Metrics.Wrapper>\n      <Metrics.Section>\n        {connector.status?.workerId && (\n          <Metrics.Indicator label=\"Worker\">\n            {connector.status.workerId}\n          </Metrics.Indicator>\n        )}\n        <Metrics.Indicator label=\"Type\">{connector.type}</Metrics.Indicator>\n        {connector.config['connector.class'] && (\n          <Metrics.Indicator label=\"Class\">\n            {connector.config['connector.class']}\n          </Metrics.Indicator>\n        )}\n        <Metrics.Indicator label=\"State\">\n          <C.Tag color={getTagColor(connector.status.state)}>\n            {connector.status.state}\n          </C.Tag>\n        </Metrics.Indicator>\n        <Metrics.Indicator label=\"Tasks Running\">{running}</Metrics.Indicator>\n        <Metrics.Indicator\n          label=\"Tasks Failed\"\n          isAlert\n          alertType={failed > 0 ? 'error' : 'success'}\n        >\n          {failed}\n        </Metrics.Indicator>\n      </Metrics.Section>\n    </Metrics.Wrapper>\n  );\n};\n\nexport default Overview;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/Details/Overview/__tests__/Overview.spec.tsx",
    "content": "import React from 'react';\nimport Overview from 'components/Connect/Details/Overview/Overview';\nimport { connector, tasks } from 'lib/fixtures/kafkaConnect';\nimport { screen } from '@testing-library/react';\nimport { render } from 'lib/testHelpers';\nimport { useConnector, useConnectorTasks } from 'lib/hooks/api/kafkaConnect';\n\njest.mock('lib/hooks/api/kafkaConnect', () => ({\n  useConnector: jest.fn(),\n  useConnectorTasks: jest.fn(),\n}));\n\ndescribe('Overview', () => {\n  it('is empty when no connector', () => {\n    (useConnector as jest.Mock).mockImplementation(() => ({\n      data: undefined,\n    }));\n    (useConnectorTasks as jest.Mock).mockImplementation(() => ({\n      data: undefined,\n    }));\n\n    render(<Overview />);\n    expect(screen.queryByText('Worker')).not.toBeInTheDocument();\n  });\n\n  describe('when connector is loaded', () => {\n    beforeEach(() => {\n      (useConnector as jest.Mock).mockImplementation(() => ({\n        data: connector,\n      }));\n    });\n    beforeEach(() => {\n      (useConnectorTasks as jest.Mock).mockImplementation(() => ({\n        data: tasks,\n      }));\n    });\n\n    it('renders metrics', () => {\n      render(<Overview />);\n\n      expect(screen.getByText('Worker')).toBeInTheDocument();\n      expect(\n        screen.getByText(connector.status.workerId as string)\n      ).toBeInTheDocument();\n\n      expect(screen.getByText('Type')).toBeInTheDocument();\n      expect(\n        screen.getByText(connector.config['connector.class'] as string)\n      ).toBeInTheDocument();\n\n      expect(screen.getByText('Tasks Running')).toBeInTheDocument();\n      expect(screen.getByText(2)).toBeInTheDocument();\n      expect(screen.getByText('Tasks Failed')).toBeInTheDocument();\n      expect(screen.getByText(1)).toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/Details/Overview/__tests__/getTaskMetrics.spec.ts",
    "content": "import { tasks } from 'lib/fixtures/kafkaConnect';\nimport getTaskMetrics from 'components/Connect/Details/Overview/getTaskMetrics';\n\ndescribe('getTaskMetrics', () => {\n  it('should return the correct metrics when task list is undefined', () => {\n    const metrics = getTaskMetrics();\n    expect(metrics).toEqual({\n      running: 0,\n      failed: 0,\n    });\n  });\n\n  it('should return the correct metrics', () => {\n    expect(getTaskMetrics(tasks)).toEqual({\n      running: 2,\n      failed: 1,\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/Details/Overview/getTaskMetrics.ts",
    "content": "import { ConnectorTaskStatus, Task } from 'generated-sources';\n\nexport default function getTaskMetrics(tasks?: Task[]) {\n  const initialMetrics = {\n    running: 0,\n    failed: 0,\n  };\n\n  if (!tasks) {\n    return initialMetrics;\n  }\n\n  return tasks.reduce((acc, { status }) => {\n    const state = status?.state;\n    if (state === ConnectorTaskStatus.RUNNING) {\n      return { ...acc, running: acc.running + 1 };\n    }\n    if (state === ConnectorTaskStatus.FAILED) {\n      return { ...acc, failed: acc.failed + 1 };\n    }\n    return acc;\n  }, initialMetrics);\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/Details/Tasks/ActionsCellTasks.tsx",
    "content": "import React from 'react';\nimport { Action, ResourceType, Task } from 'generated-sources';\nimport { CellContext } from '@tanstack/react-table';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { useRestartConnectorTask } from 'lib/hooks/api/kafkaConnect';\nimport { Dropdown } from 'components/common/Dropdown';\nimport { ActionDropdownItem } from 'components/common/ActionComponent';\nimport { RouterParamsClusterConnectConnector } from 'lib/paths';\n\nconst ActionsCellTasks: React.FC<CellContext<Task, unknown>> = ({ row }) => {\n  const { id } = row.original;\n  const routerProps = useAppParams<RouterParamsClusterConnectConnector>();\n  const restartMutation = useRestartConnectorTask(routerProps);\n\n  const restartTaskHandler = (taskId?: number) => {\n    if (taskId === undefined) return;\n    restartMutation.mutateAsync(taskId);\n  };\n\n  return (\n    <Dropdown>\n      <ActionDropdownItem\n        onClick={() => restartTaskHandler(id?.task)}\n        danger\n        confirm=\"Are you sure you want to restart the task?\"\n        permission={{\n          resource: ResourceType.CONNECT,\n          action: Action.RESTART,\n          value: routerProps.connectorName,\n        }}\n      >\n        <span>Restart task</span>\n      </ActionDropdownItem>\n    </Dropdown>\n  );\n};\n\nexport default ActionsCellTasks;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/Details/Tasks/Tasks.tsx",
    "content": "import React from 'react';\nimport { useConnectorTasks } from 'lib/hooks/api/kafkaConnect';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { RouterParamsClusterConnectConnector } from 'lib/paths';\nimport { ColumnDef, Row } from '@tanstack/react-table';\nimport { Task } from 'generated-sources';\nimport Table, { TagCell } from 'components/common/NewTable';\n\nimport ActionsCellTasks from './ActionsCellTasks';\n\nconst ExpandedTaskRow: React.FC<{ row: Row<Task> }> = ({ row }) => {\n  return <div>{row.original.status.trace}</div>;\n};\n\nconst MAX_LENGTH = 100;\n\nconst Tasks: React.FC = () => {\n  const routerProps = useAppParams<RouterParamsClusterConnectConnector>();\n  const { data = [] } = useConnectorTasks(routerProps);\n\n  const columns = React.useMemo<ColumnDef<Task>[]>(\n    () => [\n      { header: 'ID', accessorKey: 'status.id' },\n      { header: 'Worker', accessorKey: 'status.workerId' },\n      { header: 'State', accessorKey: 'status.state', cell: TagCell },\n      {\n        header: 'Trace',\n        accessorKey: 'status.trace',\n        enableSorting: false,\n        cell: ({ getValue }) => {\n          const trace = getValue<string>() || '';\n          return trace.toString().length > MAX_LENGTH\n            ? `${trace.toString().substring(0, MAX_LENGTH - 3)}...`\n            : trace;\n        },\n        meta: { width: '70%' },\n      },\n      {\n        id: 'actions',\n        header: '',\n        cell: ActionsCellTasks,\n      },\n    ],\n    []\n  );\n\n  return (\n    <Table\n      columns={columns}\n      data={data}\n      emptyMessage=\"No tasks found\"\n      enableSorting\n      getRowCanExpand={(row) => row.original.status.trace?.length > 0}\n      renderSubComponent={ExpandedTaskRow}\n    />\n  );\n};\n\nexport default Tasks;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx",
    "content": "import React from 'react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { clusterConnectConnectorTasksPath } from 'lib/paths';\nimport Tasks from 'components/Connect/Details/Tasks/Tasks';\nimport { tasks } from 'lib/fixtures/kafkaConnect';\nimport { screen, within, waitFor } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport {\n  useConnectorTasks,\n  useRestartConnectorTask,\n} from 'lib/hooks/api/kafkaConnect';\nimport { Task } from 'generated-sources';\n\njest.mock('lib/hooks/api/kafkaConnect', () => ({\n  useConnectorTasks: jest.fn(),\n  useRestartConnectorTask: jest.fn(),\n}));\n\nconst path = clusterConnectConnectorTasksPath('local', 'ghp', '1');\n\nconst restartConnectorMock = jest.fn();\n\ndescribe('Tasks', () => {\n  beforeEach(() => {\n    (useRestartConnectorTask as jest.Mock).mockImplementation(() => ({\n      mutateAsync: restartConnectorMock,\n    }));\n  });\n\n  const renderComponent = (currentData: Task[] | undefined = undefined) => {\n    (useConnectorTasks as jest.Mock).mockImplementation(() => ({\n      data: currentData,\n    }));\n\n    render(\n      <WithRoute path={clusterConnectConnectorTasksPath()}>\n        <Tasks />\n      </WithRoute>,\n      { initialEntries: [path] }\n    );\n  };\n\n  it('renders empty table', () => {\n    renderComponent();\n    expect(screen.getByRole('table')).toBeInTheDocument();\n    expect(screen.getByText('No tasks found')).toBeInTheDocument();\n  });\n\n  it('renders tasks table', () => {\n    renderComponent(tasks);\n    expect(screen.getAllByRole('row').length).toEqual(tasks.length + 1);\n\n    expect(\n      screen.getByRole('row', {\n        name: '1 kafka-connect0:8083 RUNNING',\n      })\n    ).toBeInTheDocument();\n  });\n\n  it('renders truncates long trace and expands', async () => {\n    renderComponent(tasks);\n\n    const trace = tasks[2]?.status?.trace || '';\n    const truncatedTrace = trace.toString().substring(0, 100 - 3);\n\n    const thirdRow = screen.getByRole('row', {\n      name: `3 kafka-connect0:8083 RUNNING ${truncatedTrace}...`,\n    });\n    expect(thirdRow).toBeInTheDocument();\n\n    const expandedDetails = screen.queryByText(trace);\n    //  Full trace is not visible\n    expect(expandedDetails).not.toBeInTheDocument();\n\n    await userEvent.click(thirdRow);\n\n    expect(\n      screen.getByRole('row', {\n        name: trace,\n      })\n    ).toBeInTheDocument();\n  });\n\n  describe('Action button', () => {\n    const expectDropdownExists = async () => {\n      const firstTaskRow = screen.getByRole('row', {\n        name: '1 kafka-connect0:8083 RUNNING',\n      });\n      expect(firstTaskRow).toBeInTheDocument();\n      const extBtn = within(firstTaskRow).getByRole('button', {\n        name: 'Dropdown Toggle',\n      });\n      expect(extBtn).toBeEnabled();\n      await userEvent.click(extBtn);\n      expect(screen.getByRole('menu')).toBeInTheDocument();\n    };\n\n    it('renders action button', async () => {\n      renderComponent(tasks);\n      await expectDropdownExists();\n      expect(\n        screen.getAllByRole('button', { name: 'Dropdown Toggle' }).length\n      ).toEqual(tasks.length);\n      // Action buttons are enabled\n      const actionBtn = screen.getAllByRole('menuitem');\n      expect(actionBtn[0]).toHaveTextContent('Restart task');\n    });\n\n    it('works as expected', async () => {\n      renderComponent(tasks);\n      await expectDropdownExists();\n      const actionBtn = screen.getAllByRole('menuitem');\n      expect(actionBtn[0]).toHaveTextContent('Restart task');\n\n      await userEvent.click(actionBtn[0]);\n      expect(\n        screen.getByText('Are you sure you want to restart the task?')\n      ).toBeInTheDocument();\n\n      expect(screen.getByText('Confirm the action')).toBeInTheDocument();\n      userEvent.click(screen.getByRole('button', { name: 'Confirm' }));\n\n      await waitFor(() => expect(restartConnectorMock).toHaveBeenCalled());\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/Details/__tests__/DetailsPage.spec.tsx",
    "content": "import React from 'react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport {\n  clusterConnectConnectorConfigPath,\n  clusterConnectConnectorPath,\n  getNonExactPath,\n} from 'lib/paths';\nimport { screen } from '@testing-library/dom';\nimport DetailsPage from 'components/Connect/Details/DetailsPage';\n\nconst DetailsCompText = {\n  overview: 'Overview Pane',\n  tasks: 'Tasks Page',\n  config: 'Config Page',\n  actions: 'Actions',\n};\n\njest.mock('components/Connect/Details/Overview/Overview', () => () => (\n  <div>{DetailsCompText.overview}</div>\n));\n\njest.mock('components/Connect/Details/Tasks/Tasks', () => () => (\n  <div>{DetailsCompText.tasks}</div>\n));\n\njest.mock('components/Connect/Details/Config/Config', () => () => (\n  <div>{DetailsCompText.config}</div>\n));\n\njest.mock('components/Connect/Details/Actions/Actions', () => () => (\n  <div>{DetailsCompText.actions}</div>\n));\n\ndescribe('Details Page', () => {\n  const clusterName = 'my-cluster';\n  const connectName = 'my-connect';\n  const connectorName = 'my-connector';\n  const defaultPath = clusterConnectConnectorPath(\n    clusterName,\n    connectName,\n    connectorName\n  );\n\n  const renderComponent = (path: string = defaultPath) =>\n    render(\n      <WithRoute path={getNonExactPath(clusterConnectConnectorPath())}>\n        <DetailsPage />\n      </WithRoute>,\n      { initialEntries: [path] }\n    );\n\n  it('renders actions', () => {\n    renderComponent();\n    expect(screen.getByText(DetailsCompText.actions));\n  });\n\n  it('renders overview pane', () => {\n    renderComponent();\n    expect(screen.getByText(DetailsCompText.overview));\n  });\n\n  describe('Router component tests', () => {\n    it('should test if tasks is rendering', () => {\n      renderComponent();\n      expect(screen.getByText(DetailsCompText.tasks));\n    });\n\n    it('should test if list is rendering', () => {\n      const path = clusterConnectConnectorConfigPath(\n        clusterName,\n        connectName,\n        connectorName\n      );\n      renderComponent(path);\n      expect(screen.getByText(DetailsCompText.config));\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/List/ActionsCell.tsx",
    "content": "import React from 'react';\nimport {\n  Action,\n  ConnectorAction,\n  ConnectorState,\n  FullConnectorInfo,\n  ResourceType,\n} from 'generated-sources';\nimport { CellContext } from '@tanstack/react-table';\nimport { ClusterNameRoute } from 'lib/paths';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { Dropdown, DropdownItem } from 'components/common/Dropdown';\nimport {\n  useDeleteConnector,\n  useUpdateConnectorState,\n} from 'lib/hooks/api/kafkaConnect';\nimport { useConfirm } from 'lib/hooks/useConfirm';\nimport { useIsMutating } from '@tanstack/react-query';\nimport { ActionDropdownItem } from 'components/common/ActionComponent';\n\nconst ActionsCell: React.FC<CellContext<FullConnectorInfo, unknown>> = ({\n  row,\n}) => {\n  const { connect, name, status } = row.original;\n  const { clusterName } = useAppParams<ClusterNameRoute>();\n  const mutationsNumber = useIsMutating();\n  const isMutating = mutationsNumber > 0;\n  const confirm = useConfirm();\n  const deleteMutation = useDeleteConnector({\n    clusterName,\n    connectName: connect,\n    connectorName: name,\n  });\n  const stateMutation = useUpdateConnectorState({\n    clusterName,\n    connectName: connect,\n    connectorName: name,\n  });\n  const handleDelete = () => {\n    confirm(\n      <>\n        Are you sure want to remove <b>{name}</b> connector?\n      </>,\n      async () => {\n        await deleteMutation.mutateAsync();\n      }\n    );\n  };\n  // const stateMutation = useUpdateConnectorState(routerProps);\n  const resumeConnectorHandler = () =>\n    stateMutation.mutateAsync(ConnectorAction.RESUME);\n  const restartConnectorHandler = () =>\n    stateMutation.mutateAsync(ConnectorAction.RESTART);\n\n  const restartAllTasksHandler = () =>\n    stateMutation.mutateAsync(ConnectorAction.RESTART_ALL_TASKS);\n\n  const restartFailedTasksHandler = () =>\n    stateMutation.mutateAsync(ConnectorAction.RESTART_FAILED_TASKS);\n\n  return (\n    <Dropdown>\n      {status.state === ConnectorState.PAUSED && (\n        <ActionDropdownItem\n          onClick={resumeConnectorHandler}\n          disabled={isMutating}\n          permission={{\n            resource: ResourceType.CONNECT,\n            action: Action.EDIT,\n            value: name,\n          }}\n        >\n          Resume\n        </ActionDropdownItem>\n      )}\n      <ActionDropdownItem\n        onClick={restartConnectorHandler}\n        disabled={isMutating}\n        permission={{\n          resource: ResourceType.CONNECT,\n          action: Action.RESTART,\n          value: name,\n        }}\n      >\n        Restart Connector\n      </ActionDropdownItem>\n      <ActionDropdownItem\n        onClick={restartAllTasksHandler}\n        disabled={isMutating}\n        permission={{\n          resource: ResourceType.CONNECT,\n          action: Action.RESTART,\n          value: name,\n        }}\n      >\n        Restart All Tasks\n      </ActionDropdownItem>\n      <ActionDropdownItem\n        onClick={restartFailedTasksHandler}\n        disabled={isMutating}\n        permission={{\n          resource: ResourceType.CONNECT,\n          action: Action.RESTART,\n          value: name,\n        }}\n      >\n        Restart Failed Tasks\n      </ActionDropdownItem>\n      <DropdownItem onClick={handleDelete} danger>\n        Remove Connector\n      </DropdownItem>\n    </Dropdown>\n  );\n};\n\nexport default ActionsCell;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/List/List.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const TagsWrapper = styled.div`\n  display: flex;\n  flex-wrap: wrap;\n  span {\n    color: rgb(76, 76, 255) !important;\n    &:hover {\n      color: rgb(23, 23, 207) !important;\n    }\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/List/List.tsx",
    "content": "import React from 'react';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { clusterConnectConnectorPath, ClusterNameRoute } from 'lib/paths';\nimport Table, { TagCell } from 'components/common/NewTable';\nimport { FullConnectorInfo } from 'generated-sources';\nimport { useConnectors } from 'lib/hooks/api/kafkaConnect';\nimport { ColumnDef } from '@tanstack/react-table';\nimport { useNavigate, useSearchParams } from 'react-router-dom';\n\nimport ActionsCell from './ActionsCell';\nimport TopicsCell from './TopicsCell';\nimport RunningTasksCell from './RunningTasksCell';\n\nconst List: React.FC = () => {\n  const navigate = useNavigate();\n  const { clusterName } = useAppParams<ClusterNameRoute>();\n  const [searchParams] = useSearchParams();\n  const { data: connectors } = useConnectors(\n    clusterName,\n    searchParams.get('q') || ''\n  );\n\n  const columns = React.useMemo<ColumnDef<FullConnectorInfo>[]>(\n    () => [\n      { header: 'Name', accessorKey: 'name' },\n      { header: 'Connect', accessorKey: 'connect' },\n      { header: 'Type', accessorKey: 'type' },\n      { header: 'Plugin', accessorKey: 'connectorClass' },\n      { header: 'Topics', cell: TopicsCell },\n      { header: 'Status', accessorKey: 'status.state', cell: TagCell },\n      { header: 'Running Tasks', cell: RunningTasksCell },\n      { header: '', id: 'action', cell: ActionsCell },\n    ],\n    []\n  );\n\n  return (\n    <Table\n      data={connectors || []}\n      columns={columns}\n      enableSorting\n      onRowClick={({ original: { connect, name } }) =>\n        navigate(clusterConnectConnectorPath(clusterName, connect, name))\n      }\n      emptyMessage=\"No connectors found\"\n    />\n  );\n};\n\nexport default List;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/List/ListPage.tsx",
    "content": "import React, { Suspense } from 'react';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { clusterConnectorNewRelativePath, ClusterNameRoute } from 'lib/paths';\nimport ClusterContext from 'components/contexts/ClusterContext';\nimport Search from 'components/common/Search/Search';\nimport * as Metrics from 'components/common/Metrics';\nimport PageHeading from 'components/common/PageHeading/PageHeading';\nimport { ActionButton } from 'components/common/ActionComponent';\nimport { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';\nimport PageLoader from 'components/common/PageLoader/PageLoader';\nimport { Action, ConnectorState, ResourceType } from 'generated-sources';\nimport { useConnectors } from 'lib/hooks/api/kafkaConnect';\n\nimport List from './List';\n\nconst ListPage: React.FC = () => {\n  const { isReadOnly } = React.useContext(ClusterContext);\n  const { clusterName } = useAppParams<ClusterNameRoute>();\n\n  // Fetches all connectors from the API, without search criteria. Used to display general metrics.\n  const { data: connectorsMetrics, isLoading } = useConnectors(clusterName);\n\n  const numberOfFailedConnectors = connectorsMetrics?.filter(\n    ({ status: { state } }) => state === ConnectorState.FAILED\n  ).length;\n\n  const numberOfFailedTasks = connectorsMetrics?.reduce(\n    (acc, metric) => acc + (metric.failedTasksCount ?? 0),\n    0\n  );\n\n  return (\n    <>\n      <PageHeading text=\"Connectors\">\n        {!isReadOnly && (\n          <ActionButton\n            buttonType=\"primary\"\n            buttonSize=\"M\"\n            to={clusterConnectorNewRelativePath}\n            permission={{\n              resource: ResourceType.CONNECT,\n              action: Action.CREATE,\n            }}\n          >\n            Create Connector\n          </ActionButton>\n        )}\n      </PageHeading>\n      <Metrics.Wrapper>\n        <Metrics.Section>\n          <Metrics.Indicator\n            label=\"Connectors\"\n            title=\"Total number of connectors\"\n            fetching={isLoading}\n          >\n            {connectorsMetrics?.length || '-'}\n          </Metrics.Indicator>\n          <Metrics.Indicator\n            label=\"Failed Connectors\"\n            title=\"Number of failed connectors\"\n            fetching={isLoading}\n          >\n            {numberOfFailedConnectors ?? '-'}\n          </Metrics.Indicator>\n          <Metrics.Indicator\n            label=\"Failed Tasks\"\n            title=\"Number of failed tasks\"\n            fetching={isLoading}\n          >\n            {numberOfFailedTasks ?? '-'}\n          </Metrics.Indicator>\n        </Metrics.Section>\n      </Metrics.Wrapper>\n      <ControlPanelWrapper hasInput>\n        <Search placeholder=\"Search by Connect Name, Status or Type\" />\n      </ControlPanelWrapper>\n      <Suspense fallback={<PageLoader />}>\n        <List />\n      </Suspense>\n    </>\n  );\n};\n\nexport default ListPage;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/List/RunningTasksCell.tsx",
    "content": "import React from 'react';\nimport { FullConnectorInfo } from 'generated-sources';\nimport { CellContext } from '@tanstack/react-table';\n\nconst RunningTasksCell: React.FC<CellContext<FullConnectorInfo, unknown>> = ({\n  row,\n}) => {\n  const { tasksCount, failedTasksCount } = row.original;\n\n  if (!tasksCount) {\n    return null;\n  }\n\n  return (\n    <>\n      {tasksCount - (failedTasksCount || 0)} of {tasksCount}\n    </>\n  );\n};\n\nexport default RunningTasksCell;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/List/TopicsCell.tsx",
    "content": "import React from 'react';\nimport { FullConnectorInfo } from 'generated-sources';\nimport { CellContext } from '@tanstack/react-table';\nimport { useNavigate } from 'react-router-dom';\nimport { Tag } from 'components/common/Tag/Tag.styled';\nimport { ClusterNameRoute, clusterTopicPath } from 'lib/paths';\nimport useAppParams from 'lib/hooks/useAppParams';\n\nimport * as S from './List.styled';\n\nconst TopicsCell: React.FC<CellContext<FullConnectorInfo, unknown>> = ({\n  row,\n}) => {\n  const { topics } = row.original;\n  const { clusterName } = useAppParams<ClusterNameRoute>();\n  const navigate = useNavigate();\n\n  const navigateToTopic = (\n    e: React.KeyboardEvent | React.MouseEvent,\n    topic: string\n  ) => {\n    e.preventDefault();\n    e.stopPropagation();\n    navigate(clusterTopicPath(clusterName, topic));\n  };\n\n  return (\n    <S.TagsWrapper>\n      {topics?.map((t) => (\n        <Tag key={t} color=\"green\">\n          <span\n            role=\"link\"\n            onClick={(e) => navigateToTopic(e, t)}\n            onKeyDown={(e) => navigateToTopic(e, t)}\n            tabIndex={0}\n          >\n            {t}\n          </span>\n        </Tag>\n      ))}\n    </S.TagsWrapper>\n  );\n};\n\nexport default TopicsCell;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx",
    "content": "import React from 'react';\nimport { connectors } from 'lib/fixtures/kafkaConnect';\nimport ClusterContext, {\n  ContextProps,\n  initialValue,\n} from 'components/contexts/ClusterContext';\nimport List from 'components/Connect/List/List';\nimport { screen, waitFor } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { clusterConnectConnectorPath, clusterConnectorsPath } from 'lib/paths';\nimport {\n  useConnectors,\n  useDeleteConnector,\n  useUpdateConnectorState,\n} from 'lib/hooks/api/kafkaConnect';\n\nconst mockedUsedNavigate = jest.fn();\nconst mockDelete = jest.fn();\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useNavigate: () => mockedUsedNavigate,\n}));\n\njest.mock('lib/hooks/api/kafkaConnect', () => ({\n  useConnectors: jest.fn(),\n  useDeleteConnector: jest.fn(),\n  useUpdateConnectorState: jest.fn(),\n}));\n\nconst clusterName = 'local';\n\nconst renderComponent = (contextValue: ContextProps = initialValue) =>\n  render(\n    <ClusterContext.Provider value={contextValue}>\n      <WithRoute path={clusterConnectorsPath()}>\n        <List />\n      </WithRoute>\n    </ClusterContext.Provider>,\n    { initialEntries: [clusterConnectorsPath(clusterName)] }\n  );\n\ndescribe('Connectors List', () => {\n  describe('when the connectors are loaded', () => {\n    beforeEach(() => {\n      (useConnectors as jest.Mock).mockImplementation(() => ({\n        data: connectors,\n      }));\n      const restartConnector = jest.fn();\n      (useUpdateConnectorState as jest.Mock).mockImplementation(() => ({\n        mutateAsync: restartConnector,\n      }));\n    });\n\n    it('renders', async () => {\n      renderComponent();\n      expect(screen.getByRole('table')).toBeInTheDocument();\n      expect(screen.getAllByRole('row').length).toEqual(3);\n    });\n\n    it('opens broker when row clicked', async () => {\n      renderComponent();\n      await userEvent.click(\n        screen.getByRole('row', {\n          name: 'hdfs-source-connector first SOURCE FileStreamSource a b c RUNNING 2 of 2',\n        })\n      );\n      await waitFor(() =>\n        expect(mockedUsedNavigate).toBeCalledWith(\n          clusterConnectConnectorPath(\n            clusterName,\n            'first',\n            'hdfs-source-connector'\n          )\n        )\n      );\n    });\n  });\n\n  describe('when table is empty', () => {\n    beforeEach(() => {\n      (useConnectors as jest.Mock).mockImplementation(() => ({\n        data: [],\n      }));\n    });\n\n    it('renders empty table', async () => {\n      renderComponent();\n      expect(screen.getByRole('table')).toBeInTheDocument();\n      expect(\n        screen.getByRole('row', { name: 'No connectors found' })\n      ).toBeInTheDocument();\n    });\n  });\n\n  describe('when remove connector modal is open', () => {\n    beforeEach(() => {\n      (useConnectors as jest.Mock).mockImplementation(() => ({\n        data: connectors,\n      }));\n      (useDeleteConnector as jest.Mock).mockImplementation(() => ({\n        mutateAsync: mockDelete,\n      }));\n    });\n\n    it('calls removeConnector on confirm', async () => {\n      renderComponent();\n      const removeButton = screen.getAllByText('Remove Connector')[0];\n      await waitFor(() => userEvent.click(removeButton));\n\n      const submitButton = screen.getAllByRole('button', {\n        name: 'Confirm',\n      })[0];\n      await userEvent.click(submitButton);\n      expect(mockDelete).toHaveBeenCalledWith();\n    });\n\n    it('closes the modal when cancel button is clicked', async () => {\n      renderComponent();\n      const removeButton = screen.getAllByText('Remove Connector')[0];\n      await waitFor(() => userEvent.click(removeButton));\n\n      const cancelButton = screen.getAllByRole('button', {\n        name: 'Cancel',\n      })[0];\n      await waitFor(() => userEvent.click(cancelButton));\n      expect(cancelButton).not.toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/List/__tests__/ListPage.spec.tsx",
    "content": "import React from 'react';\nimport { connectors } from 'lib/fixtures/kafkaConnect';\nimport ClusterContext, {\n  ContextProps,\n  initialValue,\n} from 'components/contexts/ClusterContext';\nimport ListPage from 'components/Connect/List/ListPage';\nimport { screen, within } from '@testing-library/react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { clusterConnectorsPath } from 'lib/paths';\nimport { useConnectors } from 'lib/hooks/api/kafkaConnect';\n\njest.mock('components/Connect/List/List', () => () => (\n  <div>Connectors List</div>\n));\n\njest.mock('lib/hooks/api/kafkaConnect', () => ({\n  useConnectors: jest.fn(),\n}));\n\njest.mock('components/common/Icons/SpinnerIcon', () => () => 'progressbar');\n\nconst clusterName = 'local';\n\ndescribe('Connectors List Page', () => {\n  beforeEach(() => {\n    (useConnectors as jest.Mock).mockImplementation(() => ({\n      isLoading: false,\n      data: [],\n    }));\n  });\n\n  const renderComponent = async (contextValue: ContextProps = initialValue) =>\n    render(\n      <ClusterContext.Provider value={contextValue}>\n        <WithRoute path={clusterConnectorsPath()}>\n          <ListPage />\n        </WithRoute>\n      </ClusterContext.Provider>,\n      { initialEntries: [clusterConnectorsPath(clusterName)] }\n    );\n\n  describe('Heading', () => {\n    it('renders header without create button for readonly cluster', async () => {\n      await renderComponent({ ...initialValue, isReadOnly: true });\n      expect(\n        screen.getByRole('heading', { name: 'Connectors' })\n      ).toBeInTheDocument();\n      expect(\n        screen.queryByRole('link', { name: 'Create Connector' })\n      ).not.toBeInTheDocument();\n    });\n\n    it('renders header with create button for read/write cluster', async () => {\n      await renderComponent();\n      expect(\n        screen.getByRole('heading', { name: 'Connectors' })\n      ).toBeInTheDocument();\n      expect(\n        screen.getByRole('link', { name: 'Create Connector' })\n      ).toBeInTheDocument();\n    });\n  });\n\n  it('renders search input', async () => {\n    await renderComponent();\n    expect(\n      screen.getByPlaceholderText('Search by Connect Name, Status or Type')\n    ).toBeInTheDocument();\n  });\n\n  it('renders list', async () => {\n    await renderComponent();\n    expect(screen.getByText('Connectors List')).toBeInTheDocument();\n  });\n\n  describe('Metrics', () => {\n    it('renders indicators in loading state', async () => {\n      (useConnectors as jest.Mock).mockImplementation(() => ({\n        isLoading: true,\n        data: connectors,\n      }));\n\n      await renderComponent();\n      const metrics = screen.getByRole('group');\n      expect(metrics).toBeInTheDocument();\n      expect(within(metrics).getAllByText('progressbar').length).toEqual(3);\n    });\n\n    it('renders indicators for empty list of connectors', async () => {\n      await renderComponent();\n      const metrics = screen.getByRole('group');\n      expect(metrics).toBeInTheDocument();\n\n      const connectorsIndicator = within(metrics).getByTitle(\n        'Total number of connectors'\n      );\n      expect(connectorsIndicator).toBeInTheDocument();\n      expect(connectorsIndicator).toHaveTextContent('Connectors -');\n\n      const failedConnectorsIndicator = within(metrics).getByTitle(\n        'Number of failed connectors'\n      );\n      expect(failedConnectorsIndicator).toBeInTheDocument();\n      expect(failedConnectorsIndicator).toHaveTextContent(\n        'Failed Connectors 0'\n      );\n\n      const failedTasksIndicator = within(metrics).getByTitle(\n        'Number of failed tasks'\n      );\n      expect(failedTasksIndicator).toBeInTheDocument();\n      expect(failedTasksIndicator).toHaveTextContent('Failed Tasks 0');\n    });\n\n    it('renders indicators when connectors list is undefined', async () => {\n      (useConnectors as jest.Mock).mockImplementation(() => ({\n        isFetching: false,\n        data: undefined,\n      }));\n\n      await renderComponent();\n      const metrics = screen.getByRole('group');\n      expect(metrics).toBeInTheDocument();\n\n      const connectorsIndicator = within(metrics).getByTitle(\n        'Total number of connectors'\n      );\n      expect(connectorsIndicator).toBeInTheDocument();\n      expect(connectorsIndicator).toHaveTextContent('Connectors -');\n\n      const failedConnectorsIndicator = within(metrics).getByTitle(\n        'Number of failed connectors'\n      );\n      expect(failedConnectorsIndicator).toBeInTheDocument();\n      expect(failedConnectorsIndicator).toHaveTextContent(\n        'Failed Connectors -'\n      );\n\n      const failedTasksIndicator = within(metrics).getByTitle(\n        'Number of failed tasks'\n      );\n      expect(failedTasksIndicator).toBeInTheDocument();\n      expect(failedTasksIndicator).toHaveTextContent('Failed Tasks -');\n    });\n\n    it('renders indicators list of connectors', async () => {\n      (useConnectors as jest.Mock).mockImplementation(() => ({\n        isLoading: false,\n        data: connectors,\n      }));\n\n      await renderComponent();\n\n      const metrics = screen.getByRole('group');\n      expect(metrics).toBeInTheDocument();\n\n      const connectorsIndicator = within(metrics).getByTitle(\n        'Total number of connectors'\n      );\n      expect(connectorsIndicator).toBeInTheDocument();\n      expect(connectorsIndicator).toHaveTextContent(\n        `Connectors ${connectors.length}`\n      );\n\n      const failedConnectorsIndicator = within(metrics).getByTitle(\n        'Number of failed connectors'\n      );\n      expect(failedConnectorsIndicator).toBeInTheDocument();\n      expect(failedConnectorsIndicator).toHaveTextContent(\n        'Failed Connectors 1'\n      );\n\n      const failedTasksIndicator = within(metrics).getByTitle(\n        'Number of failed tasks'\n      );\n      expect(failedTasksIndicator).toBeInTheDocument();\n      expect(failedTasksIndicator).toHaveTextContent('Failed Tasks 1');\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/New/New.styled.ts",
    "content": "import styled, { css } from 'styled-components';\n\nexport const NewConnectFormStyled = styled.form`\n  padding: 0 16px 16px;\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n\n  & > button:last-child {\n    align-self: flex-start;\n  }\n`;\n\nexport const Filed = styled.div<{ $hidden: boolean }>(\n  ({ $hidden }) =>\n    $hidden &&\n    css`\n      display: none;\n    `\n);\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/New/New.tsx",
    "content": "import React from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { Controller, FormProvider, useForm } from 'react-hook-form';\nimport { ErrorMessage } from '@hookform/error-message';\nimport { yupResolver } from '@hookform/resolvers/yup';\nimport {\n  clusterConnectConnectorPath,\n  clusterConnectorsPath,\n  ClusterNameRoute,\n} from 'lib/paths';\nimport yup from 'lib/yupExtended';\nimport Editor from 'components/common/Editor/Editor';\nimport Select from 'components/common/Select/Select';\nimport { FormError } from 'components/common/Input/Input.styled';\nimport Input from 'components/common/Input/Input';\nimport { Button } from 'components/common/Button/Button';\nimport PageHeading from 'components/common/PageHeading/PageHeading';\nimport Heading from 'components/common/heading/Heading.styled';\nimport { useConnects, useCreateConnector } from 'lib/hooks/api/kafkaConnect';\nimport get from 'lodash/get';\nimport { Connect } from 'generated-sources';\n\nimport * as S from './New.styled';\n\nconst validationSchema = yup.object().shape({\n  name: yup.string().required(),\n  config: yup.string().required().isJsonObject(),\n});\n\ninterface FormValues {\n  connectName: Connect['name'];\n  name: string;\n  config: string;\n}\n\nconst New: React.FC = () => {\n  const { clusterName } = useAppParams<ClusterNameRoute>();\n  const navigate = useNavigate();\n\n  const { data: connects = [] } = useConnects(clusterName);\n  const mutation = useCreateConnector(clusterName);\n\n  const methods = useForm<FormValues>({\n    mode: 'all',\n    resolver: yupResolver(validationSchema),\n    defaultValues: {\n      connectName: get(connects, '0.name', ''),\n      name: '',\n      config: '',\n    },\n  });\n  const {\n    handleSubmit,\n    control,\n    formState: { isDirty, isSubmitting, isValid, errors },\n    getValues,\n    setValue,\n  } = methods;\n\n  React.useEffect(() => {\n    if (connects && connects.length > 0 && !getValues().connectName) {\n      setValue('connectName', connects[0].name);\n    }\n  }, [connects, getValues, setValue]);\n\n  const onSubmit = async (values: FormValues) => {\n    try {\n      const connector = await mutation.createResource({\n        connectName: values.connectName,\n        newConnector: {\n          name: values.name,\n          config: JSON.parse(values.config.trim()),\n        },\n      });\n\n      if (connector) {\n        navigate(\n          clusterConnectConnectorPath(\n            clusterName,\n            connector.connect,\n            connector.name\n          )\n        );\n      }\n    } catch (e) {\n      // do nothing\n    }\n  };\n\n  const connectOptions = connects.map(({ name: connectName }) => ({\n    value: connectName,\n    label: connectName,\n  }));\n\n  return (\n    <FormProvider {...methods}>\n      <PageHeading\n        text=\"Create new connector\"\n        backTo={clusterConnectorsPath(clusterName)}\n        backText=\"Connectors\"\n      />\n      <S.NewConnectFormStyled\n        onSubmit={handleSubmit(onSubmit)}\n        aria-label=\"Create connect form\"\n      >\n        <S.Filed $hidden={connects?.length <= 1}>\n          <Heading level={3}>Connect *</Heading>\n          <Controller\n            defaultValue={connectOptions[0]?.value}\n            control={control}\n            name=\"connectName\"\n            render={({ field: { name, onChange } }) => (\n              <Select\n                selectSize=\"M\"\n                name={name}\n                disabled={isSubmitting}\n                onChange={onChange}\n                value={connectOptions[0]?.value}\n                minWidth=\"100%\"\n                options={connectOptions}\n              />\n            )}\n          />\n          <FormError>\n            <ErrorMessage errors={errors} name=\"connectName\" />\n          </FormError>\n        </S.Filed>\n\n        <div>\n          <Heading level={3}>Name</Heading>\n          <Input\n            inputSize=\"M\"\n            placeholder=\"Connector Name\"\n            name=\"name\"\n            autoFocus\n            autoComplete=\"off\"\n            disabled={isSubmitting}\n          />\n          <FormError>\n            <ErrorMessage errors={errors} name=\"name\" />\n          </FormError>\n        </div>\n\n        <div>\n          <Heading level={3}>Config</Heading>\n          <Controller\n            control={control}\n            name=\"config\"\n            render={({ field }) => (\n              <Editor {...field} readOnly={isSubmitting} ref={null} />\n            )}\n          />\n          <FormError>\n            <ErrorMessage errors={errors} name=\"config\" />\n          </FormError>\n        </div>\n        <Button\n          buttonSize=\"M\"\n          buttonType=\"primary\"\n          type=\"submit\"\n          disabled={!isValid || isSubmitting || !isDirty}\n        >\n          Submit\n        </Button>\n      </S.NewConnectFormStyled>\n    </FormProvider>\n  );\n};\n\nexport default New;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx",
    "content": "import React from 'react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport {\n  clusterConnectConnectorPath,\n  clusterConnectorNewPath,\n} from 'lib/paths';\nimport New from 'components/Connect/New/New';\nimport { connects, connector } from 'lib/fixtures/kafkaConnect';\nimport { fireEvent, screen, act } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { ControllerRenderProps } from 'react-hook-form';\nimport { useConnects, useCreateConnector } from 'lib/hooks/api/kafkaConnect';\n\njest.mock(\n  'components/common/Editor/Editor',\n  () => (props: ControllerRenderProps) => {\n    return <textarea {...props} placeholder=\"json\" />;\n  }\n);\n\nconst mockHistoryPush = jest.fn();\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useNavigate: () => mockHistoryPush,\n}));\n\njest.mock('lib/hooks/api/kafkaConnect', () => ({\n  useConnects: jest.fn(),\n  useCreateConnector: jest.fn(),\n}));\n\ndescribe('New', () => {\n  const clusterName = 'my-cluster';\n  const simulateFormSubmit = async () => {\n    await userEvent.type(\n      screen.getByPlaceholderText('Connector Name'),\n      'my-connector'\n    );\n    await userEvent.type(\n      screen.getByPlaceholderText('json'),\n      '{\"class\":\"MyClass\"}'.replace(/[{[]/g, '$&$&')\n    );\n\n    expect(screen.getByPlaceholderText('json')).toHaveValue(\n      '{\"class\":\"MyClass\"}'\n    );\n    await act(() => {\n      fireEvent.submit(screen.getByRole('form'));\n    });\n  };\n\n  const renderComponent = () =>\n    render(\n      <WithRoute path={clusterConnectorNewPath()}>\n        <New />\n      </WithRoute>,\n      { initialEntries: [clusterConnectorNewPath(clusterName)] }\n    );\n\n  beforeEach(() => {\n    (useConnects as jest.Mock).mockImplementation(() => ({\n      data: connects,\n    }));\n  });\n\n  it('calls createConnector on form submit and redirects to the list page on success', async () => {\n    const createConnectorMock = jest.fn(() => {\n      return Promise.resolve(connector);\n    });\n    (useCreateConnector as jest.Mock).mockImplementation(() => ({\n      createResource: createConnectorMock,\n    }));\n    renderComponent();\n    await simulateFormSubmit();\n    expect(createConnectorMock).toHaveBeenCalledTimes(1);\n    expect(mockHistoryPush).toHaveBeenCalledTimes(1);\n    expect(mockHistoryPush).toHaveBeenCalledWith(\n      clusterConnectConnectorPath(clusterName, connects[0].name, connector.name)\n    );\n  });\n\n  it('does not redirect to connector details view on unsuccessful submit', async () => {\n    const createConnectorMock = jest.fn(() => {\n      return Promise.resolve();\n    });\n    (useCreateConnector as jest.Mock).mockImplementation(() => ({\n      createResource: createConnectorMock,\n    }));\n    renderComponent();\n    await simulateFormSubmit();\n    expect(mockHistoryPush).not.toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Connect/__tests__/Connect.spec.tsx",
    "content": "import React from 'react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { screen } from '@testing-library/react';\nimport Connect from 'components/Connect/Connect';\nimport {\n  clusterConnectorsPath,\n  clusterConnectorNewPath,\n  clusterConnectConnectorPath,\n  getNonExactPath,\n  clusterConnectsPath,\n} from 'lib/paths';\n\nconst ConnectCompText = {\n  new: 'New Page',\n  list: 'List Page',\n  details: 'Details Page',\n};\n\njest.mock('components/Connect/New/New', () => () => (\n  <div>{ConnectCompText.new}</div>\n));\njest.mock('components/Connect/List/ListPage', () => () => (\n  <div>{ConnectCompText.list}</div>\n));\njest.mock('components/Connect/Details/DetailsPage', () => () => (\n  <div>{ConnectCompText.details}</div>\n));\n\ndescribe('Connect', () => {\n  const renderComponent = (pathname: string, routePath: string) =>\n    render(\n      <WithRoute path={getNonExactPath(routePath)}>\n        <Connect />\n      </WithRoute>,\n      { initialEntries: [pathname] }\n    );\n\n  it('renders ListPage', () => {\n    renderComponent(\n      clusterConnectorsPath('my-cluster'),\n      clusterConnectorsPath()\n    );\n    expect(screen.getByText(ConnectCompText.list)).toBeInTheDocument();\n  });\n\n  it('renders New Page', () => {\n    renderComponent(\n      clusterConnectorNewPath('my-cluster'),\n      clusterConnectorsPath()\n    );\n    expect(screen.getByText(ConnectCompText.new)).toBeInTheDocument();\n  });\n\n  it('renders Details Page', () => {\n    renderComponent(\n      clusterConnectConnectorPath('my-cluster', 'my-connect', 'my-connector'),\n      clusterConnectsPath()\n    );\n    expect(screen.getByText(ConnectCompText.details)).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx",
    "content": "import React from 'react';\nimport { Route, Routes } from 'react-router-dom';\nimport Details from 'components/ConsumerGroups/Details/Details';\nimport ResetOffsets from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets';\nimport {\n  clusterConsumerGroupResetOffsetsRelativePath,\n  RouteParams,\n} from 'lib/paths';\n\nimport List from './List';\n\nconst ConsumerGroups: React.FC = () => {\n  return (\n    <Routes>\n      <Route index element={<List />} />\n      <Route path={RouteParams.consumerGroupID} element={<Details />} />\n      <Route\n        path={clusterConsumerGroupResetOffsetsRelativePath}\n        element={<ResetOffsets />}\n      />\n    </Routes>\n  );\n};\n\nexport default ConsumerGroups;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx",
    "content": "import React from 'react';\nimport { useNavigate, useSearchParams } from 'react-router-dom';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport {\n  clusterConsumerGroupResetRelativePath,\n  clusterConsumerGroupsPath,\n  ClusterGroupParam,\n} from 'lib/paths';\nimport Search from 'components/common/Search/Search';\nimport ClusterContext from 'components/contexts/ClusterContext';\nimport PageHeading from 'components/common/PageHeading/PageHeading';\nimport * as Metrics from 'components/common/Metrics';\nimport { Tag } from 'components/common/Tag/Tag.styled';\nimport groupBy from 'lodash/groupBy';\nimport { Table } from 'components/common/table/Table/Table.styled';\nimport getTagColor from 'components/common/Tag/getTagColor';\nimport { Dropdown } from 'components/common/Dropdown';\nimport { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';\nimport { Action, ConsumerGroupState, ResourceType } from 'generated-sources';\nimport { ActionDropdownItem } from 'components/common/ActionComponent';\nimport TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';\nimport {\n  useConsumerGroupDetails,\n  useDeleteConsumerGroupMutation,\n} from 'lib/hooks/api/consumers';\nimport Tooltip from 'components/common/Tooltip/Tooltip';\nimport { CONSUMER_GROUP_STATE_TOOLTIPS } from 'lib/constants';\n\nimport ListItem from './ListItem';\n\nconst Details: React.FC = () => {\n  const navigate = useNavigate();\n  const [searchParams] = useSearchParams();\n  const searchValue = searchParams.get('q') || '';\n  const { isReadOnly } = React.useContext(ClusterContext);\n  const routeParams = useAppParams<ClusterGroupParam>();\n  const { clusterName, consumerGroupID } = routeParams;\n\n  const consumerGroup = useConsumerGroupDetails(routeParams);\n  const deleteConsumerGroup = useDeleteConsumerGroupMutation(routeParams);\n\n  const onDelete = async () => {\n    await deleteConsumerGroup.mutateAsync();\n    navigate('../');\n  };\n\n  const onResetOffsets = () => {\n    navigate(clusterConsumerGroupResetRelativePath);\n  };\n\n  const partitionsByTopic = groupBy(consumerGroup.data?.partitions, 'topic');\n  const filteredPartitionsByTopic = Object.keys(partitionsByTopic).filter(\n    (el) => el.includes(searchValue)\n  );\n  const currentPartitionsByTopic = searchValue.length\n    ? filteredPartitionsByTopic\n    : Object.keys(partitionsByTopic);\n\n  const hasAssignedTopics = consumerGroup?.data?.topics !== 0;\n\n  return (\n    <div>\n      <div>\n        <PageHeading\n          text={consumerGroupID}\n          backTo={clusterConsumerGroupsPath(clusterName)}\n          backText=\"Consumers\"\n        >\n          {!isReadOnly && (\n            <Dropdown>\n              <ActionDropdownItem\n                onClick={onResetOffsets}\n                permission={{\n                  resource: ResourceType.CONSUMER,\n                  action: Action.RESET_OFFSETS,\n                  value: consumerGroupID,\n                }}\n                disabled={!hasAssignedTopics}\n              >\n                Reset offset\n              </ActionDropdownItem>\n              <ActionDropdownItem\n                confirm=\"Are you sure you want to delete this consumer group?\"\n                onClick={onDelete}\n                danger\n                permission={{\n                  resource: ResourceType.CONSUMER,\n                  action: Action.DELETE,\n                  value: consumerGroupID,\n                }}\n              >\n                Delete consumer group\n              </ActionDropdownItem>\n            </Dropdown>\n          )}\n        </PageHeading>\n      </div>\n      <Metrics.Wrapper>\n        <Metrics.Section>\n          <Metrics.Indicator label=\"State\">\n            <Tooltip\n              value={\n                <Tag color={getTagColor(consumerGroup.data?.state)}>\n                  {consumerGroup.data?.state}\n                </Tag>\n              }\n              content={\n                CONSUMER_GROUP_STATE_TOOLTIPS[\n                  consumerGroup.data?.state || ConsumerGroupState.UNKNOWN\n                ]\n              }\n              placement=\"bottom-start\"\n            />\n          </Metrics.Indicator>\n          <Metrics.Indicator label=\"Members\">\n            {consumerGroup.data?.members}\n          </Metrics.Indicator>\n          <Metrics.Indicator label=\"Assigned Topics\">\n            {consumerGroup.data?.topics}\n          </Metrics.Indicator>\n          <Metrics.Indicator label=\"Assigned Partitions\">\n            {consumerGroup.data?.partitions?.length}\n          </Metrics.Indicator>\n          <Metrics.Indicator label=\"Coordinator ID\">\n            {consumerGroup.data?.coordinator?.id}\n          </Metrics.Indicator>\n          <Metrics.Indicator label=\"Total lag\">\n            {consumerGroup.data?.consumerLag}\n          </Metrics.Indicator>\n        </Metrics.Section>\n      </Metrics.Wrapper>\n      <ControlPanelWrapper hasInput style={{ margin: '16px 0 20px' }}>\n        <Search placeholder=\"Search by Topic Name\" />\n      </ControlPanelWrapper>\n      <Table isFullwidth>\n        <thead>\n          <tr>\n            <TableHeaderCell title=\"Topic\" />\n            <TableHeaderCell title=\"Consumer Lag\" />\n          </tr>\n        </thead>\n        <tbody>\n          {currentPartitionsByTopic.map((key) => (\n            <ListItem\n              clusterName={clusterName}\n              consumers={partitionsByTopic[key]}\n              name={key}\n              key={key}\n            />\n          ))}\n        </tbody>\n      </Table>\n    </div>\n  );\n};\n\nexport default Details;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ConsumerGroups/Details/ListItem.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const FlexWrapper = styled.div`\n  display: flex;\n  align-items: center;\n  gap: 8px;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ConsumerGroups/Details/ListItem.tsx",
    "content": "import React from 'react';\nimport { ConsumerGroupTopicPartition } from 'generated-sources';\nimport { Link } from 'react-router-dom';\nimport { ClusterName } from 'redux/interfaces/cluster';\nimport { clusterTopicPath } from 'lib/paths';\nimport MessageToggleIcon from 'components/common/Icons/MessageToggleIcon';\nimport IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';\nimport { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled';\n\nimport TopicContents from './TopicContents/TopicContents';\nimport { FlexWrapper } from './ListItem.styled';\n\ninterface Props {\n  clusterName: ClusterName;\n  name: string;\n  consumers: ConsumerGroupTopicPartition[];\n}\n\nconst ListItem: React.FC<Props> = ({ clusterName, name, consumers }) => {\n  const [isOpen, setIsOpen] = React.useState(false);\n\n  const getTotalconsumerLag = () => {\n    let count = 0;\n    consumers.forEach((consumer) => {\n      count += consumer?.consumerLag || 0;\n    });\n    return count;\n  };\n\n  return (\n    <>\n      <tr>\n        <td>\n          <FlexWrapper>\n            <IconButtonWrapper onClick={() => setIsOpen(!isOpen)} aria-hidden>\n              <MessageToggleIcon isOpen={isOpen} />\n            </IconButtonWrapper>\n            <TableKeyLink>\n              <Link to={clusterTopicPath(clusterName, name)}>{name}</Link>\n            </TableKeyLink>\n          </FlexWrapper>\n        </td>\n        <td>{getTotalconsumerLag()}</td>\n      </tr>\n      {isOpen && <TopicContents consumers={consumers} />}\n    </>\n  );\n};\n\nexport default ListItem;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/Form.tsx",
    "content": "import React from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n  ConsumerGroupDetails,\n  ConsumerGroupOffsetsReset,\n  ConsumerGroupOffsetsResetType,\n} from 'generated-sources';\nimport { ClusterGroupParam } from 'lib/paths';\nimport {\n  Controller,\n  FormProvider,\n  useFieldArray,\n  useForm,\n} from 'react-hook-form';\nimport { MultiSelect, Option } from 'react-multi-select-component';\nimport 'react-datepicker/dist/react-datepicker.css';\nimport { ErrorMessage } from '@hookform/error-message';\nimport { InputLabel } from 'components/common/Input/InputLabel.styled';\nimport { Button } from 'components/common/Button/Button';\nimport Input from 'components/common/Input/Input';\nimport { FormError } from 'components/common/Input/Input.styled';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { useResetConsumerGroupOffsetsMutation } from 'lib/hooks/api/consumers';\nimport { FlexFieldset, StyledForm } from 'components/common/Form/Form.styled';\nimport ControlledSelect from 'components/common/Select/ControlledSelect';\n\nimport * as S from './ResetOffsets.styled';\n\ninterface FormProps {\n  defaultValues: ConsumerGroupOffsetsReset;\n  topics: string[];\n  partitions: ConsumerGroupDetails['partitions'];\n}\n\nconst resetTypeOptions = Object.values(ConsumerGroupOffsetsResetType).map(\n  (value) => ({ value, label: value })\n);\n\nconst Form: React.FC<FormProps> = ({ defaultValues, partitions, topics }) => {\n  const navigate = useNavigate();\n  const routerParams = useAppParams<ClusterGroupParam>();\n  const reset = useResetConsumerGroupOffsetsMutation(routerParams);\n  const topicOptions = React.useMemo(\n    () => topics.map((value) => ({ value, label: value })),\n    [topics]\n  );\n  const methods = useForm<ConsumerGroupOffsetsReset>({\n    mode: 'onChange',\n    defaultValues,\n  });\n\n  const {\n    handleSubmit,\n    setValue,\n    watch,\n    control,\n    formState: { errors },\n  } = methods;\n  const { fields } = useFieldArray({\n    control,\n    name: 'partitionsOffsets',\n  });\n\n  const resetTypeValue = watch('resetType');\n  const topicValue = watch('topic');\n  const offsetsValue = watch('partitionsOffsets');\n  const partitionsValue = watch('partitions') || [];\n\n  const partitionOptions =\n    partitions\n      ?.filter((p) => p.topic === topicValue)\n      .map((p) => ({\n        label: `Partition #${p.partition.toString()}`,\n        value: p.partition,\n      })) || [];\n\n  const onSelectedPartitionsChange = (selected: Option[]) => {\n    setValue(\n      'partitions',\n      selected.map(({ value }) => value)\n    );\n\n    setValue(\n      'partitionsOffsets',\n      selected.map(({ value }) => {\n        const currentOffset = offsetsValue?.find(\n          ({ partition }) => partition === value\n        );\n        return { offset: currentOffset?.offset, partition: value };\n      })\n    );\n  };\n\n  React.useEffect(() => {\n    onSelectedPartitionsChange([]);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [topicValue]);\n\n  const onSubmit = async (data: ConsumerGroupOffsetsReset) => {\n    await reset.mutateAsync(data);\n    navigate('../');\n  };\n\n  return (\n    <FormProvider {...methods}>\n      <StyledForm onSubmit={handleSubmit(onSubmit)}>\n        <FlexFieldset>\n          <ControlledSelect\n            name=\"topic\"\n            label=\"Topic\"\n            placeholder=\"Select Topic\"\n            options={topicOptions}\n          />\n          <ControlledSelect\n            name=\"resetType\"\n            label=\"Reset Type\"\n            placeholder=\"Select Reset Type\"\n            options={resetTypeOptions}\n          />\n          <div>\n            <InputLabel>Partitions</InputLabel>\n            <MultiSelect\n              options={partitionOptions}\n              value={partitionsValue.map((p) => ({\n                value: p,\n                label: String(p),\n              }))}\n              onChange={onSelectedPartitionsChange}\n              labelledBy=\"Select partitions\"\n            />\n          </div>\n          {resetTypeValue === ConsumerGroupOffsetsResetType.TIMESTAMP &&\n            partitionsValue.length > 0 && (\n              <div>\n                <InputLabel>Timestamp</InputLabel>\n                <Controller\n                  control={control}\n                  name=\"resetToTimestamp\"\n                  rules={{\n                    required: 'Timestamp is required',\n                  }}\n                  render={({ field: { onChange, onBlur, value, ref } }) => (\n                    <S.DatePickerInput\n                      ref={ref}\n                      selected={new Date(value as number)}\n                      onChange={(e: Date | null) => onChange(e?.getTime())}\n                      onBlur={onBlur}\n                    />\n                  )}\n                />\n                <ErrorMessage\n                  errors={errors}\n                  name=\"resetToTimestamp\"\n                  render={({ message }) => <FormError>{message}</FormError>}\n                />\n              </div>\n            )}\n\n          {resetTypeValue === ConsumerGroupOffsetsResetType.OFFSET &&\n            partitionsValue.length > 0 && (\n              <S.OffsetsWrapper>\n                {fields.map((field, index) => (\n                  <Input\n                    key={field.id}\n                    label={`Partition #${field.partition} Offset`}\n                    type=\"number\"\n                    name={`partitionsOffsets.${index}.offset` as const}\n                    hookFormOptions={{\n                      shouldUnregister: true,\n                      required: 'Offset is required',\n                      min: {\n                        value: 0,\n                        message: 'must be greater than or equal to 0',\n                      },\n                    }}\n                    withError\n                  />\n                ))}\n              </S.OffsetsWrapper>\n            )}\n        </FlexFieldset>\n        <div>\n          <Button\n            buttonSize=\"M\"\n            buttonType=\"primary\"\n            type=\"submit\"\n            disabled={partitionsValue.length === 0}\n          >\n            Reset Offsets\n          </Button>\n        </div>\n      </StyledForm>\n    </FormProvider>\n  );\n};\n\nexport default Form;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.styled.ts",
    "content": "import styled from 'styled-components';\nimport DatePicker from 'react-datepicker';\n\nexport const OffsetsWrapper = styled.div`\n  display: flex;\n  width: 100%;\n  flex-wrap: wrap;\n  gap: 16px;\n`;\n\nexport const DatePickerInput = styled(DatePicker).attrs({\n  showTimeInput: true,\n  timeInputLabel: 'Time:',\n  dateFormat: 'MMMM d, yyyy h:mm aa',\n})`\n  height: 40px;\n  border: 1px ${({ theme }) => theme.select.borderColor.normal} solid;\n  border-radius: 4px;\n  font-size: 14px;\n  width: 270px;\n  padding-left: 12px;\n  background-color: ${({ theme }) => theme.input.backgroundColor.normal};\n  color: ${({ theme }) => theme.input.color.normal};\n  &::placeholder {\n    color: ${({ theme }) => theme.input.color.normal};\n  }\n  &:hover {\n    cursor: pointer;\n  }\n  &:focus {\n    outline: none;\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.tsx",
    "content": "import React from 'react';\nimport { clusterConsumerGroupsPath, ClusterGroupParam } from 'lib/paths';\nimport 'react-datepicker/dist/react-datepicker.css';\nimport PageHeading from 'components/common/PageHeading/PageHeading';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { useConsumerGroupDetails } from 'lib/hooks/api/consumers';\nimport PageLoader from 'components/common/PageLoader/PageLoader';\nimport {\n  ConsumerGroupOffsetsReset,\n  ConsumerGroupOffsetsResetType,\n} from 'generated-sources';\n\nimport Form from './Form';\n\nconst ResetOffsets: React.FC = () => {\n  const routerParams = useAppParams<ClusterGroupParam>();\n\n  const { consumerGroupID } = routerParams;\n  const consumerGroup = useConsumerGroupDetails(routerParams);\n\n  if (consumerGroup.isLoading || !consumerGroup.isSuccess)\n    return <PageLoader />;\n\n  const partitions = consumerGroup.data.partitions || [];\n  const { topic } = partitions[0] || '';\n\n  const uniqTopics = Array.from(\n    new Set(partitions.map((partition) => partition.topic))\n  );\n\n  const defaultValues: ConsumerGroupOffsetsReset = {\n    resetType: ConsumerGroupOffsetsResetType.EARLIEST,\n    topic,\n    partitionsOffsets: [],\n    resetToTimestamp: new Date().getTime(),\n  };\n\n  return (\n    <>\n      <PageHeading\n        text={consumerGroupID}\n        backTo={clusterConsumerGroupsPath(routerParams.clusterName)}\n        backText=\"Consumers\"\n      />\n      <Form\n        defaultValues={defaultValues}\n        topics={uniqTopics}\n        partitions={partitions}\n      />\n    </>\n  );\n};\n\nexport default ResetOffsets;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/TopicContent.styled.ts",
    "content": "import styled, { css } from 'styled-components';\n\nexport const TopicContentWrapper = styled.tr`\n  background-color: ${({ theme }) => theme.default.backgroundColor};\n  & > td {\n    padding: 16px !important;\n    background-color: ${({ theme }) =>\n      theme.consumerTopicContent.td.backgroundColor};\n  }\n`;\n\nexport const ContentBox = styled.div(\n  ({ theme }) => css`\n    background-color: ${theme.default.backgroundColor};\n    padding: 20px;\n    border-radius: 8px;\n  `\n);\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/TopicContents.tsx",
    "content": "import { Table } from 'components/common/table/Table/Table.styled';\nimport TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';\nimport { ConsumerGroupTopicPartition, SortOrder } from 'generated-sources';\nimport React from 'react';\n\nimport { ContentBox, TopicContentWrapper } from './TopicContent.styled';\n\ninterface Props {\n  consumers: ConsumerGroupTopicPartition[];\n}\n\ntype OrderByKey = keyof ConsumerGroupTopicPartition;\ninterface Headers {\n  title: string;\n  orderBy: OrderByKey | undefined;\n}\n\nconst TABLE_HEADERS_MAP: Headers[] = [\n  { title: 'Partition', orderBy: 'partition' },\n  { title: 'Consumer ID', orderBy: 'consumerId' },\n  { title: 'Host', orderBy: 'host' },\n  { title: 'Consumer Lag', orderBy: 'consumerLag' },\n  { title: 'Current Offset', orderBy: 'currentOffset' },\n  { title: 'End offset', orderBy: 'endOffset' },\n];\n\nconst ipV4ToNum = (ip?: string) => {\n  if (typeof ip === 'string' && ip.length !== 0) {\n    const withoutSlash = ip.indexOf('/') !== -1 ? ip.slice(1) : ip;\n    return Number(\n      withoutSlash\n        .split('.')\n        .map((octet) => `000${octet}`.slice(-3))\n        .join('')\n    );\n  }\n  return 0;\n};\n\ntype ComparatorFunction<T> = (\n  valueA: T,\n  valueB: T,\n  order: SortOrder,\n  property?: keyof T\n) => number;\n\nconst numberComparator: ComparatorFunction<ConsumerGroupTopicPartition> = (\n  valueA,\n  valueB,\n  order,\n  property\n) => {\n  if (property !== undefined) {\n    return order === SortOrder.ASC\n      ? Number(valueA[property]) - Number(valueB[property])\n      : Number(valueB[property]) - Number(valueA[property]);\n  }\n  return 0;\n};\n\nconst ipComparator: ComparatorFunction<ConsumerGroupTopicPartition> = (\n  valueA,\n  valueB,\n  order\n) =>\n  order === SortOrder.ASC\n    ? ipV4ToNum(valueA.host) - ipV4ToNum(valueB.host)\n    : ipV4ToNum(valueB.host) - ipV4ToNum(valueA.host);\n\nconst consumerIdComparator: ComparatorFunction<ConsumerGroupTopicPartition> = (\n  valueA,\n  valueB,\n  order\n) => {\n  if (valueA.consumerId && valueB.consumerId) {\n    if (order === SortOrder.ASC) {\n      if (valueA.consumerId?.toLowerCase() > valueB.consumerId?.toLowerCase()) {\n        return 1;\n      }\n    }\n\n    if (order === SortOrder.DESC) {\n      if (valueB.consumerId?.toLowerCase() > valueA.consumerId?.toLowerCase()) {\n        return -1;\n      }\n    }\n  }\n\n  return 0;\n};\n\nconst TopicContents: React.FC<Props> = ({ consumers }) => {\n  const [orderBy, setOrderBy] = React.useState<OrderByKey>('partition');\n  const [sortOrder, setSortOrder] = React.useState<SortOrder>(SortOrder.DESC);\n\n  const handleOrder = React.useCallback((columnName: string | null) => {\n    if (typeof columnName === 'string') {\n      setOrderBy(columnName as OrderByKey);\n      setSortOrder((prevOrder) =>\n        prevOrder === SortOrder.DESC ? SortOrder.ASC : SortOrder.DESC\n      );\n    }\n  }, []);\n\n  const sortedConsumers = React.useMemo(() => {\n    if (orderBy && sortOrder) {\n      const isNumberProperty =\n        orderBy === 'partition' ||\n        orderBy === 'currentOffset' ||\n        orderBy === 'endOffset' ||\n        orderBy === 'consumerLag';\n\n      let comparator: ComparatorFunction<ConsumerGroupTopicPartition>;\n      if (isNumberProperty) {\n        comparator = numberComparator;\n      }\n\n      if (orderBy === 'host') {\n        comparator = ipComparator;\n      }\n\n      if (orderBy === 'consumerId') {\n        comparator = consumerIdComparator;\n      }\n\n      return consumers.sort((a, b) => comparator(a, b, sortOrder, orderBy));\n    }\n    return consumers;\n  }, [orderBy, sortOrder, consumers]);\n\n  return (\n    <TopicContentWrapper>\n      <td colSpan={3}>\n        <ContentBox>\n          <Table isFullwidth>\n            <thead>\n              <tr>\n                {TABLE_HEADERS_MAP.map((header) => (\n                  <TableHeaderCell\n                    key={header.orderBy}\n                    title={header.title}\n                    orderBy={orderBy}\n                    sortOrder={sortOrder}\n                    orderValue={header.orderBy}\n                    handleOrderBy={handleOrder}\n                  />\n                ))}\n              </tr>\n            </thead>\n            <tbody>\n              {sortedConsumers.map((consumer) => (\n                <tr key={consumer.partition}>\n                  <td>{consumer.partition}</td>\n                  <td>{consumer.consumerId}</td>\n                  <td>{consumer.host}</td>\n                  <td>{consumer.consumerLag}</td>\n                  <td>{consumer.currentOffset}</td>\n                  <td>{consumer.endOffset}</td>\n                </tr>\n              ))}\n            </tbody>\n          </Table>\n        </ContentBox>\n      </td>\n    </TopicContentWrapper>\n  );\n};\n\nexport default TopicContents;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/__test__/TopicContents.spec.tsx",
    "content": "import React from 'react';\nimport { clusterConsumerGroupDetailsPath } from 'lib/paths';\nimport { screen } from '@testing-library/react';\nimport TopicContents from 'components/ConsumerGroups/Details/TopicContents/TopicContents';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { ConsumerGroupTopicPartition } from 'generated-sources';\nimport { consumerGroupPayload } from 'lib/fixtures/consumerGroups';\n\nconst clusterName = 'cluster1';\n\nconst renderComponent = (consumers: ConsumerGroupTopicPartition[] = []) =>\n  render(\n    <WithRoute path={clusterConsumerGroupDetailsPath()}>\n      <table>\n        <tbody>\n          <TopicContents consumers={consumers} />\n        </tbody>\n      </table>\n    </WithRoute>,\n    {\n      initialEntries: [\n        clusterConsumerGroupDetailsPath(\n          clusterName,\n          consumerGroupPayload.groupId\n        ),\n      ],\n    }\n  );\n\ndescribe('TopicContent', () => {\n  it('renders empty table', () => {\n    renderComponent();\n    const table = screen.getAllByRole('table')[1];\n    expect(table.getElementsByTagName('td').length).toBe(0);\n  });\n\n  it('renders table with content', () => {\n    renderComponent(consumerGroupPayload.partitions);\n    const table = screen.getAllByRole('table')[1];\n    expect(table.getElementsByTagName('td').length).toBe(\n      consumerGroupPayload.partitions.length * 6\n    );\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ConsumerGroups/List.tsx",
    "content": "import React from 'react';\nimport PageHeading from 'components/common/PageHeading/PageHeading';\nimport Search from 'components/common/Search/Search';\nimport { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';\nimport {\n  ConsumerGroupDetails,\n  ConsumerGroupOrdering,\n  ConsumerGroupState,\n  SortOrder,\n} from 'generated-sources';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { clusterConsumerGroupDetailsPath, ClusterNameRoute } from 'lib/paths';\nimport { ColumnDef } from '@tanstack/react-table';\nimport Table, { LinkCell, TagCell } from 'components/common/NewTable';\nimport { useNavigate, useSearchParams } from 'react-router-dom';\nimport { CONSUMER_GROUP_STATE_TOOLTIPS, PER_PAGE } from 'lib/constants';\nimport { useConsumerGroups } from 'lib/hooks/api/consumers';\nimport Tooltip from 'components/common/Tooltip/Tooltip';\n\nconst List = () => {\n  const { clusterName } = useAppParams<ClusterNameRoute>();\n  const [searchParams] = useSearchParams();\n  const navigate = useNavigate();\n\n  const consumerGroups = useConsumerGroups({\n    clusterName,\n    orderBy: (searchParams.get('sortBy') as ConsumerGroupOrdering) || undefined,\n    sortOrder:\n      (searchParams.get('sortDirection')?.toUpperCase() as SortOrder) ||\n      undefined,\n    page: Number(searchParams.get('page') || 1),\n    perPage: Number(searchParams.get('perPage') || PER_PAGE),\n    search: searchParams.get('q') || '',\n  });\n\n  const columns = React.useMemo<ColumnDef<ConsumerGroupDetails>[]>(\n    () => [\n      {\n        id: ConsumerGroupOrdering.NAME,\n        header: 'Group ID',\n        accessorKey: 'groupId',\n        // eslint-disable-next-line react/no-unstable-nested-components\n        cell: ({ getValue }) => (\n          <LinkCell\n            value={`${getValue<string | number>()}`}\n            to={encodeURIComponent(`${getValue<string | number>()}`)}\n          />\n        ),\n      },\n      {\n        id: ConsumerGroupOrdering.MEMBERS,\n        header: 'Num Of Members',\n        accessorKey: 'members',\n      },\n      {\n        id: ConsumerGroupOrdering.TOPIC_NUM,\n        header: 'Num Of Topics',\n        accessorKey: 'topics',\n      },\n      {\n        id: ConsumerGroupOrdering.MESSAGES_BEHIND,\n        header: 'Consumer Lag',\n        accessorKey: 'consumerLag',\n        cell: (args) => {\n          return args.getValue() || 'N/A';\n        },\n      },\n      {\n        header: 'Coordinator',\n        accessorKey: 'coordinator.id',\n        enableSorting: false,\n      },\n      {\n        id: ConsumerGroupOrdering.STATE,\n        header: 'State',\n        accessorKey: 'state',\n        // eslint-disable-next-line react/no-unstable-nested-components\n        cell: (args) => {\n          const value = args.getValue() as ConsumerGroupState;\n          return (\n            <Tooltip\n              value={<TagCell {...args} />}\n              content={CONSUMER_GROUP_STATE_TOOLTIPS[value]}\n              placement=\"bottom-end\"\n            />\n          );\n        },\n      },\n    ],\n    []\n  );\n\n  return (\n    <>\n      <PageHeading text=\"Consumers\" />\n      <ControlPanelWrapper hasInput>\n        <Search placeholder=\"Search by Consumer Group ID\" />\n      </ControlPanelWrapper>\n      <Table\n        columns={columns}\n        pageCount={consumerGroups.data?.pageCount || 0}\n        data={consumerGroups.data?.consumerGroups || []}\n        emptyMessage={\n          consumerGroups.isSuccess\n            ? 'No active consumer groups found'\n            : 'Loading...'\n        }\n        serverSideProcessing\n        enableSorting\n        onRowClick={({ original }) =>\n          navigate(\n            clusterConsumerGroupDetailsPath(clusterName, original.groupId)\n          )\n        }\n        disabled={consumerGroups.isFetching}\n      />\n    </>\n  );\n};\n\nexport default List;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ConsumerGroups/__test__/ConsumerGroups.spec.tsx",
    "content": "import React from 'react';\nimport {\n  clusterConsumerGroupDetailsPath,\n  clusterConsumerGroupResetOffsetsPath,\n  clusterConsumerGroupsPath,\n  getNonExactPath,\n} from 'lib/paths';\nimport { screen } from '@testing-library/react';\nimport ConsumerGroups from 'components/ConsumerGroups/ConsumerGroups';\nimport { render, WithRoute } from 'lib/testHelpers';\n\nconst clusterName = 'cluster1';\n\njest.mock('components/ConsumerGroups/List', () => () => <div>ListPage</div>);\njest.mock('components/ConsumerGroups/Details/Details', () => () => (\n  <div>DetailsMock</div>\n));\njest.mock(\n  'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets',\n  () => () => <div>ResetOffsetsMock</div>\n);\n\nconst renderComponent = (path?: string) =>\n  render(\n    <WithRoute path={getNonExactPath(clusterConsumerGroupsPath())}>\n      <ConsumerGroups />\n    </WithRoute>,\n    {\n      initialEntries: [path || clusterConsumerGroupsPath(clusterName)],\n    }\n  );\n\ndescribe('ConsumerGroups', () => {\n  it('renders ListContainer', async () => {\n    renderComponent();\n    expect(screen.getByText('ListPage')).toBeInTheDocument();\n  });\n  it('renders ResetOffsets', async () => {\n    renderComponent(\n      clusterConsumerGroupResetOffsetsPath(clusterName, 'groupId1')\n    );\n    expect(screen.getByText('ResetOffsetsMock')).toBeInTheDocument();\n  });\n  it('renders Details', async () => {\n    renderComponent(clusterConsumerGroupDetailsPath(clusterName, 'groupId1'));\n    expect(screen.getByText('DetailsMock')).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Dashboard/ClusterName.tsx",
    "content": "import React from 'react';\nimport { CellContext } from '@tanstack/react-table';\nimport { Tag } from 'components/common/Tag/Tag.styled';\nimport { Cluster } from 'generated-sources';\n\ntype ClusterNameProps = CellContext<Cluster, unknown>;\n\nconst ClusterName: React.FC<ClusterNameProps> = ({ row }) => {\n  const { readOnly, name } = row.original;\n  return (\n    <>\n      {readOnly && <Tag color=\"blue\">readonly</Tag>}\n      {name}\n    </>\n  );\n};\n\nexport default ClusterName;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Dashboard/ClusterTableActionsCell.tsx",
    "content": "import React, { useMemo } from 'react';\nimport { Cluster, ResourceType } from 'generated-sources';\nimport { CellContext } from '@tanstack/react-table';\nimport { clusterConfigPath } from 'lib/paths';\nimport { useGetUserInfo } from 'lib/hooks/api/roles';\nimport { ActionCanButton } from 'components/common/ActionComponent';\n\ntype Props = CellContext<Cluster, unknown>;\n\nconst ClusterTableActionsCell: React.FC<Props> = ({ row }) => {\n  const { name } = row.original;\n  const { data } = useGetUserInfo();\n\n  const hasPermissions = useMemo(() => {\n    if (!data?.rbacEnabled) return true;\n    return !!data?.userInfo?.permissions.some(\n      (permission) => permission.resource === ResourceType.APPLICATIONCONFIG\n    );\n  }, [data]);\n\n  return (\n    <ActionCanButton\n      buttonType=\"secondary\"\n      buttonSize=\"S\"\n      to={clusterConfigPath(name)}\n      canDoAction={hasPermissions}\n    >\n      Configure\n    </ActionCanButton>\n  );\n};\n\nexport default ClusterTableActionsCell;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Dashboard/Dashboard.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const Toolbar = styled.div`\n  padding: 8px 16px;\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  color: ${({ theme }) => theme.default.color.normal};\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx",
    "content": "import React, { useMemo } from 'react';\nimport PageHeading from 'components/common/PageHeading/PageHeading';\nimport * as Metrics from 'components/common/Metrics';\nimport { Tag } from 'components/common/Tag/Tag.styled';\nimport Switch from 'components/common/Switch/Switch';\nimport { useClusters } from 'lib/hooks/api/clusters';\nimport { Cluster, ResourceType, ServerStatus } from 'generated-sources';\nimport { ColumnDef } from '@tanstack/react-table';\nimport Table, { SizeCell } from 'components/common/NewTable';\nimport useBoolean from 'lib/hooks/useBoolean';\nimport { clusterNewConfigPath } from 'lib/paths';\nimport { GlobalSettingsContext } from 'components/contexts/GlobalSettingsContext';\nimport { ActionCanButton } from 'components/common/ActionComponent';\nimport { useGetUserInfo } from 'lib/hooks/api/roles';\n\nimport * as S from './Dashboard.styled';\nimport ClusterName from './ClusterName';\nimport ClusterTableActionsCell from './ClusterTableActionsCell';\n\nconst Dashboard: React.FC = () => {\n  const { data } = useGetUserInfo();\n  const clusters = useClusters();\n  const { value: showOfflineOnly, toggle } = useBoolean(false);\n  const appInfo = React.useContext(GlobalSettingsContext);\n\n  const config = React.useMemo(() => {\n    const clusterList = clusters.data || [];\n    const offlineClusters = clusterList.filter(\n      ({ status }) => status === ServerStatus.OFFLINE\n    );\n    return {\n      list: showOfflineOnly ? offlineClusters : clusterList,\n      online: clusterList.length - offlineClusters.length,\n      offline: offlineClusters.length,\n    };\n  }, [clusters, showOfflineOnly]);\n\n  const columns = React.useMemo<ColumnDef<Cluster>[]>(() => {\n    const initialColumns: ColumnDef<Cluster>[] = [\n      { header: 'Cluster name', accessorKey: 'name', cell: ClusterName },\n      { header: 'Version', accessorKey: 'version' },\n      { header: 'Brokers count', accessorKey: 'brokerCount' },\n      { header: 'Partitions', accessorKey: 'onlinePartitionCount' },\n      { header: 'Topics', accessorKey: 'topicCount' },\n      { header: 'Production', accessorKey: 'bytesInPerSec', cell: SizeCell },\n      { header: 'Consumption', accessorKey: 'bytesOutPerSec', cell: SizeCell },\n    ];\n\n    if (appInfo.hasDynamicConfig) {\n      initialColumns.push({\n        header: '',\n        id: 'actions',\n        cell: ClusterTableActionsCell,\n      });\n    }\n\n    return initialColumns;\n  }, []);\n\n  const hasPermissions = useMemo(() => {\n    if (!data?.rbacEnabled) return true;\n    return !!data?.userInfo?.permissions.some(\n      (permission) => permission.resource === ResourceType.APPLICATIONCONFIG\n    );\n  }, [data]);\n  return (\n    <>\n      <PageHeading text=\"Dashboard\" />\n      <Metrics.Wrapper>\n        <Metrics.Section>\n          <Metrics.Indicator label={<Tag color=\"green\">Online</Tag>}>\n            <span>{config.online || 0}</span>{' '}\n            <Metrics.LightText>clusters</Metrics.LightText>\n          </Metrics.Indicator>\n          <Metrics.Indicator label={<Tag color=\"gray\">Offline</Tag>}>\n            <span>{config.offline || 0}</span>{' '}\n            <Metrics.LightText>clusters</Metrics.LightText>\n          </Metrics.Indicator>\n        </Metrics.Section>\n      </Metrics.Wrapper>\n      <S.Toolbar>\n        <div>\n          <Switch\n            name=\"switchRoundedDefault\"\n            checked={showOfflineOnly}\n            onChange={toggle}\n          />\n          <label>Only offline clusters</label>\n        </div>\n        {appInfo.hasDynamicConfig && (\n          <ActionCanButton\n            buttonType=\"primary\"\n            buttonSize=\"M\"\n            to={clusterNewConfigPath}\n            canDoAction={hasPermissions}\n          >\n            Configure new cluster\n          </ActionCanButton>\n        )}\n      </S.Toolbar>\n      <Table\n        columns={columns}\n        data={config?.list}\n        enableSorting\n        emptyMessage={clusters.isFetched ? 'No clusters found' : 'Loading...'}\n      />\n    </>\n  );\n};\n\nexport default Dashboard;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ErrorPage/ErrorPage.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const Wrapper = styled.div`\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  flex-direction: column;\n  gap: 20px;\n  margin-top: 100px;\n`;\n\nexport const Status = styled.div`\n  font-size: 100px;\n  color: ${({ theme }) => theme.default.color.normal};\n  line-height: initial;\n`;\n\nexport const Text = styled.div`\n  font-size: 20px;\n  color: ${({ theme }) => theme.default.color.normal};\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ErrorPage/ErrorPage.tsx",
    "content": "import React from 'react';\nimport { Button } from 'components/common/Button/Button';\n\nimport * as S from './ErrorPage.styled';\n\ninterface Props {\n  status?: number;\n  text?: string;\n  btnText?: string;\n}\n\nconst ErrorPage: React.FC<Props> = ({\n  status = 404,\n  text = 'Page is not found',\n  btnText = 'Go Back to Dashboard',\n}) => {\n  return (\n    <S.Wrapper>\n      <S.Status>{status}</S.Status>\n      <S.Text>{text}</S.Text>\n      <Button buttonType=\"primary\" buttonSize=\"M\" to=\"/\">\n        {btnText}\n      </Button>\n    </S.Wrapper>\n  );\n};\n\nexport default ErrorPage;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/ErrorPage/__tests__/ErrorPage.spec.tsx",
    "content": "import React from 'react';\nimport { screen } from '@testing-library/react';\nimport { render } from 'lib/testHelpers';\nimport ErrorPage from 'components/ErrorPage/ErrorPage';\n\ndescribe('ErrorPage', () => {\n  it('should check Error Page rendering with default text', () => {\n    render(<ErrorPage />);\n    expect(screen.getByText('404')).toBeInTheDocument();\n    expect(screen.getByText('Page is not found')).toBeInTheDocument();\n    expect(screen.getByText('Go Back to Dashboard')).toBeInTheDocument();\n  });\n  it('should check Error Page rendering with custom text', () => {\n    const props = {\n      status: 403,\n      text: 'access is denied',\n      btnText: 'Go back',\n    };\n    render(<ErrorPage {...props} />);\n    expect(screen.getByText(props.status)).toBeInTheDocument();\n    expect(screen.getByText(props.text)).toBeInTheDocument();\n    expect(screen.getByText(props.btnText)).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx",
    "content": "import React from 'react';\nimport Query from 'components/KsqlDb/Query/Query';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport * as Metrics from 'components/common/Metrics';\nimport {\n  clusterKsqlDbQueryRelativePath,\n  clusterKsqlDbStreamsPath,\n  clusterKsqlDbStreamsRelativePath,\n  clusterKsqlDbTablesPath,\n  clusterKsqlDbTablesRelativePath,\n  ClusterNameRoute,\n} from 'lib/paths';\nimport PageHeading from 'components/common/PageHeading/PageHeading';\nimport { ActionButton } from 'components/common/ActionComponent';\nimport Navbar from 'components/common/Navigation/Navbar.styled';\nimport { Navigate, NavLink, Route, Routes } from 'react-router-dom';\nimport { Action, ResourceType } from 'generated-sources';\nimport { useKsqlkDb } from 'lib/hooks/api/ksqlDb';\nimport 'ace-builds/src-noconflict/ace';\n\nimport TableView from './TableView';\n\nconst KsqlDb: React.FC = () => {\n  const { clusterName } = useAppParams<ClusterNameRoute>();\n\n  const [tables, streams] = useKsqlkDb(clusterName);\n\n  const isFetching = tables.isFetching || streams.isFetching;\n\n  return (\n    <>\n      <PageHeading text=\"KSQL DB\">\n        <ActionButton\n          to={clusterKsqlDbQueryRelativePath}\n          buttonType=\"primary\"\n          buttonSize=\"M\"\n          permission={{\n            resource: ResourceType.KSQL,\n            action: Action.EXECUTE,\n          }}\n        >\n          Execute KSQL Request\n        </ActionButton>\n      </PageHeading>\n      <Metrics.Wrapper>\n        <Metrics.Section>\n          <Metrics.Indicator\n            label=\"Tables\"\n            title=\"Tables\"\n            fetching={isFetching}\n          >\n            {tables.isSuccess ? tables.data.length : '-'}\n          </Metrics.Indicator>\n          <Metrics.Indicator\n            label=\"Streams\"\n            title=\"Streams\"\n            fetching={isFetching}\n          >\n            {streams.isSuccess ? streams.data.length : '-'}\n          </Metrics.Indicator>\n        </Metrics.Section>\n      </Metrics.Wrapper>\n      <div>\n        <Navbar role=\"navigation\">\n          <NavLink\n            to={clusterKsqlDbTablesPath(clusterName)}\n            className={({ isActive }) => (isActive ? 'is-active' : '')}\n            end\n          >\n            Tables\n          </NavLink>\n          <NavLink\n            to={clusterKsqlDbStreamsPath(clusterName)}\n            className={({ isActive }) => (isActive ? 'is-active' : '')}\n            end\n          >\n            Streams\n          </NavLink>\n        </Navbar>\n        <Routes>\n          <Route\n            index\n            element={<Navigate to={clusterKsqlDbTablesRelativePath} />}\n          />\n          <Route\n            path={clusterKsqlDbTablesRelativePath}\n            element={\n              <TableView\n                fetching={tables.isFetching}\n                rows={tables.data || []}\n              />\n            }\n          />\n          <Route\n            path={clusterKsqlDbStreamsRelativePath}\n            element={\n              <TableView\n                fetching={streams.isFetching}\n                rows={streams.data || []}\n              />\n            }\n          />\n          <Route path={clusterKsqlDbQueryRelativePath} element={<Query />} />\n        </Routes>\n      </div>\n    </>\n  );\n};\n\nexport default KsqlDb;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/KsqlDb/Query/Query.tsx",
    "content": "import React from 'react';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport TableRenderer from 'components/KsqlDb/Query/renderer/TableRenderer/TableRenderer';\nimport { ClusterNameRoute } from 'lib/paths';\nimport {\n  useExecuteKsqlkDbQueryMutation,\n  useKsqlkDbSSE,\n} from 'lib/hooks/api/ksqlDb';\n\nimport type { FormValues } from './QueryForm/QueryForm';\nimport QueryForm from './QueryForm/QueryForm';\n\nconst Query = () => {\n  const { clusterName } = useAppParams<ClusterNameRoute>();\n  const executeQuery = useExecuteKsqlkDbQueryMutation();\n  const [pipeId, setPipeId] = React.useState<string | false>(false);\n\n  const sse = useKsqlkDbSSE({ clusterName, pipeId });\n\n  const isFetching = executeQuery.isLoading || sse.isFetching;\n\n  const submitHandler = async (values: FormValues) => {\n    const filtered = values.streamsProperties.filter(({ key }) => key != null);\n    const streamsProperties = filtered.reduce<Record<string, string>>(\n      (acc, current) => ({ ...acc, [current.key]: current.value }),\n      {}\n    );\n    await executeQuery.mutateAsync(\n      {\n        clusterName,\n        ksqlCommandV2: {\n          ...values,\n          streamsProperties:\n            values.streamsProperties[0].key !== ''\n              ? JSON.parse(JSON.stringify(streamsProperties))\n              : undefined,\n        },\n      },\n      { onSuccess: (data) => setPipeId(data.pipeId) }\n    );\n  };\n\n  return (\n    <>\n      <QueryForm\n        fetching={isFetching}\n        hasResults={!!sse.data && !!pipeId}\n        resetResults={() => setPipeId(false)}\n        submitHandler={submitHandler}\n      />\n      {pipeId && !!sse.data && <TableRenderer table={sse.data} />}\n    </>\n  );\n};\n\nexport default Query;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/QueryForm.styled.ts",
    "content": "import styled, { css } from 'styled-components';\nimport BaseSQLEditor from 'components/common/SQLEditor/SQLEditor';\n\nexport const QueryWrapper = styled.div`\n  padding: 16px;\n`;\n\nexport const KSQLInputsWrapper = styled.div`\n  display: flex;\n  gap: 24px;\n  padding-bottom: 16px;\n\n  @media screen and (max-width: 769px) {\n    flex-direction: column;\n  }\n`;\n\nexport const KSQLInputHeader = styled.div`\n  display: flex;\n  justify-content: space-between;\n  color: ${({ theme }) => theme.default.color.normal};\n`;\n\nexport const InputsContainer = styled.div`\n  display: grid;\n  grid-template-columns: 1fr 1fr 30px;\n  align-items: center;\n  gap: 10px;\n`;\n\nexport const Fieldset = styled.fieldset`\n  display: flex;\n  flex: 1;\n  flex-direction: column;\n  gap: 8px;\n  color: ${({ theme }) => theme.default.color.normal};\n`;\n\nexport const ButtonsContainer = styled.div`\n  display: flex;\n  gap: 8px;\n`;\n\nexport const SQLEditor = styled(BaseSQLEditor)(\n  ({ readOnly, theme }) =>\n    css`\n      background: ${readOnly && theme.ksqlDb.query.editor.readonly.background};\n      .ace-cursor {\n        ${readOnly && `background: ${theme.default.transparentColor} `}\n      }\n\n      .ace_content {\n        background-color: ${theme.default.backgroundColor};\n        color: ${theme.default.color.normal};\n      }\n      .ace_line {\n        background-color: ${theme.ksqlDb.query.editor.activeLine\n          .backgroundColor};\n      }\n      .ace_gutter-cell {\n        background-color: ${theme.ksqlDb.query.editor.cell.backgroundColor};\n      }\n      .ace_gutter-layer {\n        background-color: ${theme.ksqlDb.query.editor.layer.backgroundColor};\n        color: ${theme.default.color.normal};\n      }\n      .ace_cursor {\n        color: ${theme.ksqlDb.query.editor.cursor};\n      }\n\n      .ace_print-margin {\n        display: none;\n      }\n    `\n);\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/QueryForm.tsx",
    "content": "import React from 'react';\nimport { FormError } from 'components/common/Input/Input.styled';\nimport { ErrorMessage } from '@hookform/error-message';\nimport {\n  useForm,\n  Controller,\n  useFieldArray,\n  FormProvider,\n} from 'react-hook-form';\nimport { Button } from 'components/common/Button/Button';\nimport IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';\nimport CloseCircleIcon from 'components/common/Icons/CloseCircleIcon';\nimport { yupResolver } from '@hookform/resolvers/yup';\nimport yup from 'lib/yupExtended';\nimport PlusIcon from 'components/common/Icons/PlusIcon';\nimport ReactAce from 'react-ace';\nimport Input from 'components/common/Input/Input';\n\nimport * as S from './QueryForm.styled';\n\ninterface QueryFormProps {\n  fetching: boolean;\n  hasResults: boolean;\n  resetResults: () => void;\n  submitHandler: (values: FormValues) => void;\n}\ntype StreamsPropertiesType = {\n  key: string;\n  value: string;\n};\nexport type FormValues = {\n  ksql: string;\n  streamsProperties: StreamsPropertiesType[];\n};\n\nconst streamsPropertiesSchema = yup.object().shape({\n  key: yup.string().trim(),\n  value: yup.string().trim(),\n});\nconst validationSchema = yup.object({\n  ksql: yup.string().trim().required(),\n  streamsProperties: yup.array().of(streamsPropertiesSchema),\n});\n\nconst QueryForm: React.FC<QueryFormProps> = ({\n  fetching,\n  hasResults,\n  submitHandler,\n  resetResults,\n}) => {\n  const methods = useForm<FormValues>({\n    mode: 'onTouched',\n    resolver: yupResolver(validationSchema),\n    defaultValues: {\n      ksql: '',\n      streamsProperties: [{ key: '', value: '' }],\n    },\n  });\n\n  const {\n    handleSubmit,\n    setValue,\n    control,\n    watch,\n    formState: { errors, isDirty },\n  } = methods;\n\n  const { fields, append, remove, update } = useFieldArray<\n    FormValues,\n    'streamsProperties'\n  >({\n    control,\n    name: 'streamsProperties',\n  });\n\n  const watchStreamProps = watch('streamsProperties');\n\n  const appendProperty = () => {\n    append({ key: '', value: '' });\n  };\n  const removeProperty = (index: number) => () => {\n    if (fields.length === 1) {\n      update(index, { key: '', value: '' });\n      return;\n    }\n\n    remove(index);\n  };\n\n  const isAppendDisabled =\n    fetching || !!watchStreamProps.find((field) => !field.key);\n\n  const inputRef = React.useRef<ReactAce>(null);\n\n  const handleFocus = () => {\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const textInput = inputRef?.current?.editor?.textInput as any;\n\n    if (textInput) {\n      textInput.focus();\n    }\n  };\n\n  const handleClear = () => {\n    handleFocus();\n    resetResults();\n  };\n\n  return (\n    <FormProvider {...methods}>\n      <S.QueryWrapper>\n        <form onSubmit={handleSubmit(submitHandler)}>\n          <S.KSQLInputsWrapper>\n            <S.Fieldset>\n              <S.KSQLInputHeader>\n                <label id=\"ksqlLabel\">KSQL</label>\n                <Button\n                  onClick={() => setValue('ksql', '')}\n                  buttonType=\"primary\"\n                  buttonSize=\"S\"\n                  isInverted\n                >\n                  Clear\n                </Button>\n              </S.KSQLInputHeader>\n              <Controller\n                control={control}\n                name=\"ksql\"\n                render={({ field }) => (\n                  <S.SQLEditor\n                    {...field}\n                    commands={[\n                      {\n                        // commands is array of key bindings.\n                        // name for the key binding.\n                        name: 'commandName',\n                        // key combination used for the command.\n                        bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter' },\n                        // function to execute when keys are pressed.\n                        exec: () => {\n                          handleSubmit(submitHandler)();\n                        },\n                      },\n                    ]}\n                    readOnly={fetching}\n                    ref={inputRef}\n                  />\n                )}\n              />\n              <FormError>\n                <ErrorMessage errors={errors} name=\"ksql\" />\n              </FormError>\n            </S.Fieldset>\n\n            <S.Fieldset>\n              Stream properties:\n              {fields.map((field, index) => (\n                <S.InputsContainer key={field.id}>\n                  <Input\n                    name={`streamsProperties.${index}.key`}\n                    placeholder=\"Key\"\n                    type=\"text\"\n                    autoComplete=\"off\"\n                    withError\n                  />\n                  <Input\n                    name={`streamsProperties.${index}.value`}\n                    placeholder=\"Value\"\n                    type=\"text\"\n                    autoComplete=\"off\"\n                    withError\n                  />\n                  <IconButtonWrapper\n                    aria-label=\"deleteProperty\"\n                    onClick={removeProperty(index)}\n                  >\n                    <CloseCircleIcon aria-hidden />\n                  </IconButtonWrapper>\n                </S.InputsContainer>\n              ))}\n              <Button\n                type=\"button\"\n                buttonSize=\"M\"\n                buttonType=\"secondary\"\n                disabled={isAppendDisabled}\n                onClick={appendProperty}\n              >\n                <PlusIcon />\n                Add Stream Property\n              </Button>\n            </S.Fieldset>\n          </S.KSQLInputsWrapper>\n          <S.ButtonsContainer>\n            <Button\n              buttonType=\"secondary\"\n              buttonSize=\"M\"\n              disabled={fetching || !isDirty || !hasResults}\n              onClick={handleClear}\n            >\n              Clear results\n            </Button>\n            <Button\n              buttonType=\"primary\"\n              buttonSize=\"M\"\n              type=\"submit\"\n              disabled={fetching}\n              onClick={handleFocus}\n            >\n              Execute\n            </Button>\n          </S.ButtonsContainer>\n        </form>\n      </S.QueryWrapper>\n    </FormProvider>\n  );\n};\n\nexport default QueryForm;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/KsqlDb/Query/renderer/TableRenderer/TableRenderer.styled.tsx",
    "content": "import styled from 'styled-components';\nimport { Table } from 'components/common/table/Table/Table.styled';\n\nexport const Wrapper = styled.div`\n  display: block;\n  overflow-y: scroll;\n`;\n\nexport const ScrollableTable = styled(Table)`\n  overflow-y: scroll;\n  width: 100%;\n\n  td {\n    vertical-align: top;\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/KsqlDb/Query/renderer/TableRenderer/TableRenderer.tsx",
    "content": "import React from 'react';\nimport { KsqlTableResponse } from 'generated-sources';\nimport TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';\nimport { nanoid } from '@reduxjs/toolkit';\nimport { TableTitle } from 'components/common/table/TableTitle/TableTitle.styled';\n\nimport * as S from './TableRenderer.styled';\n\ninterface TableRendererProps {\n  table: KsqlTableResponse;\n}\n\nfunction hasJsonStructure(str: string | Record<string, unknown>): boolean {\n  if (typeof str === 'object') {\n    return true;\n  }\n\n  if (typeof str === 'string') {\n    try {\n      const result = JSON.parse(str);\n      const type = Object.prototype.toString.call(result);\n      return type === '[object Object]' || type === '[object Array]';\n    } catch (err) {\n      return false;\n    }\n  }\n\n  return false;\n}\n\nconst TableRenderer: React.FC<TableRendererProps> = ({ table }) => {\n  const rows = React.useMemo(() => {\n    return (table.values || []).map((row) => {\n      return {\n        id: nanoid(),\n        cells: row.map((cell) => {\n          return {\n            id: nanoid(),\n            value: hasJsonStructure(cell)\n              ? JSON.stringify(cell, null, 2)\n              : cell,\n          };\n        }),\n      };\n    });\n  }, [table.values]);\n\n  const ths = table.columnNames || [];\n\n  return (\n    <S.Wrapper>\n      <TableTitle>{table.header}</TableTitle>\n      <S.ScrollableTable>\n        <thead>\n          <tr>\n            {ths.map((th) => (\n              <TableHeaderCell title={th} key={th} />\n            ))}\n          </tr>\n        </thead>\n        <tbody>\n          {ths.length === 0 ? (\n            <tr>\n              <td colSpan={ths.length}>No tables or streams found</td>\n            </tr>\n          ) : (\n            rows.map((row) => (\n              <tr key={row.id}>\n                {row.cells.map((cell) => (\n                  <td key={cell.id}>{cell.value.toString()}</td>\n                ))}\n              </tr>\n            ))\n          )}\n        </tbody>\n      </S.ScrollableTable>\n    </S.Wrapper>\n  );\n};\n\nexport default TableRenderer;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/KsqlDb/TableView.tsx",
    "content": "import React from 'react';\nimport { KsqlStreamDescription, KsqlTableDescription } from 'generated-sources';\nimport Table from 'components/common/NewTable';\nimport { ColumnDef } from '@tanstack/react-table';\n\ninterface TableViewProps {\n  fetching: boolean;\n  rows: KsqlTableDescription[] | KsqlStreamDescription[];\n}\n\nconst TableView: React.FC<TableViewProps> = ({ fetching, rows }) => {\n  const columns = React.useMemo<\n    ColumnDef<KsqlTableDescription | KsqlStreamDescription>[]\n  >(\n    () => [\n      { header: 'Name', accessorKey: 'name' },\n      { header: 'Topic', accessorKey: 'topic' },\n      { header: 'Key Format', accessorKey: 'keyFormat' },\n      { header: 'Value Format', accessorKey: 'valueFormat' },\n      {\n        header: 'Is Windowed',\n        accessorKey: 'isWindowed',\n        cell: ({ row }) =>\n          'isWindowed' in row.original ? String(row.original.isWindowed) : '-',\n      },\n    ],\n    []\n  );\n  return (\n    <Table\n      data={rows || []}\n      columns={columns}\n      emptyMessage={fetching ? 'Loading...' : 'No rows found'}\n      enableSorting\n    />\n  );\n};\n\nexport default TableView;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx",
    "content": "import React from 'react';\nimport { Cluster, ClusterFeaturesEnum } from 'generated-sources';\nimport {\n  clusterBrokersPath,\n  clusterTopicsPath,\n  clusterConsumerGroupsPath,\n  clusterSchemasPath,\n  clusterConnectorsPath,\n  clusterKsqlDbPath,\n  clusterACLPath,\n} from 'lib/paths';\n\nimport ClusterMenuItem from './ClusterMenuItem';\nimport ClusterTab from './ClusterTab/ClusterTab';\nimport * as S from './Nav.styled';\n\ninterface Props {\n  cluster: Cluster;\n  singleMode?: boolean;\n}\n\nconst ClusterMenu: React.FC<Props> = ({\n  cluster: { name, status, features },\n  singleMode,\n}) => {\n  const hasFeatureConfigured = (key: ClusterFeaturesEnum) =>\n    features?.includes(key);\n  const [isOpen, setIsOpen] = React.useState(!!singleMode);\n  return (\n    <S.List>\n      <hr />\n      <ClusterTab\n        title={name}\n        status={status}\n        isOpen={isOpen}\n        toggleClusterMenu={() => setIsOpen((prev) => !prev)}\n      />\n      {isOpen && (\n        <S.List>\n          <ClusterMenuItem to={clusterBrokersPath(name)} title=\"Brokers\" />\n          <ClusterMenuItem to={clusterTopicsPath(name)} title=\"Topics\" />\n          <ClusterMenuItem\n            to={clusterConsumerGroupsPath(name)}\n            title=\"Consumers\"\n          />\n          {hasFeatureConfigured(ClusterFeaturesEnum.SCHEMA_REGISTRY) && (\n            <ClusterMenuItem\n              to={clusterSchemasPath(name)}\n              title=\"Schema Registry\"\n            />\n          )}\n          {hasFeatureConfigured(ClusterFeaturesEnum.KAFKA_CONNECT) && (\n            <ClusterMenuItem\n              to={clusterConnectorsPath(name)}\n              title=\"Kafka Connect\"\n            />\n          )}\n          {hasFeatureConfigured(ClusterFeaturesEnum.KSQL_DB) && (\n            <ClusterMenuItem to={clusterKsqlDbPath(name)} title=\"KSQL DB\" />\n          )}\n          {(hasFeatureConfigured(ClusterFeaturesEnum.KAFKA_ACL_VIEW) ||\n            hasFeatureConfigured(ClusterFeaturesEnum.KAFKA_ACL_EDIT)) && (\n            <ClusterMenuItem to={clusterACLPath(name)} title=\"ACL\" />\n          )}\n        </S.List>\n      )}\n    </S.List>\n  );\n};\n\nexport default ClusterMenu;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Nav/ClusterMenuItem.tsx",
    "content": "import React, { PropsWithChildren } from 'react';\n\nimport * as S from './Nav.styled';\n\nexport interface ClusterMenuItemProps {\n  to: string;\n  title?: string;\n  isTopLevel?: boolean;\n}\n\nconst ClusterMenuItem: React.FC<PropsWithChildren<ClusterMenuItemProps>> = (\n  props\n) => {\n  const { to, title, children, isTopLevel } = props;\n\n  if (to) {\n    return (\n      <S.ListItem $isTopLevel={isTopLevel}>\n        <S.Link to={to} title={title}>\n          {title}\n        </S.Link>\n        {children}\n      </S.ListItem>\n    );\n  }\n\n  return <S.ListItem {...props} />;\n};\n\nexport default ClusterMenuItem;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Nav/ClusterTab/ClusterTab.styled.ts",
    "content": "import styled, { css } from 'styled-components';\nimport { ServerStatus } from 'generated-sources';\n\nexport const Wrapper = styled.li.attrs({ role: 'menuitem' })<{\n  isOpen: boolean;\n}>(\n  ({ theme, isOpen }) => css`\n    font-size: 14px;\n    font-weight: 500;\n    user-select: none;\n\n    display: grid;\n    grid-template-columns: min-content min-content auto min-content;\n    grid-template-areas: 'title status . chevron';\n    gap: 0 5px;\n\n    padding: 0.5em 0.75em;\n    cursor: pointer;\n    text-decoration: none;\n    margin: 0;\n    line-height: 20px;\n    align-items: center;\n    color: ${isOpen ? theme.menu.color.isOpen : theme.menu.color.normal};\n    background-color: ${theme.menu.backgroundColor.normal};\n\n    &:hover {\n      background-color: ${theme.menu.backgroundColor.hover};\n      color: ${theme.menu.color.hover};\n    }\n  `\n);\n\nexport const Title = styled.div`\n  grid-area: title;\n  white-space: nowrap;\n  max-width: 110px;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  color: ${({ theme }) => theme.menu.titleColor};\n`;\n\nexport const StatusIconWrapper = styled.svg.attrs({\n  viewBox: '0 0 4 4',\n  xmlns: 'http://www.w3.org/2000/svg',\n})`\n  grid-area: status;\n  fill: none;\n  width: 4px;\n  height: 4px;\n`;\n\nexport const StatusIcon = styled.circle.attrs({\n  cx: 2,\n  cy: 2,\n  r: 2,\n  role: 'status-circle',\n})<{ status: ServerStatus }>(({ theme, status }) => {\n  const statusColor: {\n    [k in ServerStatus]: string;\n  } = {\n    [ServerStatus.ONLINE]: theme.menu.statusIconColor.online,\n    [ServerStatus.OFFLINE]: theme.menu.statusIconColor.offline,\n    [ServerStatus.INITIALIZING]: theme.menu.statusIconColor.initializing,\n  };\n\n  return css`\n    fill: ${statusColor[status]};\n  `;\n});\n\nexport const ChevronWrapper = styled.svg.attrs({\n  viewBox: '0 0 10 6',\n  xmlns: 'http://www.w3.org/2000/svg',\n})`\n  grid-area: chevron;\n  width: 10px;\n  height: 6px;\n  fill: none;\n`;\n\ntype ChevronIconProps = { $open: boolean };\n\nexport const ChevronIcon = styled.path.attrs<ChevronIconProps>(({ $open }) => ({\n  d: $open ? 'M8.99988 5L4.99988 1L0.999878 5' : 'M1 1L5 5L9 1',\n}))<ChevronIconProps>`\n  stroke: ${({ theme }) => theme.menu.chevronIconColor};\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Nav/ClusterTab/ClusterTab.tsx",
    "content": "import React from 'react';\nimport { ServerStatus } from 'generated-sources';\n\nimport * as S from './ClusterTab.styled';\n\nexport interface ClusterTabProps {\n  title?: string;\n  status: ServerStatus;\n  isOpen: boolean;\n  toggleClusterMenu: () => void;\n}\n\nconst ClusterTab: React.FC<ClusterTabProps> = ({\n  status,\n  title,\n  isOpen,\n  toggleClusterMenu,\n}) => (\n  <S.Wrapper onClick={toggleClusterMenu} isOpen>\n    <S.Title title={title}>{title}</S.Title>\n\n    <S.StatusIconWrapper>\n      <S.StatusIcon status={status} aria-label=\"status\">\n        <title>{status}</title>\n      </S.StatusIcon>\n    </S.StatusIconWrapper>\n\n    <S.ChevronWrapper>\n      <S.ChevronIcon $open={isOpen} />\n    </S.ChevronWrapper>\n  </S.Wrapper>\n);\n\nexport default ClusterTab;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Nav/ClusterTab/__tests__/ClusterTab.spec.tsx",
    "content": "import { screen, waitFor } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport ClusterTab, {\n  ClusterTabProps,\n} from 'components/Nav/ClusterTab/ClusterTab';\nimport { ServerStatus } from 'generated-sources';\nimport React from 'react';\nimport { render } from 'lib/testHelpers';\n\nconst testClusterName = 'My-Huge-Cluster';\nconst toggleClusterMenuMock = jest.fn();\n\ndescribe('ClusterTab component', () => {\n  const setupWrapper = (props?: Partial<ClusterTabProps>) => (\n    <ClusterTab\n      status={ServerStatus.ONLINE}\n      isOpen\n      title={testClusterName}\n      toggleClusterMenu={toggleClusterMenuMock}\n      {...props}\n    />\n  );\n\n  it('renders cluster name', () => {\n    render(setupWrapper());\n    expect(screen.getByText(testClusterName)).toBeInTheDocument();\n  });\n\n  it('renders correct status icon for online cluster', () => {\n    render(setupWrapper());\n    expect(screen.getByText(ServerStatus.ONLINE)).toBeInTheDocument();\n  });\n\n  it('renders correct status icon for offline cluster', () => {\n    render(setupWrapper({ status: ServerStatus.OFFLINE }));\n    expect(screen.getByText(ServerStatus.OFFLINE)).toBeInTheDocument();\n  });\n\n  it('handles onClick action', () => {\n    const { baseElement } = render(setupWrapper());\n    userEvent.click(baseElement);\n    waitFor(() => expect(toggleClusterMenuMock).toHaveBeenCalled());\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Nav/ClusterTab/__tests__/ClusterTab.styled.spec.tsx",
    "content": "import React from 'react';\nimport { render } from 'lib/testHelpers';\nimport { theme } from 'theme/theme';\nimport { screen } from '@testing-library/react';\nimport * as S from 'components/Nav/ClusterTab/ClusterTab.styled';\nimport { ServerStatus } from 'generated-sources';\n\ndescribe('Cluster Styled Components', () => {\n  const getMenuItem = () => screen.getByRole('menuitem');\n  describe('Wrapper Component', () => {\n    it('should check the rendering and correct Styling when it is open', () => {\n      render(<S.Wrapper isOpen />);\n      expect(getMenuItem()).toHaveStyle(`color:${theme.menu.color.isOpen}`);\n    });\n    it('should check the rendering and correct Styling when it is Not open', () => {\n      render(<S.Wrapper isOpen={false} />);\n      expect(getMenuItem()).toHaveStyle(`color:${theme.menu.color.normal}`);\n    });\n  });\n\n  describe('StatusIcon Component', () => {\n    const getStatusCircle = () => screen.getByRole('status-circle');\n    it('should check the rendering and correct Styling when it is online', () => {\n      render(\n        <svg>\n          <S.StatusIcon status={ServerStatus.ONLINE} />\n        </svg>\n      );\n\n      expect(getStatusCircle()).toHaveStyle(\n        `fill:${theme.menu.statusIconColor.online}`\n      );\n    });\n\n    it('should check the rendering and correct Styling when it is offline', () => {\n      render(\n        <svg>\n          <S.StatusIcon status={ServerStatus.OFFLINE} />\n        </svg>\n      );\n      expect(getStatusCircle()).toHaveStyle(\n        `fill:${theme.menu.statusIconColor.offline}`\n      );\n    });\n\n    it('should check the rendering and correct Styling when it is Initializing', () => {\n      render(\n        <svg>\n          <S.StatusIcon status={ServerStatus.INITIALIZING} />\n        </svg>\n      );\n      expect(getStatusCircle()).toHaveStyle(\n        `fill:${theme.menu.statusIconColor.initializing}`\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Nav/Nav.styled.ts",
    "content": "import { NavLink } from 'react-router-dom';\nimport styled, { css } from 'styled-components';\n\nexport const List = styled.ul.attrs({ role: 'menu' })`\n  padding-bottom: 4px;\n\n  & > & {\n    padding-left: 8px;\n  }\n`;\n\nexport const Link = styled(NavLink)(\n  ({ theme }) => css`\n    width: 100%;\n    padding: 0.5em 0.75em;\n    cursor: pointer;\n    text-decoration: none;\n    margin: 0 0;\n    background-color: ${theme.menu.backgroundColor.normal};\n    color: ${theme.menu.color.normal};\n\n    &:hover {\n      background-color: ${theme.menu.backgroundColor.hover};\n      color: ${theme.menu.color.hover};\n    }\n    &.active {\n      background-color: ${theme.menu.backgroundColor.active};\n      color: ${theme.menu.color.active};\n    }\n  `\n);\n\nexport const ListItem = styled('li').attrs({ role: 'menuitem' })<{\n  $isTopLevel?: boolean;\n}>`\n  font-size: 14px;\n  font-weight: ${({ $isTopLevel }) => ($isTopLevel ? 500 : 'normal')};\n  height: 32px;\n  display: flex;\n  user-select: none;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Nav/Nav.tsx",
    "content": "import { useClusters } from 'lib/hooks/api/clusters';\nimport React from 'react';\n\nimport ClusterMenu from './ClusterMenu';\nimport ClusterMenuItem from './ClusterMenuItem';\nimport * as S from './Nav.styled';\n\nconst Nav: React.FC = () => {\n  const clusters = useClusters();\n\n  return (\n    <aside aria-label=\"Sidebar Menu\">\n      <S.List>\n        <ClusterMenuItem to=\"/\" title=\"Dashboard\" isTopLevel />\n      </S.List>\n      {clusters.isSuccess &&\n        clusters.data.map((cluster) => (\n          <ClusterMenu\n            cluster={cluster}\n            key={cluster.name}\n            singleMode={clusters.data.length === 1}\n          />\n        ))}\n    </aside>\n  );\n};\n\nexport default Nav;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Nav/__tests__/ClusterMenu.spec.tsx",
    "content": "import React from 'react';\nimport { screen } from '@testing-library/react';\nimport { Cluster, ClusterFeaturesEnum } from 'generated-sources';\nimport ClusterMenu from 'components/Nav/ClusterMenu';\nimport userEvent from '@testing-library/user-event';\nimport { clusterConnectorsPath } from 'lib/paths';\nimport { render } from 'lib/testHelpers';\nimport { onlineClusterPayload } from 'lib/fixtures/clusters';\n\ndescribe('ClusterMenu', () => {\n  const setupComponent = (cluster: Cluster, singleMode?: boolean) => (\n    <ClusterMenu cluster={cluster} singleMode={singleMode} />\n  );\n  const getMenuItems = () => screen.getAllByRole('menuitem');\n  const getMenuItem = () => screen.getByRole('menuitem');\n  const getBrokers = () => screen.getByTitle('Brokers');\n  const getTopics = () => screen.getByTitle('Brokers');\n  const getConsumers = () => screen.getByTitle('Brokers');\n  const getKafkaConnect = () => screen.getByTitle('Kafka Connect');\n  const getCluster = () => screen.getByText(onlineClusterPayload.name);\n\n  it('renders cluster menu with default set of features', async () => {\n    render(setupComponent(onlineClusterPayload));\n    expect(getCluster()).toBeInTheDocument();\n\n    expect(getMenuItems().length).toEqual(1);\n    await userEvent.click(getMenuItem());\n    expect(getMenuItems().length).toEqual(4);\n\n    expect(getBrokers()).toBeInTheDocument();\n    expect(getTopics()).toBeInTheDocument();\n    expect(getConsumers()).toBeInTheDocument();\n  });\n  it('renders cluster menu with correct set of features', async () => {\n    render(\n      setupComponent({\n        ...onlineClusterPayload,\n        features: [\n          ClusterFeaturesEnum.SCHEMA_REGISTRY,\n          ClusterFeaturesEnum.KAFKA_CONNECT,\n          ClusterFeaturesEnum.KSQL_DB,\n        ],\n      })\n    );\n    expect(getMenuItems().length).toEqual(1);\n    await userEvent.click(getMenuItem());\n    expect(getMenuItems().length).toEqual(7);\n\n    expect(getBrokers()).toBeInTheDocument();\n    expect(getTopics()).toBeInTheDocument();\n    expect(getConsumers()).toBeInTheDocument();\n    expect(screen.getByTitle('Schema Registry')).toBeInTheDocument();\n    expect(getKafkaConnect()).toBeInTheDocument();\n    expect(screen.getByTitle('KSQL DB')).toBeInTheDocument();\n  });\n  it('renders open cluster menu', () => {\n    render(setupComponent(onlineClusterPayload, true), {\n      initialEntries: [clusterConnectorsPath(onlineClusterPayload.name)],\n    });\n\n    expect(getMenuItems().length).toEqual(4);\n    expect(getCluster()).toBeInTheDocument();\n    expect(getBrokers()).toBeInTheDocument();\n    expect(getTopics()).toBeInTheDocument();\n    expect(getConsumers()).toBeInTheDocument();\n  });\n  it('makes Kafka Connect link active', async () => {\n    render(\n      setupComponent({\n        ...onlineClusterPayload,\n        features: [ClusterFeaturesEnum.KAFKA_CONNECT],\n      }),\n      { initialEntries: [clusterConnectorsPath(onlineClusterPayload.name)] }\n    );\n    expect(getMenuItems().length).toEqual(1);\n    await userEvent.click(getMenuItem());\n    expect(getMenuItems().length).toEqual(5);\n\n    const kafkaConnect = getKafkaConnect();\n    expect(kafkaConnect).toBeInTheDocument();\n\n    expect(getKafkaConnect()).toHaveClass('active');\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Nav/__tests__/ClusterMenuItem.spec.tsx",
    "content": "import React from 'react';\nimport ClusterMenuItem, {\n  ClusterMenuItemProps,\n} from 'components/Nav/ClusterMenuItem';\nimport { screen } from '@testing-library/react';\nimport { render } from 'lib/testHelpers';\n\ndescribe('ClusterMenuItem', () => {\n  const setupComponent = (props: Partial<ClusterMenuItemProps> = {}) => (\n    <ul>\n      <ClusterMenuItem to=\"/test\" {...props} />\n    </ul>\n  );\n\n  const getMenuItem = () => screen.getByRole('menuitem');\n  const getLink = () => screen.queryByRole('link');\n\n  it('renders component with correct title', () => {\n    const testTitle = 'My Test Title';\n    render(setupComponent({ title: testTitle }));\n    expect(screen.getByText(testTitle)).toBeInTheDocument();\n  });\n\n  it('renders top level component with correct styles', () => {\n    render(setupComponent({ isTopLevel: true }));\n    expect(getMenuItem()).toHaveStyle({ fontWeight: '500' });\n  });\n\n  it('renders non-top level component with correct styles', () => {\n    render(setupComponent({ isTopLevel: false }));\n    expect(getMenuItem()).toHaveStyle({ fontWeight: 'normal' });\n  });\n\n  it('renders list item with link inside', () => {\n    render(setupComponent({ to: '/my-cluster' }));\n    expect(getMenuItem()).toBeInTheDocument();\n    expect(getLink()).toBeInTheDocument();\n  });\n\n  it('renders list item without link inside', () => {\n    render(setupComponent({ to: '' }));\n    expect(getMenuItem()).toBeInTheDocument();\n    expect(getLink()).not.toBeInTheDocument();\n  });\n\n  it('renders list item with children', () => {\n    render(\n      <ul>\n        <ClusterMenuItem to=\"/test\">Test Text Box</ClusterMenuItem>\n      </ul>\n    );\n    expect(getMenuItem()).toBeInTheDocument();\n    expect(getLink()).toBeInTheDocument();\n    expect(screen.getByText('Test Text Box')).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Nav/__tests__/Nav.spec.tsx",
    "content": "import React from 'react';\nimport Nav from 'components/Nav/Nav';\nimport { screen } from '@testing-library/react';\nimport { render } from 'lib/testHelpers';\nimport { Cluster } from 'generated-sources';\nimport { useClusters } from 'lib/hooks/api/clusters';\nimport {\n  offlineClusterPayload,\n  onlineClusterPayload,\n} from 'lib/fixtures/clusters';\n\njest.mock('lib/hooks/api/clusters', () => ({\n  useClusters: jest.fn(),\n}));\n\ndescribe('Nav', () => {\n  const renderComponent = (payload: Cluster[] = []) => {\n    (useClusters as jest.Mock).mockImplementation(() => ({\n      data: payload,\n      isSuccess: true,\n    }));\n    render(<Nav />);\n  };\n\n  const getDashboard = () => screen.getByText('Dashboard');\n\n  const getMenuItemsCount = () => screen.getAllByRole('menuitem').length;\n  it('renders loader', () => {\n    renderComponent();\n\n    expect(getMenuItemsCount()).toEqual(1);\n    expect(getDashboard()).toBeInTheDocument();\n  });\n\n  it('renders ClusterMenu', () => {\n    renderComponent([onlineClusterPayload, offlineClusterPayload]);\n    expect(screen.getAllByRole('menu').length).toEqual(3);\n    expect(getMenuItemsCount()).toEqual(3);\n    expect(getDashboard()).toBeInTheDocument();\n    expect(screen.getByText(onlineClusterPayload.name)).toBeInTheDocument();\n    expect(screen.getByText(offlineClusterPayload.name)).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/NavBar/NavBar.styled.ts",
    "content": "import styled, { css } from 'styled-components';\nimport { Link } from 'react-router-dom';\nimport DiscordIcon from 'components/common/Icons/DiscordIcon';\nimport GitIcon from 'components/common/Icons/GitIcon';\n\nexport const Navbar = styled.nav(\n  ({ theme }) => css`\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    border-bottom: 1px solid ${theme.layout.stuffBorderColor};\n    position: fixed;\n    top: 0;\n    left: 0;\n    right: 0;\n    z-index: 30;\n    background-color: ${theme.menu.backgroundColor.normal};\n    min-height: 3.25rem;\n  `\n);\n\nexport const NavbarBrand = styled.div`\n  display: flex;\n  justify-content: flex-end;\n  align-items: center !important;\n  flex-shrink: 0;\n  min-height: 3.25rem;\n`;\n\nexport const SocialLink = styled.a(\n  ({ theme: { icons } }) => css`\n    display: block;\n    margin-top: 5px;\n    cursor: pointer;\n    fill: ${icons.discord.normal};\n\n    &:hover {\n      ${DiscordIcon} {\n        fill: ${icons.discord.hover};\n      }\n\n      ${GitIcon} {\n        fill: ${icons.git.hover};\n      }\n    }\n\n    &:active {\n      ${DiscordIcon} {\n        fill: ${icons.discord.active};\n      }\n\n      ${GitIcon} {\n        fill: ${icons.git.active};\n      }\n    }\n  `\n);\n\nexport const NavbarSocial = styled.div`\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  margin: 5px 10px 5px;\n`;\n\nexport const NavbarItem = styled.div`\n  display: flex;\n  position: relative;\n  flex-grow: 0;\n  flex-shrink: 0;\n  align-items: center;\n  line-height: 1.5;\n  padding: 0.5rem 0.75rem;\n`;\n\nexport const NavbarBurger = styled.div(\n  ({ theme }) => css`\n    display: block;\n    position: relative;\n    cursor: pointer;\n    height: 3.25rem;\n    width: 3.25rem;\n    margin: 0;\n    padding: 0;\n\n    &:hover {\n      background-color: ${theme.menu.backgroundColor.hover};\n    }\n\n    @media screen and (min-width: 1024px) {\n      display: none;\n    }\n  `\n);\n\nexport const Span = styled.span(\n  ({ theme }) => css`\n    display: block;\n    position: absolute;\n    background: ${theme.menu.color.active};\n    height: 1px;\n    left: calc(50% - 8px);\n    transform-origin: center;\n    transition-duration: 86ms;\n    transition-property: background-color, opacity, transform, -webkit-transform;\n    transition-timing-function: ease-out;\n    width: 16px;\n\n    &:first-child {\n      top: calc(50% - 6px);\n    }\n\n    &:nth-child(2) {\n      top: calc(50% - 1px);\n    }\n\n    &:nth-child(3) {\n      top: calc(50% + 4px);\n    }\n  `\n);\n\nexport const Hyperlink = styled(Link)(\n  ({ theme }) => css`\n    position: relative;\n\n    display: flex;\n    flex-grow: 0;\n    flex-shrink: 0;\n    align-items: center;\n    gap: 8px;\n\n    margin: 0;\n    padding: 0.5rem 0.75rem;\n\n    font-family: Inter, sans-serif;\n    font-style: normal;\n    font-weight: bold;\n    font-size: 12px;\n    line-height: 16px;\n    color: ${theme.default.color.normal};\n    &:hover {\n      color: ${theme.default.color.normal};\n    }\n    text-decoration: none;\n    word-break: break-word;\n    cursor: pointer;\n  `\n);\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/NavBar/NavBar.tsx",
    "content": "import React, { useContext } from 'react';\nimport Select from 'components/common/Select/Select';\nimport Logo from 'components/common/Logo/Logo';\nimport Version from 'components/Version/Version';\nimport GitIcon from 'components/common/Icons/GitIcon';\nimport DiscordIcon from 'components/common/Icons/DiscordIcon';\nimport AutoIcon from 'components/common/Icons/AutoIcon';\nimport SunIcon from 'components/common/Icons/SunIcon';\nimport MoonIcon from 'components/common/Icons/MoonIcon';\nimport { ThemeModeContext } from 'components/contexts/ThemeModeContext';\n\nimport UserInfo from './UserInfo/UserInfo';\nimport * as S from './NavBar.styled';\n\ninterface Props {\n  onBurgerClick: () => void;\n}\n\nexport type ThemeDropDownValue = 'auto_theme' | 'light_theme' | 'dark_theme';\n\nconst options = [\n  {\n    label: (\n      <>\n        <AutoIcon />\n        <div>Auto theme</div>\n      </>\n    ),\n    value: 'auto_theme',\n  },\n  {\n    label: (\n      <>\n        <SunIcon />\n        <div>Light theme</div>\n      </>\n    ),\n    value: 'light_theme',\n  },\n  {\n    label: (\n      <>\n        <MoonIcon />\n        <div>Dark theme</div>\n      </>\n    ),\n    value: 'dark_theme',\n  },\n];\n\nconst NavBar: React.FC<Props> = ({ onBurgerClick }) => {\n  const { themeMode, setThemeMode } = useContext(ThemeModeContext);\n\n  return (\n    <S.Navbar role=\"navigation\" aria-label=\"Page Header\">\n      <S.NavbarBrand>\n        <S.NavbarBrand>\n          <S.NavbarBurger\n            onClick={onBurgerClick}\n            onKeyDown={onBurgerClick}\n            role=\"button\"\n            tabIndex={0}\n            aria-label=\"burger\"\n          >\n            <S.Span role=\"separator\" />\n            <S.Span role=\"separator\" />\n            <S.Span role=\"separator\" />\n          </S.NavbarBurger>\n\n          <S.Hyperlink to=\"/\">\n            <Logo />\n            UI for Apache Kafka\n          </S.Hyperlink>\n\n          <S.NavbarItem>\n            <Version />\n          </S.NavbarItem>\n        </S.NavbarBrand>\n      </S.NavbarBrand>\n      <S.NavbarSocial>\n        <Select\n          options={options}\n          value={themeMode}\n          onChange={setThemeMode}\n          isThemeMode\n        />\n        <S.SocialLink\n          href=\"https://github.com/provectus/kafka-ui\"\n          target=\"_blank\"\n        >\n          <GitIcon />\n        </S.SocialLink>\n        <S.SocialLink\n          href=\"https://discord.com/invite/4DWzD7pGE5\"\n          target=\"_blank\"\n        >\n          <DiscordIcon />\n        </S.SocialLink>\n        <UserInfo />\n      </S.NavbarSocial>\n    </S.Navbar>\n  );\n};\n\nexport default NavBar;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/NavBar/UserInfo/UserInfo.styled.ts",
    "content": "import styled, { css } from 'styled-components';\n\nexport const Wrapper = styled.div`\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  gap: 5px;\n  svg {\n    position: relative;\n  }\n`;\n\nexport const Text = styled.div(\n  ({ theme }) => css`\n    color: ${theme.button.primary.invertedColors.normal};\n  `\n);\n\nexport const LogoutLink = styled.a``;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/NavBar/UserInfo/UserInfo.tsx",
    "content": "import React from 'react';\nimport { Dropdown, DropdownItem } from 'components/common/Dropdown';\nimport UserIcon from 'components/common/Icons/UserIcon';\nimport DropdownArrowIcon from 'components/common/Icons/DropdownArrowIcon';\nimport { useUserInfo } from 'lib/hooks/useUserInfo';\n\nimport * as S from './UserInfo.styled';\n\nconst UserInfo = () => {\n  const { username } = useUserInfo();\n\n  return username ? (\n    <Dropdown\n      label={\n        <S.Wrapper>\n          <UserIcon />\n          <S.Text>{username}</S.Text>\n          <DropdownArrowIcon isOpen={false} />\n        </S.Wrapper>\n      }\n    >\n      <DropdownItem href={`${window.basePath}/logout`}>\n        <S.LogoutLink>Log out</S.LogoutLink>\n      </DropdownItem>\n    </Dropdown>\n  ) : null;\n};\n\nexport default UserInfo;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/NavBar/UserInfo/__tests__/UserInfo.spec.tsx",
    "content": "import React from 'react';\nimport { screen } from '@testing-library/react';\nimport { render } from 'lib/testHelpers';\nimport UserInfo from 'components/NavBar/UserInfo/UserInfo';\nimport { useUserInfo } from 'lib/hooks/useUserInfo';\nimport userEvent from '@testing-library/user-event';\n\njest.mock('lib/hooks/useUserInfo', () => ({\n  useUserInfo: jest.fn(),\n}));\n\ndescribe('UserInfo', () => {\n  const renderComponent = () => render(<UserInfo />);\n\n  it('should render the userInfo with correct data', () => {\n    const username = 'someName';\n    (useUserInfo as jest.Mock).mockImplementation(() => ({ username }));\n\n    renderComponent();\n    expect(screen.getByText(username)).toBeInTheDocument();\n  });\n\n  it('should render the userInfo during click opens the dropdown', async () => {\n    const username = 'someName';\n    Object.defineProperty(window, 'basePath', {\n      value: '',\n      writable: true,\n    });\n    (useUserInfo as jest.Mock).mockImplementation(() => ({ username }));\n\n    renderComponent();\n    const dropdown = screen.getByText(username);\n    await userEvent.click(dropdown);\n\n    const logout = screen.getByText('Log out');\n    expect(logout).toBeInTheDocument();\n  });\n\n  it('should render correct url during basePath initialization', async () => {\n    const username = 'someName';\n    const baseUrl = '/path';\n    Object.defineProperty(window, 'basePath', {\n      value: baseUrl,\n      writable: true,\n    });\n    (useUserInfo as jest.Mock).mockImplementation(() => ({ username }));\n\n    renderComponent();\n\n    const logout = screen.getByText('Log out');\n    expect(logout).toBeInTheDocument();\n  });\n\n  it('should not render anything if the username does not exists', () => {\n    (useUserInfo as jest.Mock).mockImplementation(() => ({\n      username: undefined,\n    }));\n\n    renderComponent();\n    expect(screen.queryByRole('listbox')).not.toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/NavBar/__tests__/NavBar.spec.tsx",
    "content": "import React from 'react';\nimport { render } from 'lib/testHelpers';\nimport NavBar from 'components/NavBar/NavBar';\nimport { screen, within } from '@testing-library/react';\n\nconst burgerButtonOptions = { name: 'burger' };\n\njest.mock('components/Version/Version', () => () => <div>Version</div>);\njest.mock('components/NavBar/UserInfo/UserInfo', () => () => (\n  <div>UserInfo</div>\n));\n\ndescribe('NavBar', () => {\n  beforeEach(() => {\n    Object.defineProperty(window, 'matchMedia', {\n      writable: true,\n      value: jest.fn().mockImplementation(() => ({\n        matches: false,\n        addListener: jest.fn(),\n      })),\n    });\n\n    render(<NavBar onBurgerClick={jest.fn()} setDarkMode={jest.fn()} />);\n  });\n\n  it('correctly renders header', () => {\n    const header = screen.getByLabelText('Page Header');\n    expect(header).toBeInTheDocument();\n    expect(within(header).getByText('UI for Apache Kafka')).toBeInTheDocument();\n    expect(within(header).getAllByRole('separator').length).toEqual(3);\n    expect(\n      within(header).getByRole('button', burgerButtonOptions)\n    ).toBeInTheDocument();\n    expect(within(header).getByText('UserInfo')).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/PageContainer/PageContainer.styled.ts",
    "content": "import styled, { css } from 'styled-components';\n\nexport const Container = styled.main(\n  ({ theme }) => css`\n    margin-top: ${theme.layout.navBarHeight};\n    margin-left: ${theme.layout.navBarWidth};\n    position: relative;\n    padding-bottom: 30px;\n    z-index: 20;\n    max-width: calc(100vw - ${theme.layout.navBarWidth});\n    @media screen and (max-width: 1023px) {\n      margin-left: initial;\n      max-width: 100vw;\n    }\n  `\n);\n\nexport const Sidebar = styled.div<{ $visible: boolean }>(\n  ({ theme, $visible }) => css`\n    width: ${theme.layout.navBarWidth};\n    display: flex;\n    flex-direction: column;\n    border-right: 1px solid ${theme.layout.stuffBorderColor};\n    position: fixed;\n    top: ${theme.layout.navBarHeight};\n    left: 0;\n    bottom: 0;\n    padding: 8px 16px;\n    overflow-y: scroll;\n    transition: width 0.25s, opacity 0.25s, transform 0.25s,\n      -webkit-transform 0.25s;\n    background: ${theme.menu.backgroundColor.normal};\n    @media screen and (max-width: 1023px) {\n      ${$visible &&\n      css`\n        transform: translate3d(${theme.layout.navBarWidth}, 0, 0);\n      `};\n      left: -${theme.layout.navBarWidth};\n      z-index: 100;\n    }\n\n    &::-webkit-scrollbar {\n      width: 8px;\n    }\n\n    &::-webkit-scrollbar-track {\n      background-color: ${theme.scrollbar.trackColor.normal};\n    }\n\n    &::-webkit-scrollbar-thumb {\n      width: 8px;\n      background-color: ${theme.scrollbar.thumbColor.normal};\n      border-radius: 4px;\n    }\n\n    &:hover::-webkit-scrollbar-thumb {\n      background: ${theme.scrollbar.thumbColor.active};\n    }\n\n    &:hover::-webkit-scrollbar-track {\n      background-color: ${theme.scrollbar.trackColor.active};\n    }\n  `\n);\n\nexport const Overlay = styled.div<{ $visible: boolean }>(\n  ({ theme, $visible }) => css`\n    height: calc(100vh - ${theme.layout.navBarHeight});\n    z-index: 99;\n    visibility: hidden;\n    opacity: 0;\n    -webkit-transition: all 0.5s ease;\n    transition: all 0.5s ease;\n    left: 0;\n    position: absolute;\n    top: 0;\n    ${$visible &&\n    css`\n      @media screen and (max-width: 1023px) {\n        bottom: 0;\n        right: 0;\n        visibility: visible;\n        opacity: 0.7;\n        background-color: ${theme.layout.overlay.backgroundColor};\n      }\n    `}\n  `\n);\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/PageContainer/PageContainer.tsx",
    "content": "import React, { PropsWithChildren, useEffect, useMemo } from 'react';\nimport { useLocation, useNavigate } from 'react-router-dom';\nimport NavBar from 'components/NavBar/NavBar';\nimport * as S from 'components/PageContainer/PageContainer.styled';\nimport Nav from 'components/Nav/Nav';\nimport useBoolean from 'lib/hooks/useBoolean';\nimport { clusterNewConfigPath } from 'lib/paths';\nimport { GlobalSettingsContext } from 'components/contexts/GlobalSettingsContext';\nimport { useClusters } from 'lib/hooks/api/clusters';\nimport { ResourceType } from 'generated-sources';\nimport { useGetUserInfo } from 'lib/hooks/api/roles';\n\nconst PageContainer: React.FC<PropsWithChildren<unknown>> = ({ children }) => {\n  const {\n    value: isSidebarVisible,\n    toggle,\n    setFalse: closeSidebar,\n  } = useBoolean(false);\n  const clusters = useClusters();\n  const appInfo = React.useContext(GlobalSettingsContext);\n  const location = useLocation();\n  const navigate = useNavigate();\n  const { data: authInfo } = useGetUserInfo();\n\n  React.useEffect(() => {\n    closeSidebar();\n  }, [location, closeSidebar]);\n\n  const hasApplicationPermissions = useMemo(() => {\n    if (!authInfo?.rbacEnabled) return true;\n    return !!authInfo?.userInfo?.permissions.some(\n      (permission) => permission.resource === ResourceType.APPLICATIONCONFIG\n    );\n  }, [authInfo]);\n\n  useEffect(() => {\n    if (!appInfo.hasDynamicConfig) return;\n    if (clusters?.data?.length !== 0) return;\n    if (!hasApplicationPermissions) return;\n    navigate(clusterNewConfigPath);\n  }, [clusters?.data, appInfo.hasDynamicConfig]);\n\n  return (\n    <>\n      <NavBar onBurgerClick={toggle} />\n      <S.Container>\n        <S.Sidebar aria-label=\"Sidebar\" $visible={isSidebarVisible}>\n          <Nav />\n        </S.Sidebar>\n        <S.Overlay\n          $visible={isSidebarVisible}\n          onClick={closeSidebar}\n          onKeyDown={closeSidebar}\n          tabIndex={-1}\n          aria-hidden=\"true\"\n          aria-label=\"Overlay\"\n        />\n        {children}\n      </S.Container>\n    </>\n  );\n};\n\nexport default PageContainer;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/PageContainer/__tests__/PageContainer.spec.tsx",
    "content": "import React from 'react';\nimport { screen, within } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { render } from 'lib/testHelpers';\nimport PageContainer from 'components/PageContainer/PageContainer';\nimport { useClusters } from 'lib/hooks/api/clusters';\nimport { Cluster, ServerStatus } from 'generated-sources';\n\nconst burgerButtonOptions = { name: 'burger' };\n\njest.mock('components/Version/Version', () => () => <div>Version</div>);\ninterface DataType {\n  data: Cluster[] | undefined;\n}\njest.mock('lib/hooks/api/clusters');\nconst mockedNavigate = jest.fn();\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useNavigate: () => mockedNavigate,\n}));\ndescribe('Page Container', () => {\n  const renderComponent = (hasDynamicConfig: boolean, data: DataType) => {\n    const useClustersMock = useClusters as jest.Mock;\n    useClustersMock.mockReturnValue(data);\n    Object.defineProperty(window, 'matchMedia', {\n      writable: true,\n      value: jest.fn().mockImplementation(() => ({\n        matches: false,\n        addListener: jest.fn(),\n      })),\n    });\n    render(\n      <PageContainer setDarkMode={jest.fn()}>\n        <div>child</div>\n      </PageContainer>,\n      {\n        globalSettings: { hasDynamicConfig },\n      }\n    );\n  };\n\n  it('handle burger click correctly', async () => {\n    renderComponent(false, { data: undefined });\n    const burger = within(screen.getByLabelText('Page Header')).getByRole(\n      'button',\n      burgerButtonOptions\n    );\n    const overlay = screen.getByLabelText('Overlay');\n    expect(screen.getByLabelText('Sidebar')).toBeInTheDocument();\n    expect(overlay).toBeInTheDocument();\n    expect(overlay).toHaveStyleRule('visibility: hidden');\n    expect(burger).toHaveStyleRule('display: none');\n    await userEvent.click(burger);\n    expect(overlay).toHaveStyleRule('visibility: visible');\n  });\n\n  it('render the inner container', async () => {\n    renderComponent(false, { data: undefined });\n    expect(screen.getByText('child')).toBeInTheDocument();\n  });\n\n  describe('Redirect to the Wizard page', () => {\n    it('redirects to new cluster configuration page if there are no clusters and dynamic config is enabled', async () => {\n      await renderComponent(true, { data: [] });\n\n      expect(mockedNavigate).toHaveBeenCalled();\n    });\n\n    it('should not navigate to new cluster config page when there are clusters', async () => {\n      await renderComponent(true, {\n        data: [{ name: 'Cluster 1', status: ServerStatus.ONLINE }],\n      });\n\n      expect(mockedNavigate).not.toHaveBeenCalled();\n    });\n\n    it('should not navigate to new cluster config page when there are no clusters and hasDynamicConfig is false', async () => {\n      await renderComponent(false, {\n        data: [],\n      });\n\n      expect(mockedNavigate).not.toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/Details/Details.tsx",
    "content": "import React from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n  ClusterSubjectParam,\n  clusterSchemaEditPageRelativePath,\n  clusterSchemaSchemaComparePageRelativePath,\n  clusterSchemasPath,\n} from 'lib/paths';\nimport ClusterContext from 'components/contexts/ClusterContext';\nimport PageLoader from 'components/common/PageLoader/PageLoader';\nimport PageHeading from 'components/common/PageHeading/PageHeading';\nimport { Button } from 'components/common/Button/Button';\nimport { useAppDispatch, useAppSelector } from 'lib/hooks/redux';\nimport {\n  fetchLatestSchema,\n  fetchSchemaVersions,\n  getAreSchemaLatestFulfilled,\n  getAreSchemaVersionsFulfilled,\n  SCHEMAS_VERSIONS_FETCH_ACTION,\n  SCHEMA_LATEST_FETCH_ACTION,\n  selectAllSchemaVersions,\n  getSchemaLatest,\n  getAreSchemaLatestRejected,\n} from 'redux/reducers/schemas/schemasSlice';\nimport { showServerError } from 'lib/errorHandling';\nimport { resetLoaderById } from 'redux/reducers/loader/loaderSlice';\nimport { TableTitle } from 'components/common/table/TableTitle/TableTitle.styled';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { schemasApiClient } from 'lib/api';\nimport { Dropdown } from 'components/common/Dropdown';\nimport Table from 'components/common/NewTable';\nimport { Action, ResourceType } from 'generated-sources';\nimport {\n  ActionButton,\n  ActionDropdownItem,\n} from 'components/common/ActionComponent';\n\nimport LatestVersionItem from './LatestVersion/LatestVersionItem';\nimport SchemaVersion from './SchemaVersion/SchemaVersion';\n\nconst Details: React.FC = () => {\n  const navigate = useNavigate();\n  const dispatch = useAppDispatch();\n  const { isReadOnly } = React.useContext(ClusterContext);\n  const { clusterName, subject } = useAppParams<ClusterSubjectParam>();\n\n  React.useEffect(() => {\n    dispatch(fetchLatestSchema({ clusterName, subject }));\n    return () => {\n      dispatch(resetLoaderById(SCHEMA_LATEST_FETCH_ACTION));\n    };\n  }, [clusterName, dispatch, subject]);\n\n  React.useEffect(() => {\n    dispatch(fetchSchemaVersions({ clusterName, subject }));\n    return () => {\n      dispatch(resetLoaderById(SCHEMAS_VERSIONS_FETCH_ACTION));\n    };\n  }, [clusterName, dispatch, subject]);\n\n  const versions = useAppSelector((state) => selectAllSchemaVersions(state));\n  const schema = useAppSelector(getSchemaLatest);\n  const isFetched = useAppSelector(getAreSchemaLatestFulfilled);\n  const isRejected = useAppSelector(getAreSchemaLatestRejected);\n  const areVersionsFetched = useAppSelector(getAreSchemaVersionsFulfilled);\n\n  const columns = React.useMemo(\n    () => [\n      { header: 'Version', accessorKey: 'version' },\n      { header: 'ID', accessorKey: 'id' },\n      { header: 'Type', accessorKey: 'schemaType' },\n    ],\n    []\n  );\n\n  const deleteHandler = async () => {\n    try {\n      await schemasApiClient.deleteSchema({\n        clusterName,\n        subject,\n      });\n      navigate('../');\n    } catch (e) {\n      showServerError(e as Response);\n    }\n  };\n\n  if (isRejected) {\n    navigate('/404');\n  }\n\n  if (!isFetched || !schema) {\n    return <PageLoader />;\n  }\n  return (\n    <>\n      <PageHeading\n        text={schema.subject}\n        backText=\"Schema Registry\"\n        backTo={clusterSchemasPath(clusterName)}\n      >\n        {!isReadOnly && (\n          <>\n            <Button\n              buttonSize=\"M\"\n              buttonType=\"primary\"\n              to={{\n                pathname: clusterSchemaSchemaComparePageRelativePath,\n                search: `leftVersion=${versions[0]?.version}&rightVersion=${versions[0]?.version}`,\n              }}\n            >\n              Compare Versions\n            </Button>\n            <ActionButton\n              buttonSize=\"M\"\n              buttonType=\"primary\"\n              to={clusterSchemaEditPageRelativePath}\n              permission={{\n                resource: ResourceType.SCHEMA,\n                action: Action.EDIT,\n                value: subject,\n              }}\n            >\n              Edit Schema\n            </ActionButton>\n            <Dropdown>\n              <ActionDropdownItem\n                confirm={\n                  <>\n                    Are you sure want to remove <b>{subject}</b> schema?\n                  </>\n                }\n                onClick={deleteHandler}\n                danger\n                permission={{\n                  resource: ResourceType.SCHEMA,\n                  action: Action.DELETE,\n                  value: subject,\n                }}\n              >\n                Remove schema\n              </ActionDropdownItem>\n            </Dropdown>\n          </>\n        )}\n      </PageHeading>\n      <LatestVersionItem schema={schema} />\n      <TableTitle>Old versions</TableTitle>\n      {areVersionsFetched ? (\n        <Table\n          columns={columns}\n          data={versions}\n          getRowCanExpand={() => true}\n          renderSubComponent={SchemaVersion}\n          enableSorting\n        />\n      ) : (\n        <PageLoader />\n      )}\n    </>\n  );\n};\n\nexport default Details;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/Details/LatestVersion/LatestVersionItem.styled.tsx",
    "content": "import Heading from 'components/common/heading/Heading.styled';\nimport React from 'react';\nimport styled from 'styled-components';\n\nexport const Wrapper = styled.div`\n  width: 100%;\n  background-color: ${({ theme }) => theme.layout.stuffColor};\n  padding: 16px;\n  display: flex;\n  justify-content: center;\n  align-items: stretch;\n  gap: 2px;\n  max-height: 700px;\n\n  & > * {\n    background-color: ${({ theme }) => theme.default.backgroundColor};\n    padding: 24px;\n    overflow-y: scroll;\n  }\n\n  & > div:first-child {\n    border-radius: 8px 0 0 8px;\n    flex-grow: 2;\n  }\n\n  & > div:last-child {\n    border-radius: 0 8px 8px 0;\n    flex-grow: 1;\n\n    & > div {\n      display: flex;\n      gap: 16px;\n      padding-bottom: 16px;\n    }\n\n    p {\n      color: ${({ theme }) => theme.schema.backgroundColor.p};\n    }\n  }\n`;\n\nexport const MetaDataLabel = styled((props) => (\n  <Heading level={4} {...props} />\n))`\n  color: ${({ theme }) => theme.lastestVersionItem.metaDataLabel.color};\n  width: 110px;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/Details/LatestVersion/LatestVersionItem.tsx",
    "content": "import React from 'react';\nimport { SchemaSubject } from 'generated-sources';\nimport EditorViewer from 'components/common/EditorViewer/EditorViewer';\nimport Heading from 'components/common/heading/Heading.styled';\n\nimport * as S from './LatestVersionItem.styled';\n\ninterface LatestVersionProps {\n  schema: SchemaSubject;\n}\n\nconst LatestVersionItem: React.FC<LatestVersionProps> = ({\n  schema: { id, subject, schema, compatibilityLevel, version, schemaType },\n}) => (\n  <S.Wrapper>\n    <div>\n      <Heading level={3}>Actual version</Heading>\n      <EditorViewer data={schema} schemaType={schemaType} maxLines={28} />\n    </div>\n    <div>\n      <div>\n        <S.MetaDataLabel>Latest version</S.MetaDataLabel>\n        <p>{version}</p>\n      </div>\n      <div>\n        <S.MetaDataLabel>ID</S.MetaDataLabel>\n        <p>{id}</p>\n      </div>\n      <div>\n        <S.MetaDataLabel>Type</S.MetaDataLabel>\n        <p>{schemaType}</p>\n      </div>\n      <div>\n        <S.MetaDataLabel>Subject</S.MetaDataLabel>\n        <p>{subject}</p>\n      </div>\n      <div>\n        <S.MetaDataLabel>Compatibility</S.MetaDataLabel>\n        <p>{compatibilityLevel}</p>\n      </div>\n    </div>\n  </S.Wrapper>\n);\n\nexport default LatestVersionItem;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/Details/SchemaVersion/SchemaVersion.tsx",
    "content": "import React from 'react';\nimport EditorViewer from 'components/common/EditorViewer/EditorViewer';\nimport { SchemaSubject } from 'generated-sources';\nimport { Row } from '@tanstack/react-table';\n\ninterface Props {\n  row: Row<SchemaSubject>;\n}\n\nconst SchemaVersion: React.FC<Props> = ({ row }) => {\n  return (\n    <EditorViewer\n      data={row?.original?.schema}\n      schemaType={row?.original?.schemaType}\n    />\n  );\n};\n\nexport default SchemaVersion;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx",
    "content": "import React from 'react';\nimport Details from 'components/Schemas/Details/Details';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { clusterSchemaPath } from 'lib/paths';\nimport { screen } from '@testing-library/dom';\nimport {\n  schemasInitialState,\n  schemaVersion,\n  schemaVersionWithNonAsciiChars,\n} from 'redux/reducers/schemas/__test__/fixtures';\nimport fetchMock from 'fetch-mock';\nimport ClusterContext, {\n  ContextProps,\n  initialValue as contextInitialValue,\n} from 'components/contexts/ClusterContext';\nimport { RootState } from 'redux/interfaces';\nimport { act } from '@testing-library/react';\n\nimport { versionPayload, versionEmptyPayload } from './fixtures';\n\nconst clusterName = 'testClusterName';\nconst schemasAPILatestUrl = `/api/clusters/${clusterName}/schemas/${schemaVersion.subject}/latest`;\nconst schemasAPIVersionsUrl = `/api/clusters/${clusterName}/schemas/${schemaVersion.subject}/versions`;\n\nconst mockHistoryPush = jest.fn();\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useNavigate: () => mockHistoryPush,\n}));\n\nconst renderComponent = (\n  initialState: RootState['schemas'] = schemasInitialState,\n  context: ContextProps = contextInitialValue\n) =>\n  render(\n    <WithRoute path={clusterSchemaPath()}>\n      <ClusterContext.Provider value={context}>\n        <Details />\n      </ClusterContext.Provider>\n    </WithRoute>,\n    {\n      initialEntries: [clusterSchemaPath(clusterName, schemaVersion.subject)],\n      preloadedState: {\n        schemas: initialState,\n      },\n    }\n  );\n\ndescribe('Details', () => {\n  afterEach(() => fetchMock.reset());\n\n  describe('fetch failed', () => {\n    it('renders pageloader', async () => {\n      const schemasAPILatestMock = fetchMock.getOnce(schemasAPILatestUrl, 404);\n      const schemasAPIVersionsMock = fetchMock.getOnce(\n        schemasAPIVersionsUrl,\n        404\n      );\n      await act(() => {\n        renderComponent();\n      });\n      expect(schemasAPILatestMock.called(schemasAPILatestUrl)).toBeTruthy();\n      expect(schemasAPIVersionsMock.called(schemasAPIVersionsUrl)).toBeTruthy();\n      expect(screen.getByRole('progressbar')).toBeInTheDocument();\n      expect(screen.queryByText(schemaVersion.subject)).not.toBeInTheDocument();\n      expect(screen.queryByText('Edit Schema')).not.toBeInTheDocument();\n      expect(screen.queryByText('Remove Schema')).not.toBeInTheDocument();\n    });\n  });\n\n  describe('fetch success', () => {\n    describe('has schema versions', () => {\n      it('renders component with schema info', async () => {\n        const schemasAPILatestMock = fetchMock.getOnce(\n          schemasAPILatestUrl,\n          schemaVersion\n        );\n        const schemasAPIVersionsMock = fetchMock.getOnce(\n          schemasAPIVersionsUrl,\n          versionPayload\n        );\n        await act(() => {\n          renderComponent();\n        });\n        expect(schemasAPILatestMock.called()).toBeTruthy();\n        expect(schemasAPIVersionsMock.called()).toBeTruthy();\n        expect(screen.getByText('Edit Schema')).toBeInTheDocument();\n        expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();\n        expect(screen.getByRole('table')).toBeInTheDocument();\n      });\n    });\n\n    describe('fetch success schema with non ascii characters', () => {\n      describe('has schema versions', () => {\n        it('renders component with schema info', async () => {\n          const schemasAPILatestMock = fetchMock.getOnce(\n            schemasAPILatestUrl,\n            schemaVersionWithNonAsciiChars\n          );\n          const schemasAPIVersionsMock = fetchMock.getOnce(\n            schemasAPIVersionsUrl,\n            versionPayload\n          );\n          await act(() => {\n            renderComponent();\n          });\n          expect(schemasAPILatestMock.called()).toBeTruthy();\n          expect(schemasAPIVersionsMock.called()).toBeTruthy();\n          expect(screen.getByText('Edit Schema')).toBeInTheDocument();\n          expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();\n          expect(screen.getByRole('table')).toBeInTheDocument();\n        });\n      });\n    });\n\n    describe('empty schema versions', () => {\n      beforeEach(async () => {\n        const schemasAPILatestMock = fetchMock.getOnce(\n          schemasAPILatestUrl,\n          schemaVersion\n        );\n        const schemasAPIVersionsMock = fetchMock.getOnce(\n          schemasAPIVersionsUrl,\n          versionEmptyPayload\n        );\n        await act(() => {\n          renderComponent();\n        });\n        expect(schemasAPILatestMock.called()).toBeTruthy();\n        expect(schemasAPIVersionsMock.called()).toBeTruthy();\n      });\n\n      // seems like incorrect behaviour\n      it('renders versions table with 0 items', () => {\n        expect(screen.getByRole('table')).toBeInTheDocument();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/Details/__test__/LatestVersionItem.spec.tsx",
    "content": "import React from 'react';\nimport LatestVersionItem from 'components/Schemas/Details/LatestVersion/LatestVersionItem';\nimport { render } from 'lib/testHelpers';\nimport { screen } from '@testing-library/react';\n\nimport { jsonSchema, protoSchema } from './fixtures';\n\ndescribe('LatestVersionItem', () => {\n  it('renders latest version of json schema', () => {\n    render(<LatestVersionItem schema={jsonSchema} />);\n    expect(screen.getByText('Actual version')).toBeInTheDocument();\n    expect(screen.getByText('Latest version')).toBeInTheDocument();\n    expect(screen.getByText('ID')).toBeInTheDocument();\n    expect(screen.getByText('Subject')).toBeInTheDocument();\n    expect(screen.getByText('Compatibility')).toBeInTheDocument();\n    expect(screen.getByText('15')).toBeInTheDocument();\n  });\n\n  it('renders latest version of compatibility', () => {\n    render(<LatestVersionItem schema={protoSchema} />);\n    expect(screen.getByText('Actual version')).toBeInTheDocument();\n    expect(screen.getByText('Latest version')).toBeInTheDocument();\n    expect(screen.getByText('ID')).toBeInTheDocument();\n    expect(screen.getByText('Subject')).toBeInTheDocument();\n    expect(screen.getByText('Compatibility')).toBeInTheDocument();\n    expect(screen.getByText('BACKWARD')).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/Details/__test__/SchemaVersion.spec.tsx",
    "content": "import React from 'react';\nimport SchemaVersion from 'components/Schemas/Details/SchemaVersion/SchemaVersion';\nimport { render } from 'lib/testHelpers';\nimport { SchemaSubject } from 'generated-sources';\nimport { Row } from '@tanstack/react-table';\n\nimport { jsonSchema } from './fixtures';\n\nconst renderComponent = () => {\n  const row = {\n    original: jsonSchema,\n  };\n\n  return render(<SchemaVersion row={row as Row<SchemaSubject>} />);\n};\n\ndescribe('SchemaVersion', () => {\n  it('renders versions', async () => {\n    renderComponent();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/Details/__test__/fixtures.ts",
    "content": "import { SchemaSubject, SchemaType } from 'generated-sources';\nimport {\n  schemaVersion1,\n  schemaVersion2,\n  schemaVersionWithNonAsciiChars,\n} from 'redux/reducers/schemas/__test__/fixtures';\n\nexport const versionPayload = [\n  schemaVersion1,\n  schemaVersion2,\n  schemaVersionWithNonAsciiChars,\n];\nexport const versionEmptyPayload = [];\n\nexport const jsonSchema: SchemaSubject = {\n  subject: 'test',\n  version: '15',\n  id: 1,\n  schema:\n    '{\"type\":\"record\",\"name\":\"MyRecord1\",\"namespace\":\"com.mycompany\",\"fields\":[{\"name\":\"id\",\"type\":\"long\"}]}',\n  compatibilityLevel: 'BACKWARD',\n  schemaType: SchemaType.JSON,\n};\n\nexport const protoSchema: SchemaSubject = {\n  subject: 'test_proto',\n  version: '1',\n  id: 2,\n  schema:\n    'syntax = \"proto3\";\\npackage com.indeed;\\n\\nmessage MyRecord {\\n  int32 id = 1;\\n  string name = 2;\\n}\\n',\n  compatibilityLevel: 'BACKWARD',\n  schemaType: SchemaType.PROTOBUF,\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/Diff/Diff.styled.ts",
    "content": "import styled from 'styled-components';\nimport { Button } from 'components/common/Button/Button';\n\nexport const DiffWrapper = styled.div`\n  align-items: stretch;\n  display: block;\n  flex-basis: 0;\n  flex-grow: 1;\n  flex-shrink: 1;\n  min-height: min-content;\n  padding-top: 1.5rem !important;\n\n  .ace_content {\n    background-color: ${({ theme }) => theme.default.backgroundColor};\n    color: ${({ theme }) => theme.default.color.normal};\n  }\n  .ace_gutter-cell {\n    background-color: ${({ theme }) =>\n      theme.ksqlDb.query.editor.cell.backgroundColor};\n  }\n  .ace_gutter-layer {\n    background-color: ${({ theme }) =>\n      theme.ksqlDb.query.editor.layer.backgroundColor};\n    color: ${({ theme }) => theme.default.color.normal};\n  }\n  .ace_cursor {\n    color: ${({ theme }) => theme.ksqlDb.query.editor.cursor};\n  }\n\n  .ace_print-margin {\n    display: none;\n  }\n  .ace_variable {\n    color: ${({ theme }) => theme.ksqlDb.query.editor.variable};\n  }\n  .ace_string {\n    color: ${({ theme }) => theme.ksqlDb.query.editor.aceString};\n  }\n  .codeMarker {\n    background-color: ${({ theme }) => theme.ksqlDb.query.editor.codeMarker};\n    position: absolute;\n    z-index: 2000;\n  }\n`;\n\nexport const Section = styled.div`\n  animation: fadein 0.5s;\n`;\n\nexport const DiffBox = styled.div`\n  flex-direction: column;\n  margin-left: -0.75rem;\n  margin-right: -0.75rem;\n  margin-top: -0.75rem;\n  box-shadow: none;\n  padding: 1.25rem;\n  &:last-child {\n    margin-bottom: -0.75rem;\n  }\n`;\n\nexport const DiffTilesWrapper = styled.div`\n  align-items: stretch;\n  display: block;\n  flex-basis: 0;\n  flex-grow: 1;\n  flex-shrink: 1;\n  min-height: min-content;\n  &:not(.is-child) {\n    display: flex;\n  }\n`;\n\nexport const DiffTile = styled.div`\n  flex: none;\n  width: 50%;\n`;\n\nexport const DiffVersionsSelect = styled.div`\n  width: 0.625em;\n`;\nexport const BackButton = styled(Button)`\n  margin: 10px 9px;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/Diff/Diff.tsx",
    "content": "import React from 'react';\nimport { SchemaSubject } from 'generated-sources';\nimport {\n  clusterSchemaComparePath,\n  clusterSchemasPath,\n  ClusterSubjectParam,\n} from 'lib/paths';\nimport PageLoader from 'components/common/PageLoader/PageLoader';\nimport DiffViewer from 'components/common/DiffViewer/DiffViewer';\nimport { useNavigate, useLocation } from 'react-router-dom';\nimport {\n  fetchSchemaVersions,\n  SCHEMAS_VERSIONS_FETCH_ACTION,\n} from 'redux/reducers/schemas/schemasSlice';\nimport { useForm, Controller } from 'react-hook-form';\nimport Select from 'components/common/Select/Select';\nimport { useAppDispatch } from 'lib/hooks/redux';\nimport { resetLoaderById } from 'redux/reducers/loader/loaderSlice';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport PageHeading from 'components/common/PageHeading/PageHeading';\n\nimport * as S from './Diff.styled';\nimport { BackButton } from './Diff.styled';\n\nexport interface DiffProps {\n  versions: SchemaSubject[];\n  areVersionsFetched: boolean;\n}\n\nconst Diff: React.FC<DiffProps> = ({ versions, areVersionsFetched }) => {\n  const { clusterName, subject } = useAppParams<ClusterSubjectParam>();\n  const navigate = useNavigate();\n  const location = useLocation();\n\n  const searchParams = React.useMemo(\n    () => new URLSearchParams(location.search),\n    [location]\n  );\n\n  const [leftVersion, setLeftVersion] = React.useState(\n    searchParams.get('leftVersion') || ''\n  );\n  const [rightVersion, setRightVersion] = React.useState(\n    searchParams.get('rightVersion') || ''\n  );\n\n  const dispatch = useAppDispatch();\n\n  React.useEffect(() => {\n    dispatch(fetchSchemaVersions({ clusterName, subject }));\n    return () => {\n      dispatch(resetLoaderById(SCHEMAS_VERSIONS_FETCH_ACTION));\n    };\n  }, [clusterName, subject, dispatch]);\n\n  const getSchemaContent = (allVersions: SchemaSubject[], version: string) => {\n    const selectedSchema =\n      allVersions.find((s) => s.version === version)?.schema ||\n      (allVersions.length ? allVersions[0].schema : '');\n    return selectedSchema.trim().startsWith('{')\n      ? JSON.stringify(JSON.parse(selectedSchema), null, '\\t')\n      : selectedSchema;\n  };\n  const getSchemaType = (allVersions: SchemaSubject[]) => {\n    return allVersions[0].schemaType;\n  };\n\n  const methods = useForm({ mode: 'onChange' });\n  const {\n    formState: { isSubmitting },\n    control,\n  } = methods;\n\n  return (\n    <>\n      <PageHeading\n        text={`${subject} compare versions`}\n        backText=\"Schema Registry\"\n        backTo={clusterSchemasPath(clusterName)}\n      />\n      <BackButton\n        buttonType=\"secondary\"\n        buttonSize=\"S\"\n        onClick={() => navigate(-1)}\n      >\n        Back\n      </BackButton>\n      <S.Section>\n        {areVersionsFetched ? (\n          <S.DiffBox>\n            <S.DiffTilesWrapper>\n              <S.DiffTile>\n                <S.DiffVersionsSelect>\n                  <Controller\n                    defaultValue={leftVersion}\n                    control={control}\n                    rules={{ required: true }}\n                    name=\"schemaType\"\n                    render={({ field: { name } }) => (\n                      <Select\n                        id=\"left-select\"\n                        name={name}\n                        value={\n                          leftVersion === '' ? versions[0].version : leftVersion\n                        }\n                        onChange={(event) => {\n                          navigate(\n                            clusterSchemaComparePath(clusterName, subject)\n                          );\n                          searchParams.set('leftVersion', event.toString());\n                          searchParams.set(\n                            'rightVersion',\n                            rightVersion === ''\n                              ? versions[0].version\n                              : rightVersion\n                          );\n                          navigate({\n                            search: `?${searchParams.toString()}`,\n                          });\n                          setLeftVersion(event.toString());\n                        }}\n                        minWidth=\"100%\"\n                        disabled={isSubmitting}\n                        options={versions.map((type) => ({\n                          value: type.version,\n                          label: `Version ${type.version}`,\n                        }))}\n                      />\n                    )}\n                  />\n                </S.DiffVersionsSelect>\n              </S.DiffTile>\n              <S.DiffTile>\n                <S.DiffVersionsSelect>\n                  <Controller\n                    defaultValue={rightVersion}\n                    control={control}\n                    rules={{ required: true }}\n                    name=\"schemaType\"\n                    render={({ field: { name } }) => (\n                      <Select\n                        id=\"right-select\"\n                        name={name}\n                        value={\n                          rightVersion === ''\n                            ? versions[0].version\n                            : rightVersion\n                        }\n                        onChange={(event) => {\n                          navigate(\n                            clusterSchemaComparePath(clusterName, subject)\n                          );\n                          searchParams.set(\n                            'leftVersion',\n                            leftVersion === ''\n                              ? versions[0].version\n                              : leftVersion\n                          );\n                          searchParams.set('rightVersion', event.toString());\n                          navigate({\n                            search: `?${searchParams.toString()}`,\n                          });\n                          setRightVersion(event.toString());\n                        }}\n                        minWidth=\"100%\"\n                        disabled={isSubmitting}\n                        options={versions.map((type) => ({\n                          value: type.version,\n                          label: `Version ${type.version}`,\n                        }))}\n                      />\n                    )}\n                  />\n                </S.DiffVersionsSelect>\n              </S.DiffTile>\n            </S.DiffTilesWrapper>\n            <S.DiffWrapper>\n              <DiffViewer\n                value={[\n                  getSchemaContent(versions, leftVersion),\n                  getSchemaContent(versions, rightVersion),\n                ]}\n                setOptions={{\n                  autoScrollEditorIntoView: true,\n                }}\n                isFixedHeight={false}\n                schemaType={getSchemaType(versions)}\n              />\n            </S.DiffWrapper>\n          </S.DiffBox>\n        ) : (\n          <PageLoader />\n        )}\n      </S.Section>\n    </>\n  );\n};\n\nexport default Diff;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/Diff/DiffContainer.ts",
    "content": "import { connect } from 'react-redux';\nimport { RootState } from 'redux/interfaces';\nimport {\n  getAreSchemaVersionsFulfilled,\n  selectAllSchemaVersions,\n} from 'redux/reducers/schemas/schemasSlice';\n\nimport Diff from './Diff';\n\nconst mapStateToProps = (state: RootState) => ({\n  versions: selectAllSchemaVersions(state),\n  areVersionsFetched: getAreSchemaVersionsFulfilled(state),\n});\n\nexport default connect(mapStateToProps)(Diff);\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/Diff/__test__/Diff.spec.tsx",
    "content": "import React from 'react';\nimport Diff, { DiffProps } from 'components/Schemas/Diff/Diff';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { screen } from '@testing-library/react';\nimport { clusterSchemaComparePath } from 'lib/paths';\nimport userEvent from '@testing-library/user-event';\n\nimport { versions } from './fixtures';\n\nconst defaultClusterName = 'defaultClusterName';\nconst defaultSubject = 'defaultSubject';\nconst defaultPathName = clusterSchemaComparePath(\n  defaultClusterName,\n  defaultSubject\n);\n\ndescribe('Diff', () => {\n  const setupComponent = (\n    props: DiffProps,\n    searchQuery: { rightVersion?: string; leftVersion?: string } = {}\n  ) => {\n    let pathname = defaultPathName;\n    const searchParams = new URLSearchParams(pathname);\n    if (searchQuery.rightVersion) {\n      searchParams.set('rightVersion', searchQuery.rightVersion);\n    }\n    if (searchQuery.leftVersion) {\n      searchParams.set('leftVersion', searchQuery.leftVersion);\n    }\n\n    pathname = `${pathname}?${searchParams.toString()}`;\n\n    return render(\n      <WithRoute path={clusterSchemaComparePath()}>\n        <Diff\n          versions={props.versions}\n          areVersionsFetched={props.areVersionsFetched}\n        />\n      </WithRoute>,\n      {\n        initialEntries: [pathname],\n      }\n    );\n  };\n\n  describe('Container', () => {\n    it('renders view', () => {\n      setupComponent({\n        areVersionsFetched: true,\n        versions,\n      });\n      expect(screen.getAllByText('Version 3').length).toEqual(4);\n    });\n  });\n\n  describe('View', () => {\n    setupComponent({\n      areVersionsFetched: true,\n      versions,\n    });\n  });\n  describe('when page with schema versions is loading', () => {\n    beforeAll(() => {\n      setupComponent({\n        areVersionsFetched: false,\n        versions: [],\n      });\n    });\n    it('renders PageLoader', () => {\n      expect(screen.getByRole('progressbar')).toBeInTheDocument();\n    });\n  });\n\n  describe('when schema versions are loaded and no specified versions in path', () => {\n    beforeEach(() => {\n      setupComponent({\n        areVersionsFetched: true,\n        versions,\n      });\n    });\n\n    it('renders all options', () => {\n      expect(screen.getAllByRole('option').length).toEqual(2);\n    });\n    it('renders left select with empty value', () => {\n      const select = screen.getAllByRole('listbox')[0];\n      expect(select).toBeInTheDocument();\n      expect(select).toHaveTextContent(versions[0].version);\n    });\n\n    it('renders right select with empty value', () => {\n      const select = screen.getAllByRole('listbox')[1];\n      expect(select).toBeInTheDocument();\n      expect(select).toHaveTextContent(versions[0].version);\n    });\n  });\n  describe('when schema versions are loaded and two versions in path', () => {\n    beforeEach(() => {\n      setupComponent(\n        {\n          areVersionsFetched: true,\n          versions,\n        },\n        { leftVersion: '1', rightVersion: '2' }\n      );\n    });\n\n    it('renders left select with version 1', () => {\n      const select = screen.getAllByRole('listbox')[0];\n      expect(select).toBeInTheDocument();\n      expect(select).toHaveTextContent('1');\n    });\n\n    it('renders right select with version 2', () => {\n      const select = screen.getAllByRole('listbox')[1];\n      expect(select).toBeInTheDocument();\n      expect(select).toHaveTextContent('2');\n    });\n  });\n\n  describe('when schema versions are loaded and only one versions in path', () => {\n    beforeEach(() => {\n      setupComponent(\n        {\n          areVersionsFetched: true,\n          versions,\n        },\n        {\n          leftVersion: '1',\n        }\n      );\n    });\n\n    it('renders left select with version 1', () => {\n      const select = screen.getAllByRole('listbox')[0];\n      expect(select).toBeInTheDocument();\n      expect(select).toHaveTextContent('1');\n    });\n\n    it('renders right select with empty value', () => {\n      const select = screen.getAllByRole('listbox')[1];\n      expect(select).toBeInTheDocument();\n      expect(select).toHaveTextContent(versions[0].version);\n    });\n  });\n\n  describe('Back button', () => {\n    beforeEach(() => {\n      setupComponent({\n        areVersionsFetched: true,\n        versions,\n      });\n    });\n\n    it('back button is appear', () => {\n      const backButton = screen.getAllByRole('button', { name: 'Back' });\n      expect(backButton[0]).toBeInTheDocument();\n    });\n\n    it('click on back button', () => {\n      const backButton = screen.getAllByRole('button', { name: 'Back' });\n      userEvent.click(backButton[0]);\n      expect(screen.queryByRole('Back')).not.toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/Diff/__test__/fixtures.ts",
    "content": "import { SchemaSubject, SchemaType } from 'generated-sources';\n\nexport const versions: SchemaSubject[] = [\n  {\n    subject: 'test',\n    version: '3',\n    id: 3,\n    schema:\n      'syntax = \"proto3\";\\npackage com.indeed;\\n\\nmessage MyRecord {\\n  int32 id = 1;\\n  string name = 2;\\n}\\n',\n    compatibilityLevel: 'BACKWARD',\n    schemaType: SchemaType.PROTOBUF,\n  },\n  {\n    subject: 'test',\n    version: '2',\n    id: 2,\n    schema:\n      '{\"type\":\"record\",\"name\":\"MyRecord2\",\"namespace\":\"com.mycompany\",\"fields\":[{\"name\":\"id\",\"type\":\"long\"}]}',\n    compatibilityLevel: 'BACKWARD',\n    schemaType: SchemaType.JSON,\n  },\n  {\n    subject: 'test',\n    version: '1',\n    id: 1,\n    schema:\n      '{\"type\":\"record\",\"name\":\"MyRecord1\",\"namespace\":\"com.mycompany\",\"fields\":[{\"name\":\"id\",\"type\":\"long\"}]}',\n    compatibilityLevel: 'BACKWARD',\n    schemaType: SchemaType.JSON,\n  },\n];\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/Edit/Edit.styled.ts",
    "content": "import styled, { css } from 'styled-components';\n\nexport const EditWrapper = styled.div`\n  padding: 16px;\n  padding-top: 0;\n  & > form {\n    display: flex;\n    flex-direction: column;\n    gap: 16px;\n\n    & > div:first-child {\n      display: flex;\n      gap: 16px;\n\n      & > * {\n        width: 20%;\n      }\n    }\n\n    & > button:last-child {\n      width: 72px;\n      align-self: center;\n    }\n  }\n`;\n\nexport const EditorsWrapper = styled.div`\n  display: flex;\n  gap: 16px;\n\n  & > * {\n    flex-grow: 1;\n  }\n`;\n\nexport const EditorContainer = styled.div(\n  ({ theme }) => css`\n    border: 1px solid ${theme.layout.stuffBorderColor};\n    border-radius: 8px;\n    margin-bottom: 16px;\n    padding: 16px;\n    & > h4 {\n      font-weight: 500;\n      font-size: 16px;\n      line-height: 24px;\n      padding-bottom: 16px;\n      color: ${theme.heading.h4};\n    }\n  `\n);\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx",
    "content": "import React from 'react';\nimport { ClusterSubjectParam } from 'lib/paths';\nimport { useAppDispatch, useAppSelector } from 'lib/hooks/redux';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport {\n  fetchLatestSchema,\n  getSchemaLatest,\n  SCHEMA_LATEST_FETCH_ACTION,\n  getAreSchemaLatestFulfilled,\n} from 'redux/reducers/schemas/schemasSlice';\nimport PageLoader from 'components/common/PageLoader/PageLoader';\nimport { resetLoaderById } from 'redux/reducers/loader/loaderSlice';\n\nimport Form from './Form';\n\nconst Edit: React.FC = () => {\n  const dispatch = useAppDispatch();\n\n  const { clusterName, subject } = useAppParams<ClusterSubjectParam>();\n\n  React.useEffect(() => {\n    dispatch(fetchLatestSchema({ clusterName, subject }));\n    return () => {\n      dispatch(resetLoaderById(SCHEMA_LATEST_FETCH_ACTION));\n    };\n  }, [clusterName, dispatch, subject]);\n\n  const schema = useAppSelector((state) => getSchemaLatest(state));\n  const isFetched = useAppSelector(getAreSchemaLatestFulfilled);\n\n  if (!isFetched || !schema) {\n    return <PageLoader />;\n  }\n  return <Form />;\n};\n\nexport default Edit;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/Edit/Form.tsx",
    "content": "import React from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { useForm, Controller, FormProvider } from 'react-hook-form';\nimport {\n  CompatibilityLevelCompatibilityEnum,\n  SchemaType,\n} from 'generated-sources';\nimport {\n  clusterSchemaPath,\n  clusterSchemasPath,\n  ClusterSubjectParam,\n} from 'lib/paths';\nimport yup from 'lib/yupExtended';\nimport { NewSchemaSubjectRaw } from 'redux/interfaces';\nimport Editor from 'components/common/Editor/Editor';\nimport Select from 'components/common/Select/Select';\nimport { Button } from 'components/common/Button/Button';\nimport { InputLabel } from 'components/common/Input/InputLabel.styled';\nimport PageHeading from 'components/common/PageHeading/PageHeading';\nimport { useAppDispatch, useAppSelector } from 'lib/hooks/redux';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport {\n  schemaAdded,\n  getSchemaLatest,\n  getAreSchemaLatestFulfilled,\n  schemaUpdated,\n  getAreSchemaLatestRejected,\n} from 'redux/reducers/schemas/schemasSlice';\nimport PageLoader from 'components/common/PageLoader/PageLoader';\nimport { schemasApiClient } from 'lib/api';\nimport { showServerError } from 'lib/errorHandling';\nimport { yupResolver } from '@hookform/resolvers/yup';\nimport { FormError } from 'components/common/Input/Input.styled';\nimport { ErrorMessage } from '@hookform/error-message';\n\nimport * as S from './Edit.styled';\n\nconst Form: React.FC = () => {\n  const navigate = useNavigate();\n  const dispatch = useAppDispatch();\n\n  const { clusterName, subject } = useAppParams<ClusterSubjectParam>();\n\n  const schema = useAppSelector((state) => getSchemaLatest(state));\n  const isFetched = useAppSelector(getAreSchemaLatestFulfilled);\n  const isRejected = useAppSelector(getAreSchemaLatestRejected);\n\n  const formatedSchema = React.useMemo(() => {\n    return schema?.schemaType === SchemaType.PROTOBUF\n      ? schema?.schema\n      : JSON.stringify(JSON.parse(schema?.schema || '{}'), null, '\\t');\n  }, [schema]);\n\n  const validationSchema = () =>\n    yup.object().shape({\n      newSchema:\n        schema?.schemaType === SchemaType.PROTOBUF\n          ? yup.string().required()\n          : yup.string().required().isJsonObject('Schema syntax is not valid'),\n    });\n  const methods = useForm<NewSchemaSubjectRaw>({\n    mode: 'onChange',\n    resolver: yupResolver(validationSchema()),\n    defaultValues: {\n      schemaType: schema?.schemaType,\n      compatibilityLevel:\n        schema?.compatibilityLevel as CompatibilityLevelCompatibilityEnum,\n      newSchema: formatedSchema,\n    },\n  });\n\n  const {\n    formState: { isDirty, isSubmitting, dirtyFields, errors },\n    control,\n    handleSubmit,\n  } = methods;\n  const onSubmit = async (props: NewSchemaSubjectRaw) => {\n    if (!schema) return;\n\n    try {\n      if (dirtyFields.compatibilityLevel) {\n        await schemasApiClient.updateSchemaCompatibilityLevel({\n          clusterName,\n          subject,\n          compatibilityLevel: {\n            compatibility: props.compatibilityLevel,\n          },\n        });\n        dispatch(\n          schemaUpdated({\n            ...schema,\n            compatibilityLevel: props.compatibilityLevel,\n          })\n        );\n      }\n      if (dirtyFields.newSchema || dirtyFields.schemaType) {\n        const resp = await schemasApiClient.createNewSchema({\n          clusterName,\n          newSchemaSubject: {\n            ...schema,\n            schema: props.newSchema || schema.schema,\n            schemaType: props.schemaType || schema.schemaType,\n          },\n        });\n        dispatch(schemaAdded(resp));\n      }\n\n      navigate(clusterSchemaPath(clusterName, subject));\n    } catch (e) {\n      showServerError(e as Response);\n    }\n  };\n\n  if (isRejected) {\n    navigate('/404');\n  }\n\n  if (!isFetched || !schema) {\n    return <PageLoader />;\n  }\n  return (\n    <FormProvider {...methods}>\n      <PageHeading\n        text={`${subject} Edit`}\n        backText=\"Schema Registry\"\n        backTo={clusterSchemasPath(clusterName)}\n      />\n      <S.EditWrapper>\n        <form onSubmit={handleSubmit(onSubmit)}>\n          <div>\n            <div>\n              <InputLabel>Type</InputLabel>\n              <Controller\n                control={control}\n                rules={{ required: true }}\n                name=\"schemaType\"\n                render={({ field: { name, onChange, value } }) => (\n                  <Select\n                    name={name}\n                    value={value}\n                    onChange={onChange}\n                    minWidth=\"100%\"\n                    disabled\n                    options={Object.keys(SchemaType).map((type) => ({\n                      value: type,\n                      label: type,\n                    }))}\n                  />\n                )}\n              />\n            </div>\n\n            <div>\n              <InputLabel>Compatibility level</InputLabel>\n              <Controller\n                control={control}\n                name=\"compatibilityLevel\"\n                render={({ field: { name, onChange, value } }) => (\n                  <Select\n                    name={name}\n                    value={value}\n                    onChange={onChange}\n                    minWidth=\"100%\"\n                    disabled={isSubmitting}\n                    options={Object.keys(\n                      CompatibilityLevelCompatibilityEnum\n                    ).map((level) => ({ value: level, label: level }))}\n                  />\n                )}\n              />\n            </div>\n          </div>\n          <S.EditorsWrapper>\n            <div>\n              <S.EditorContainer>\n                <h4>Latest schema</h4>\n                <Editor\n                  schemaType={schema?.schemaType}\n                  isFixedHeight\n                  readOnly\n                  height=\"372px\"\n                  value={formatedSchema}\n                  name=\"latestSchema\"\n                  highlightActiveLine={false}\n                />\n              </S.EditorContainer>\n            </div>\n            <div>\n              <S.EditorContainer>\n                <h4>New schema</h4>\n                <Controller\n                  control={control}\n                  name=\"newSchema\"\n                  render={({ field: { name, onChange, value } }) => (\n                    <Editor\n                      schemaType={schema?.schemaType}\n                      readOnly={isSubmitting}\n                      defaultValue={value}\n                      name={name}\n                      onChange={onChange}\n                    />\n                  )}\n                />\n              </S.EditorContainer>\n              <FormError>\n                <ErrorMessage errors={errors} name=\"newSchema\" />\n              </FormError>\n              <Button\n                buttonType=\"primary\"\n                buttonSize=\"M\"\n                type=\"submit\"\n                disabled={!isDirty || isSubmitting || !!errors.newSchema}\n              >\n                Submit\n              </Button>\n            </div>\n          </S.EditorsWrapper>\n        </form>\n      </S.EditWrapper>\n    </FormProvider>\n  );\n};\n\nexport default Form;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/Edit/__tests__/Edit.spec.tsx",
    "content": "import React from 'react';\nimport Edit from 'components/Schemas/Edit/Edit';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { clusterSchemaEditPath } from 'lib/paths';\nimport {\n  schemasInitialState,\n  schemaVersion,\n  schemaVersionWithNonAsciiChars,\n} from 'redux/reducers/schemas/__test__/fixtures';\nimport { screen } from '@testing-library/dom';\nimport ClusterContext, {\n  ContextProps,\n  initialValue as contextInitialValue,\n} from 'components/contexts/ClusterContext';\nimport { RootState } from 'redux/interfaces';\nimport fetchMock from 'fetch-mock';\nimport { act } from '@testing-library/react';\n\nconst clusterName = 'testClusterName';\nconst schemasAPILatestUrl = `/api/clusters/${clusterName}/schemas/${schemaVersion.subject}/latest`;\n\nconst renderComponent = (\n  initialState: RootState['schemas'] = schemasInitialState,\n  context: ContextProps = contextInitialValue\n) =>\n  render(\n    <WithRoute path={clusterSchemaEditPath()}>\n      <ClusterContext.Provider value={context}>\n        <Edit />\n      </ClusterContext.Provider>\n    </WithRoute>,\n    {\n      initialEntries: [\n        clusterSchemaEditPath(clusterName, schemaVersion.subject),\n      ],\n      preloadedState: {\n        schemas: initialState,\n      },\n    }\n  );\n\ndescribe('Edit', () => {\n  afterEach(() => fetchMock.reset());\n\n  describe('fetch success', () => {\n    describe('has schema versions', () => {\n      it('renders component with schema info', async () => {\n        fetchMock.getOnce(schemasAPILatestUrl, schemaVersion);\n        await act(() => {\n          renderComponent();\n        });\n        expect(fetchMock.called(schemasAPILatestUrl)).toBeTruthy();\n        expect(screen.getByText('Submit')).toBeInTheDocument();\n        expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();\n      });\n    });\n  });\n\n  describe('fetch success schema with non ascii characters', () => {\n    describe('has schema versions', () => {\n      it('renders component with schema info', async () => {\n        fetchMock.getOnce(schemasAPILatestUrl, schemaVersionWithNonAsciiChars);\n        await act(() => {\n          renderComponent();\n        });\n        expect(fetchMock.called(schemasAPILatestUrl)).toBeTruthy();\n        expect(screen.getByText('Submit')).toBeInTheDocument();\n        expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const Wrapper = styled.div`\n  display: flex;\n  gap: 5px;\n  align-items: center;\n  & > div {\n    color: ${({ theme }) => theme.select.label};\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.tsx",
    "content": "import React from 'react';\nimport {\n  Action,\n  CompatibilityLevelCompatibilityEnum,\n  ResourceType,\n} from 'generated-sources';\nimport { useAppDispatch } from 'lib/hooks/redux';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { fetchSchemas } from 'redux/reducers/schemas/schemasSlice';\nimport { ClusterNameRoute } from 'lib/paths';\nimport { schemasApiClient } from 'lib/api';\nimport { showServerError } from 'lib/errorHandling';\nimport { useConfirm } from 'lib/hooks/useConfirm';\nimport { useSearchParams } from 'react-router-dom';\nimport { PER_PAGE } from 'lib/constants';\nimport { ActionSelect } from 'components/common/ActionComponent';\n\nimport * as S from './GlobalSchemaSelector.styled';\n\nconst GlobalSchemaSelector: React.FC = () => {\n  const { clusterName } = useAppParams<ClusterNameRoute>();\n  const dispatch = useAppDispatch();\n  const [searchParams] = useSearchParams();\n  const confirm = useConfirm();\n\n  const [currentCompatibilityLevel, setCurrentCompatibilityLevel] =\n    React.useState<CompatibilityLevelCompatibilityEnum | undefined>();\n\n  const [isFetching, setIsFetching] = React.useState(false);\n\n  React.useEffect(() => {\n    const fetchData = async () => {\n      setIsFetching(true);\n      try {\n        const { compatibility } =\n          await schemasApiClient.getGlobalSchemaCompatibilityLevel({\n            clusterName,\n          });\n        setCurrentCompatibilityLevel(compatibility);\n      } catch (error) {\n        // do nothing\n      }\n      setIsFetching(false);\n    };\n\n    fetchData();\n  }, [clusterName]);\n\n  const handleChangeCompatibilityLevel = (level: string | number) => {\n    const nextLevel = level as CompatibilityLevelCompatibilityEnum;\n    confirm(\n      <>\n        Are you sure you want to update the global compatibility level and set\n        it to <b>{nextLevel}</b>? This may affect the compatibility levels of\n        the schemas.\n      </>,\n      async () => {\n        try {\n          await schemasApiClient.updateGlobalSchemaCompatibilityLevel({\n            clusterName,\n            compatibilityLevel: {\n              compatibility: nextLevel,\n            },\n          });\n          setCurrentCompatibilityLevel(nextLevel);\n          dispatch(\n            fetchSchemas({\n              clusterName,\n              page: Number(searchParams.get('page') || 1),\n              perPage: Number(searchParams.get('perPage') || PER_PAGE),\n              search: searchParams.get('q') || '',\n            })\n          );\n        } catch (e) {\n          showServerError(e as Response);\n        }\n      }\n    );\n  };\n\n  if (!currentCompatibilityLevel) return null;\n\n  return (\n    <S.Wrapper>\n      <div>Global Compatibility Level: </div>\n      <ActionSelect\n        selectSize=\"M\"\n        defaultValue={currentCompatibilityLevel}\n        minWidth=\"200px\"\n        onChange={handleChangeCompatibilityLevel}\n        disabled={isFetching}\n        options={Object.keys(CompatibilityLevelCompatibilityEnum).map(\n          (level) => ({ value: level, label: level })\n        )}\n        permission={{\n          resource: ResourceType.SCHEMA,\n          action: Action.MODIFY_GLOBAL_COMPATIBILITY,\n        }}\n      />\n    </S.Wrapper>\n  );\n};\n\nexport default GlobalSchemaSelector;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/__test__/GlobalSchemaSelector.spec.tsx",
    "content": "import React from 'react';\nimport { act, screen, waitFor, within } from '@testing-library/react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { CompatibilityLevelCompatibilityEnum } from 'generated-sources';\nimport GlobalSchemaSelector from 'components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector';\nimport userEvent from '@testing-library/user-event';\nimport { clusterSchemasPath } from 'lib/paths';\nimport fetchMock from 'fetch-mock';\n\nconst clusterName = 'testClusterName';\n\nconst selectForwardOption = async () => {\n  const dropdownElement = screen.getByRole('listbox');\n  // clicks to open dropdown\n  await userEvent.click(within(dropdownElement).getByRole('option'));\n  await userEvent.click(\n    screen.getByText(CompatibilityLevelCompatibilityEnum.FORWARD)\n  );\n};\n\nconst expectOptionIsSelected = (option: string) => {\n  const dropdownElement = screen.getByRole('listbox');\n  const selectedOption = within(dropdownElement).getAllByRole('option');\n  expect(selectedOption.length).toEqual(1);\n  expect(selectedOption[0]).toHaveTextContent(option);\n};\n\ndescribe('GlobalSchemaSelector', () => {\n  const renderComponent = () =>\n    render(\n      <WithRoute path={clusterSchemasPath()}>\n        <GlobalSchemaSelector />\n      </WithRoute>,\n      {\n        initialEntries: [clusterSchemasPath(clusterName)],\n      }\n    );\n\n  beforeEach(async () => {\n    const fetchGlobalCompatibilityLevelMock = fetchMock.getOnce(\n      `api/clusters/${clusterName}/schemas/compatibility`,\n      { compatibility: CompatibilityLevelCompatibilityEnum.FULL }\n    );\n    await act(() => {\n      renderComponent();\n    });\n    await waitFor(() =>\n      expect(fetchGlobalCompatibilityLevelMock.called()).toBeTruthy()\n    );\n  });\n\n  afterEach(() => {\n    fetchMock.reset();\n  });\n\n  it('renders with initial prop', () => {\n    expectOptionIsSelected(CompatibilityLevelCompatibilityEnum.FULL);\n  });\n\n  it('shows popup when select value is changed', async () => {\n    expectOptionIsSelected(CompatibilityLevelCompatibilityEnum.FULL);\n    await selectForwardOption();\n    expect(screen.getByText('Confirm the action')).toBeInTheDocument();\n  });\n\n  it('resets select value when cancel is clicked', async () => {\n    await selectForwardOption();\n    await userEvent.click(screen.getByText('Cancel'));\n    expect(screen.queryByText('Confirm the action')).not.toBeInTheDocument();\n    expectOptionIsSelected(CompatibilityLevelCompatibilityEnum.FULL);\n  });\n\n  it('sets new schema when confirm is clicked', async () => {\n    await selectForwardOption();\n    const putNewCompatibilityMock = fetchMock.putOnce(\n      `api/clusters/${clusterName}/schemas/compatibility`,\n      200,\n      {\n        body: {\n          compatibility: CompatibilityLevelCompatibilityEnum.FORWARD,\n        },\n      }\n    );\n    const getSchemasMock = fetchMock.getOnce(\n      `api/clusters/${clusterName}/schemas?page=1&perPage=25`,\n      200\n    );\n    await waitFor(() => {\n      userEvent.click(screen.getByRole('button', { name: 'Confirm' }));\n    });\n    await waitFor(() => expect(putNewCompatibilityMock.called()).toBeTruthy());\n    await waitFor(() => expect(getSchemasMock.called()).toBeTruthy());\n\n    await waitFor(() =>\n      expect(screen.queryByText('Confirm the action')).not.toBeInTheDocument()\n    );\n\n    await waitFor(() =>\n      expectOptionIsSelected(CompatibilityLevelCompatibilityEnum.FORWARD)\n    );\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/List/List.tsx",
    "content": "import React from 'react';\nimport {\n  ClusterNameRoute,\n  clusterSchemaNewRelativePath,\n  clusterSchemaPath,\n} from 'lib/paths';\nimport ClusterContext from 'components/contexts/ClusterContext';\nimport { ActionButton } from 'components/common/ActionComponent';\nimport PageHeading from 'components/common/PageHeading/PageHeading';\nimport { useAppDispatch, useAppSelector } from 'lib/hooks/redux';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport {\n  selectAllSchemas,\n  fetchSchemas,\n  getAreSchemasFulfilled,\n  SCHEMAS_FETCH_ACTION,\n} from 'redux/reducers/schemas/schemasSlice';\nimport PageLoader from 'components/common/PageLoader/PageLoader';\nimport { resetLoaderById } from 'redux/reducers/loader/loaderSlice';\nimport { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';\nimport Search from 'components/common/Search/Search';\nimport PlusIcon from 'components/common/Icons/PlusIcon';\nimport Table, { LinkCell } from 'components/common/NewTable';\nimport { ColumnDef } from '@tanstack/react-table';\nimport { Action, SchemaSubject, ResourceType } from 'generated-sources';\nimport { useNavigate, useSearchParams } from 'react-router-dom';\nimport { PER_PAGE } from 'lib/constants';\n\nimport GlobalSchemaSelector from './GlobalSchemaSelector/GlobalSchemaSelector';\n\nconst List: React.FC = () => {\n  const dispatch = useAppDispatch();\n  const { isReadOnly } = React.useContext(ClusterContext);\n  const { clusterName } = useAppParams<ClusterNameRoute>();\n  const navigate = useNavigate();\n  const schemas = useAppSelector(selectAllSchemas);\n  const isFetched = useAppSelector(getAreSchemasFulfilled);\n  const totalPages = useAppSelector((state) => state.schemas.totalPages);\n  const [searchParams] = useSearchParams();\n\n  React.useEffect(() => {\n    dispatch(\n      fetchSchemas({\n        clusterName,\n        page: Number(searchParams.get('page') || 1),\n        perPage: Number(searchParams.get('perPage') || PER_PAGE),\n        search: searchParams.get('q') || '',\n      })\n    );\n    return () => {\n      dispatch(resetLoaderById(SCHEMAS_FETCH_ACTION));\n    };\n  }, [clusterName, dispatch, searchParams]);\n\n  const columns = React.useMemo<ColumnDef<SchemaSubject>[]>(\n    () => [\n      {\n        header: 'Subject',\n        accessorKey: 'subject',\n        // eslint-disable-next-line react/no-unstable-nested-components\n        cell: ({ getValue }) => (\n          <LinkCell\n            value={`${getValue<string | number>()}`}\n            to={encodeURIComponent(`${getValue<string | number>()}`)}\n          />\n        ),\n      },\n      { header: 'Id', accessorKey: 'id' },\n      { header: 'Type', accessorKey: 'schemaType' },\n      { header: 'Version', accessorKey: 'version' },\n      { header: 'Compatibility', accessorKey: 'compatibilityLevel' },\n    ],\n    []\n  );\n\n  return (\n    <>\n      <PageHeading text=\"Schema Registry\">\n        {!isReadOnly && (\n          <>\n            <GlobalSchemaSelector />\n            <ActionButton\n              buttonSize=\"M\"\n              buttonType=\"primary\"\n              to={clusterSchemaNewRelativePath}\n              permission={{\n                resource: ResourceType.SCHEMA,\n                action: Action.CREATE,\n              }}\n            >\n              <PlusIcon /> Create Schema\n            </ActionButton>\n          </>\n        )}\n      </PageHeading>\n      <ControlPanelWrapper hasInput>\n        <Search placeholder=\"Search by Schema Name\" />\n      </ControlPanelWrapper>\n      {isFetched ? (\n        <Table\n          columns={columns}\n          data={schemas}\n          pageCount={totalPages}\n          emptyMessage=\"No schemas found\"\n          onRowClick={(row) =>\n            navigate(clusterSchemaPath(clusterName, row.original.subject))\n          }\n          serverSideProcessing\n        />\n      ) : (\n        <PageLoader />\n      )}\n    </>\n  );\n};\n\nexport default List;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/List/__test__/List.spec.tsx",
    "content": "import React from 'react';\nimport List from 'components/Schemas/List/List';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { clusterSchemaPath, clusterSchemasPath } from 'lib/paths';\nimport { act, screen } from '@testing-library/react';\nimport {\n  schemasFulfilledState,\n  schemasInitialState,\n  schemaVersion1,\n  schemaVersion2,\n} from 'redux/reducers/schemas/__test__/fixtures';\nimport ClusterContext, {\n  ContextProps,\n  initialValue as contextInitialValue,\n} from 'components/contexts/ClusterContext';\nimport { RootState } from 'redux/interfaces';\nimport fetchMock from 'fetch-mock';\nimport userEvent from '@testing-library/user-event';\n\nimport { schemasPayload, schemasEmptyPayload } from './fixtures';\n\nconst mockedUsedNavigate = jest.fn();\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useNavigate: () => mockedUsedNavigate,\n}));\n\nconst clusterName = 'testClusterName';\nconst schemasAPIUrl = `/api/clusters/${clusterName}/schemas?page=1&perPage=25`;\nconst schemasAPICompabilityUrl = `/api/clusters/${clusterName}/schemas/compatibility`;\nconst renderComponent = (\n  initialState: RootState['schemas'] = schemasInitialState,\n  context: ContextProps = contextInitialValue\n) =>\n  render(\n    <WithRoute path={clusterSchemasPath()}>\n      <ClusterContext.Provider value={context}>\n        <List />\n      </ClusterContext.Provider>\n    </WithRoute>,\n    {\n      initialEntries: [clusterSchemasPath(clusterName)],\n      preloadedState: {\n        schemas: initialState,\n      },\n    }\n  );\n\ndescribe('List', () => {\n  afterEach(() => {\n    fetchMock.reset();\n  });\n\n  describe('fetch error', () => {\n    it('shows progressbar', async () => {\n      const fetchSchemasMock = fetchMock.getOnce(schemasAPIUrl, 404);\n      const fetchCompabilityMock = fetchMock.getOnce(\n        schemasAPICompabilityUrl,\n        404\n      );\n      await act(() => {\n        renderComponent();\n      });\n      expect(fetchSchemasMock.called()).toBeTruthy();\n      expect(fetchCompabilityMock.called()).toBeTruthy();\n      expect(screen.getByRole('progressbar')).toBeInTheDocument();\n    });\n  });\n\n  describe('fetch success', () => {\n    describe('responded without schemas', () => {\n      beforeEach(async () => {\n        const fetchSchemasMock = fetchMock.getOnce(\n          schemasAPIUrl,\n          schemasEmptyPayload\n        );\n        const fetchCompabilityMock = fetchMock.getOnce(\n          schemasAPICompabilityUrl,\n          200\n        );\n        await act(() => {\n          renderComponent();\n        });\n        expect(fetchSchemasMock.called()).toBeTruthy();\n        expect(fetchCompabilityMock.called()).toBeTruthy();\n      });\n      it('renders empty table', () => {\n        expect(screen.getByText('No schemas found')).toBeInTheDocument();\n      });\n    });\n    describe('responded with schemas', () => {\n      beforeEach(async () => {\n        const fetchSchemasMock = fetchMock.getOnce(\n          schemasAPIUrl,\n          schemasPayload\n        );\n        const fetchCompabilityMock = fetchMock.getOnce(\n          schemasAPICompabilityUrl,\n          200\n        );\n        await act(() => {\n          renderComponent(schemasFulfilledState);\n        });\n        expect(fetchSchemasMock.called()).toBeTruthy();\n        expect(fetchCompabilityMock.called()).toBeTruthy();\n      });\n      it('renders list', () => {\n        expect(screen.getByText(schemaVersion1.subject)).toBeInTheDocument();\n        expect(screen.getByText(schemaVersion2.subject)).toBeInTheDocument();\n      });\n      it('handles onRowClick', async () => {\n        const { id, schemaType, subject, version, compatibilityLevel } =\n          schemaVersion2;\n        const row = screen.getByRole('row', {\n          name: `${subject} ${id} ${schemaType} ${version} ${compatibilityLevel}`,\n        });\n        expect(row).toBeInTheDocument();\n        await userEvent.click(row);\n        expect(mockedUsedNavigate).toHaveBeenCalledWith(\n          clusterSchemaPath(clusterName, subject)\n        );\n      });\n    });\n\n    describe('responded with readonly cluster schemas', () => {\n      beforeEach(async () => {\n        const fetchSchemasMock = fetchMock.getOnce(\n          schemasAPIUrl,\n          schemasPayload\n        );\n        fetchMock.getOnce(schemasAPICompabilityUrl, 200);\n        await act(() => {\n          renderComponent(schemasFulfilledState, {\n            ...contextInitialValue,\n            isReadOnly: true,\n          });\n        });\n        expect(fetchSchemasMock.called()).toBeTruthy();\n      });\n      it('does not render Create Schema button', () => {\n        expect(screen.queryByText('Create Schema')).not.toBeInTheDocument();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/List/__test__/fixtures.ts",
    "content": "import {\n  schemaVersion1,\n  schemaVersion2,\n  schemaVersionWithNonAsciiChars,\n} from 'redux/reducers/schemas/__test__/fixtures';\n\nconst schemas = [\n  schemaVersion1,\n  schemaVersion2,\n  schemaVersionWithNonAsciiChars,\n];\n\nexport const schemasPayload = {\n  pageCount: 1,\n  schemas,\n};\n\nexport const schemasEmptyPayload = {\n  pageCount: 1,\n  schemas: [],\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/New/New.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const Form = styled.form`\n  padding: 16px;\n  padding-top: 0;\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n  width: 50%;\n\n  & > button {\n    align-self: flex-start;\n  }\n\n  & textarea {\n    height: 200px;\n  }\n  & select {\n    width: 30%;\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/New/New.tsx",
    "content": "import React from 'react';\nimport { NewSchemaSubjectRaw } from 'redux/interfaces';\nimport { Controller, FormProvider, useForm } from 'react-hook-form';\nimport { ErrorMessage } from '@hookform/error-message';\nimport {\n  ClusterNameRoute,\n  clusterSchemaPath,\n  clusterSchemasPath,\n} from 'lib/paths';\nimport { SchemaType } from 'generated-sources';\nimport { SCHEMA_NAME_VALIDATION_PATTERN } from 'lib/constants';\nimport { useNavigate } from 'react-router-dom';\nimport { InputLabel } from 'components/common/Input/InputLabel.styled';\nimport Input from 'components/common/Input/Input';\nimport { FormError } from 'components/common/Input/Input.styled';\nimport Select, { SelectOption } from 'components/common/Select/Select';\nimport { Button } from 'components/common/Button/Button';\nimport { Textarea } from 'components/common/Textbox/Textarea.styled';\nimport PageHeading from 'components/common/PageHeading/PageHeading';\nimport { schemaAdded } from 'redux/reducers/schemas/schemasSlice';\nimport { useAppDispatch } from 'lib/hooks/redux';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { showServerError } from 'lib/errorHandling';\nimport { schemasApiClient } from 'lib/api';\nimport yup from 'lib/yupExtended';\nimport { yupResolver } from '@hookform/resolvers/yup';\n\nimport * as S from './New.styled';\n\nconst SchemaTypeOptions: Array<SelectOption> = [\n  { value: SchemaType.AVRO, label: 'AVRO' },\n  { value: SchemaType.JSON, label: 'JSON' },\n  { value: SchemaType.PROTOBUF, label: 'PROTOBUF' },\n];\n\nconst schemaCreate = async (\n  { subject, schema, schemaType }: NewSchemaSubjectRaw,\n  clusterName: string\n) => {\n  return schemasApiClient.createNewSchema({\n    clusterName,\n    newSchemaSubject: { subject, schema, schemaType },\n  });\n};\n\nconst validationSchema = yup.object().shape({\n  subject: yup\n    .string()\n    .required('Subject is required.')\n    .matches(\n      SCHEMA_NAME_VALIDATION_PATTERN,\n      'Only alphanumeric, _, -, and . allowed'\n    ),\n  schema: yup.string().required('Schema is required.'),\n  schemaType: yup.string().required('Schema Type is required.'),\n});\n\nconst New: React.FC = () => {\n  const { clusterName } = useAppParams<ClusterNameRoute>();\n  const navigate = useNavigate();\n  const dispatch = useAppDispatch();\n  const methods = useForm<NewSchemaSubjectRaw>({\n    mode: 'onChange',\n    defaultValues: {\n      schemaType: SchemaType.AVRO,\n    },\n    resolver: yupResolver(validationSchema),\n  });\n  const {\n    register,\n    handleSubmit,\n    control,\n    formState: { isDirty, isSubmitting, errors, isValid },\n  } = methods;\n\n  const onSubmit = async ({\n    subject,\n    schema,\n    schemaType,\n  }: NewSchemaSubjectRaw) => {\n    try {\n      const resp = await schemaCreate(\n        { subject, schema, schemaType } as NewSchemaSubjectRaw,\n        clusterName\n      );\n      dispatch(schemaAdded(resp));\n      navigate(clusterSchemaPath(clusterName, subject));\n    } catch (e) {\n      showServerError(e as Response);\n    }\n  };\n\n  return (\n    <FormProvider {...methods}>\n      <PageHeading\n        text=\"Create\"\n        backText=\"Schema Registry\"\n        backTo={clusterSchemasPath(clusterName)}\n      />\n      <S.Form onSubmit={handleSubmit(onSubmit)}>\n        <div>\n          <InputLabel>Subject *</InputLabel>\n          <Input\n            inputSize=\"M\"\n            placeholder=\"Schema Name\"\n            autoFocus\n            name=\"subject\"\n            autoComplete=\"off\"\n            disabled={isSubmitting}\n          />\n          <FormError>\n            <ErrorMessage errors={errors} name=\"subject\" />\n          </FormError>\n        </div>\n\n        <div>\n          <InputLabel>Schema *</InputLabel>\n          <Textarea\n            {...register('schema', {\n              required: 'Schema is required.',\n            })}\n            disabled={isSubmitting}\n          />\n          <FormError>\n            <ErrorMessage errors={errors} name=\"schema\" />\n          </FormError>\n        </div>\n\n        <div>\n          <InputLabel>Schema Type *</InputLabel>\n          <Controller\n            control={control}\n            name=\"schemaType\"\n            defaultValue={SchemaTypeOptions[0].value as SchemaType}\n            render={({ field: { name, onChange, value } }) => (\n              <Select\n                selectSize=\"M\"\n                name={name}\n                value={value}\n                onChange={onChange}\n                minWidth=\"100%\"\n                disabled={isSubmitting}\n                options={SchemaTypeOptions}\n              />\n            )}\n          />\n          <FormError>\n            <ErrorMessage errors={errors} name=\"schemaType\" />\n          </FormError>\n        </div>\n\n        <Button\n          buttonSize=\"M\"\n          buttonType=\"primary\"\n          type=\"submit\"\n          disabled={!isValid || isSubmitting || !isDirty}\n        >\n          Submit\n        </Button>\n      </S.Form>\n    </FormProvider>\n  );\n};\n\nexport default New;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/New/__test__/New.spec.tsx",
    "content": "import React from 'react';\nimport New from 'components/Schemas/New/New';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { clusterSchemaNewPath } from 'lib/paths';\nimport { act, screen } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\n\nconst clusterName = 'local';\nconst subjectValue = 'subject';\nconst schemaValue = 'schema';\n\ndescribe('New Component', () => {\n  const renderComponent = async () => {\n    render(\n      <WithRoute path={clusterSchemaNewPath()}>\n        <New />\n      </WithRoute>,\n      {\n        initialEntries: [clusterSchemaNewPath(clusterName)],\n      }\n    );\n  };\n\n  beforeEach(async () => {\n    await act(renderComponent);\n  });\n\n  it('renders component', async () => {\n    expect(screen.getByText('Create')).toBeInTheDocument();\n  });\n\n  it('submit button will be disabled while form fields are not filled', () => {\n    const submitBtn = screen.getByRole('button', { name: /submit/i });\n    expect(submitBtn).toBeDisabled();\n  });\n\n  it('submit button will be enabled when form fields are filled', async () => {\n    const subject = screen.getByPlaceholderText('Schema Name');\n    const schema = screen.getAllByRole('textbox')[1];\n    const schemaTypeSelect = screen.getByRole('listbox');\n\n    await userEvent.type(subject, subjectValue);\n    await userEvent.type(schema, schemaValue);\n    await userEvent.selectOptions(schemaTypeSelect, ['AVRO']);\n\n    const submitBtn = screen.getByRole('button', { name: /Submit/i });\n    expect(submitBtn).toBeEnabled();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/Schemas.tsx",
    "content": "import React from 'react';\nimport { Route, Routes } from 'react-router-dom';\nimport {\n  clusterSchemaEditRelativePath,\n  clusterSchemaNewRelativePath,\n  clusterSchemaSchemaDiffRelativePath,\n  RouteParams,\n} from 'lib/paths';\nimport List from 'components/Schemas/List/List';\nimport Details from 'components/Schemas/Details/Details';\nimport New from 'components/Schemas/New/New';\nimport Edit from 'components/Schemas/Edit/Edit';\nimport DiffContainer from 'components/Schemas/Diff/DiffContainer';\n\nconst Schemas: React.FC = () => {\n  return (\n    <Routes>\n      <Route index element={<List />} />\n      <Route path={clusterSchemaNewRelativePath} element={<New />} />\n      <Route path={RouteParams.subject} element={<Details />} />\n      <Route path={clusterSchemaEditRelativePath} element={<Edit />} />\n      <Route\n        path={clusterSchemaSchemaDiffRelativePath}\n        element={<DiffContainer />}\n      />\n    </Routes>\n  );\n};\n\nexport default Schemas;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Schemas/__test__/Schemas.spec.tsx",
    "content": "import React from 'react';\nimport Schemas from 'components/Schemas/Schemas';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport {\n  clusterSchemaEditPath,\n  clusterSchemaNewPath,\n  clusterSchemaPath,\n  clusterSchemasPath,\n  getNonExactPath,\n} from 'lib/paths';\nimport { screen, waitFor } from '@testing-library/dom';\nimport fetchMock from 'fetch-mock';\nimport { schemaVersion } from 'redux/reducers/schemas/__test__/fixtures';\n\nconst renderComponent = (pathname: string) =>\n  render(\n    <WithRoute path={getNonExactPath(clusterSchemasPath())}>\n      <Schemas />\n    </WithRoute>,\n    { initialEntries: [pathname] }\n  );\n\nconst clusterName = 'secondLocal';\n\nconst SchemaCompText = {\n  List: 'List',\n  Details: 'Details',\n  New: 'New',\n  Edit: 'Edit',\n};\n\njest.mock('components/Schemas/List/List', () => () => (\n  <div>{SchemaCompText.List}</div>\n));\njest.mock('components/Schemas/Details/Details', () => () => (\n  <div>{SchemaCompText.Details}</div>\n));\njest.mock('components/Schemas/New/New', () => () => (\n  <div>{SchemaCompText.New}</div>\n));\njest.mock('components/Schemas/Edit/Edit', () => () => (\n  <div>{SchemaCompText.Edit}</div>\n));\n\ndescribe('Schemas', () => {\n  beforeEach(() => {\n    fetchMock.getOnce(`/api/clusters/${clusterName}/schemas`, [schemaVersion]);\n  });\n  afterEach(() => fetchMock.restore());\n  it('renders List', async () => {\n    renderComponent(clusterSchemasPath(clusterName));\n    await waitFor(() =>\n      expect(screen.queryByText(SchemaCompText.List)).toBeInTheDocument()\n    );\n  });\n  it('renders New', async () => {\n    renderComponent(clusterSchemaNewPath(clusterName));\n    await waitFor(() =>\n      expect(screen.queryByText(SchemaCompText.New)).toBeInTheDocument()\n    );\n  });\n  it('renders Details', async () => {\n    renderComponent(clusterSchemaPath(clusterName, schemaVersion.subject));\n    await waitFor(() =>\n      expect(screen.queryByText(SchemaCompText.Details)).toBeInTheDocument()\n    );\n  });\n  it('renders Edit', async () => {\n    renderComponent(clusterSchemaEditPath(clusterName, schemaVersion.subject));\n    await waitFor(() =>\n      expect(screen.queryByText(SchemaCompText.Edit)).toBeInTheDocument()\n    );\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/List/ActionsCell.tsx",
    "content": "import React from 'react';\nimport { Action, CleanUpPolicy, Topic, ResourceType } from 'generated-sources';\nimport { CellContext } from '@tanstack/react-table';\nimport ClusterContext from 'components/contexts/ClusterContext';\nimport { ClusterNameRoute } from 'lib/paths';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { Dropdown, DropdownItemHint } from 'components/common/Dropdown';\nimport {\n  useDeleteTopic,\n  useClearTopicMessages,\n  useRecreateTopic,\n} from 'lib/hooks/api/topics';\nimport { ActionDropdownItem } from 'components/common/ActionComponent';\n\nconst ActionsCell: React.FC<CellContext<Topic, unknown>> = ({ row }) => {\n  const { name, internal, cleanUpPolicy } = row.original;\n\n  const { isReadOnly, isTopicDeletionAllowed } =\n    React.useContext(ClusterContext);\n  const { clusterName } = useAppParams<ClusterNameRoute>();\n\n  const clearMessages = useClearTopicMessages(clusterName);\n  const deleteTopic = useDeleteTopic(clusterName);\n  const recreateTopic = useRecreateTopic({ clusterName, topicName: name });\n\n  const disabled = internal || isReadOnly;\n\n  const clearTopicMessagesHandler = async () => {\n    await clearMessages.mutateAsync(name);\n  };\n\n  const isCleanupDisabled = cleanUpPolicy !== CleanUpPolicy.DELETE;\n\n  return (\n    <Dropdown disabled={disabled}>\n      <ActionDropdownItem\n        disabled={isCleanupDisabled}\n        onClick={clearTopicMessagesHandler}\n        confirm=\"Are you sure want to clear topic messages?\"\n        danger\n        permission={{\n          resource: ResourceType.TOPIC,\n          action: Action.MESSAGES_DELETE,\n          value: name,\n        }}\n      >\n        Clear Messages\n        <DropdownItemHint>\n          Clearing messages is only allowed for topics\n          <br />\n          with DELETE policy\n        </DropdownItemHint>\n      </ActionDropdownItem>\n      <ActionDropdownItem\n        disabled={!isTopicDeletionAllowed}\n        onClick={recreateTopic.mutateAsync}\n        confirm={\n          <>\n            Are you sure to recreate <b>{name}</b> topic?\n          </>\n        }\n        danger\n        permission={{\n          resource: ResourceType.TOPIC,\n          action: [Action.VIEW, Action.CREATE, Action.DELETE],\n          value: name,\n        }}\n      >\n        Recreate Topic\n      </ActionDropdownItem>\n      <ActionDropdownItem\n        disabled={!isTopicDeletionAllowed}\n        onClick={() => deleteTopic.mutateAsync(name)}\n        confirm={\n          <>\n            Are you sure want to remove <b>{name}</b> topic?\n          </>\n        }\n        danger\n        permission={{\n          resource: ResourceType.TOPIC,\n          action: Action.DELETE,\n          value: name,\n        }}\n      >\n        Remove Topic\n        {!isTopicDeletionAllowed && (\n          <DropdownItemHint>\n            The topic deletion is restricted at the broker\n            <br />\n            configuration level (delete.topic.enable = false)\n          </DropdownItemHint>\n        )}\n      </ActionDropdownItem>\n    </Dropdown>\n  );\n};\n\nexport default ActionsCell;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/List/BatchActionsBar.tsx",
    "content": "import React, { useMemo } from 'react';\nimport { Row } from '@tanstack/react-table';\nimport { Action, Topic, ResourceType } from 'generated-sources';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { ClusterName } from 'redux/interfaces';\nimport {\n  topicKeys,\n  useClearTopicMessages,\n  useDeleteTopic,\n} from 'lib/hooks/api/topics';\nimport { useConfirm } from 'lib/hooks/useConfirm';\nimport { clusterTopicCopyRelativePath } from 'lib/paths';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { ActionCanButton } from 'components/common/ActionComponent';\nimport { isPermitted } from 'lib/permissions';\nimport { useUserInfo } from 'lib/hooks/useUserInfo';\n\ninterface BatchActionsbarProps {\n  rows: Row<Topic>[];\n  resetRowSelection(): void;\n}\n\nconst BatchActionsbar: React.FC<BatchActionsbarProps> = ({\n  rows,\n  resetRowSelection,\n}) => {\n  const { clusterName } = useAppParams<{ clusterName: ClusterName }>();\n  const confirm = useConfirm();\n  const deleteTopic = useDeleteTopic(clusterName);\n  const selectedTopics = rows.map(({ original }) => original.name);\n  const client = useQueryClient();\n\n  const clearMessages = useClearTopicMessages(clusterName);\n  const clearTopicMessagesHandler = async (topicName: Topic['name']) => {\n    await clearMessages.mutateAsync(topicName);\n  };\n  const deleteTopicsHandler = () => {\n    confirm('Are you sure you want to remove selected topics?', async () => {\n      try {\n        await Promise.all(\n          selectedTopics.map((topicName) => deleteTopic.mutateAsync(topicName))\n        );\n        resetRowSelection();\n      } catch (e) {\n        // do nothing;\n      }\n    });\n  };\n\n  const purgeTopicsHandler = () => {\n    confirm(\n      'Are you sure you want to purge messages of selected topics?',\n      async () => {\n        try {\n          await Promise.all(\n            selectedTopics.map((topicName) =>\n              clearTopicMessagesHandler(topicName)\n            )\n          );\n          resetRowSelection();\n        } catch (e) {\n          // do nothing;\n        } finally {\n          client.invalidateQueries(topicKeys.all(clusterName));\n        }\n      }\n    );\n  };\n\n  type Tuple = [string, string];\n\n  const getCopyTopicPath = () => {\n    if (!rows.length) {\n      return {\n        pathname: '',\n        search: '',\n      };\n    }\n    const topic = rows[0].original;\n\n    const search = Object.keys(topic).reduce((acc: Tuple[], key) => {\n      const value = topic[key as keyof typeof topic];\n      if (!value || key === 'partitions' || key === 'internal') {\n        return acc;\n      }\n      const tuple: Tuple = [key, value.toString()];\n      return [...acc, tuple];\n    }, []);\n\n    return {\n      pathname: clusterTopicCopyRelativePath,\n      search: new URLSearchParams(search).toString(),\n    };\n  };\n  const { roles, rbacFlag } = useUserInfo();\n\n  const canDeleteSelectedTopics = useMemo(() => {\n    return selectedTopics.every((value) =>\n      isPermitted({\n        roles,\n        resource: ResourceType.TOPIC,\n        action: Action.DELETE,\n        value,\n        clusterName,\n        rbacFlag,\n      })\n    );\n  }, [selectedTopics, clusterName, roles]);\n\n  const canCopySelectedTopic = useMemo(() => {\n    return selectedTopics.every((value) =>\n      isPermitted({\n        roles,\n        resource: ResourceType.TOPIC,\n        action: Action.CREATE,\n        value,\n        clusterName,\n        rbacFlag,\n      })\n    );\n  }, [selectedTopics, clusterName, roles]);\n\n  const canPurgeSelectedTopics = useMemo(() => {\n    return selectedTopics.every((value) =>\n      isPermitted({\n        roles,\n        resource: ResourceType.TOPIC,\n        action: Action.MESSAGES_DELETE,\n        value,\n        clusterName,\n        rbacFlag,\n      })\n    );\n  }, [selectedTopics, clusterName, roles]);\n\n  return (\n    <>\n      <ActionCanButton\n        buttonSize=\"M\"\n        buttonType=\"secondary\"\n        onClick={deleteTopicsHandler}\n        disabled={!selectedTopics.length}\n        canDoAction={canDeleteSelectedTopics}\n      >\n        Delete selected topics\n      </ActionCanButton>\n      <ActionCanButton\n        buttonSize=\"M\"\n        buttonType=\"secondary\"\n        disabled={selectedTopics.length !== 1}\n        canDoAction={canCopySelectedTopic}\n        to={getCopyTopicPath()}\n      >\n        Copy selected topic\n      </ActionCanButton>\n      <ActionCanButton\n        buttonSize=\"M\"\n        buttonType=\"secondary\"\n        onClick={purgeTopicsHandler}\n        disabled={!selectedTopics.length}\n        canDoAction={canPurgeSelectedTopics}\n      >\n        Purge messages of selected topics\n      </ActionCanButton>\n    </>\n  );\n};\n\nexport default BatchActionsbar;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/List/ListPage.tsx",
    "content": "import React, { Suspense } from 'react';\nimport { useSearchParams } from 'react-router-dom';\nimport { clusterTopicNewRelativePath } from 'lib/paths';\nimport { PER_PAGE } from 'lib/constants';\nimport ClusterContext from 'components/contexts/ClusterContext';\nimport Search from 'components/common/Search/Search';\nimport { ActionButton } from 'components/common/ActionComponent';\nimport PageHeading from 'components/common/PageHeading/PageHeading';\nimport { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';\nimport Switch from 'components/common/Switch/Switch';\nimport PlusIcon from 'components/common/Icons/PlusIcon';\nimport PageLoader from 'components/common/PageLoader/PageLoader';\nimport TopicTable from 'components/Topics/List/TopicTable';\nimport { Action, ResourceType } from 'generated-sources';\n\nconst ListPage: React.FC = () => {\n  const { isReadOnly } = React.useContext(ClusterContext);\n  const [searchParams, setSearchParams] = useSearchParams();\n\n  // Set the search params to the url based on the localStorage value\n  React.useEffect(() => {\n    if (!searchParams.has('perPage')) {\n      searchParams.set('perPage', String(PER_PAGE));\n    }\n    if (\n      !!localStorage.getItem('hideInternalTopics') &&\n      !searchParams.has('hideInternal')\n    ) {\n      searchParams.set('hideInternal', 'true');\n    }\n    setSearchParams(searchParams);\n  }, []);\n\n  const handleSwitch = () => {\n    if (searchParams.has('hideInternal')) {\n      localStorage.removeItem('hideInternalTopics');\n      searchParams.delete('hideInternal');\n    } else {\n      localStorage.setItem('hideInternalTopics', 'true');\n      searchParams.set('hideInternal', 'true');\n    }\n    // Page must be reset when the switch is toggled\n    searchParams.set('page', '1');\n    setSearchParams(searchParams);\n  };\n\n  return (\n    <>\n      <PageHeading text=\"Topics\">\n        {!isReadOnly && (\n          <ActionButton\n            buttonType=\"primary\"\n            buttonSize=\"M\"\n            to={clusterTopicNewRelativePath}\n            permission={{\n              resource: ResourceType.TOPIC,\n              action: Action.CREATE,\n            }}\n          >\n            <PlusIcon /> Add a Topic\n          </ActionButton>\n        )}\n      </PageHeading>\n      <ControlPanelWrapper hasInput>\n        <Search placeholder=\"Search by Topic Name\" />\n        <label>\n          <Switch\n            name=\"ShowInternalTopics\"\n            checked={!searchParams.has('hideInternal')}\n            onChange={handleSwitch}\n          />\n          Show Internal Topics\n        </label>\n      </ControlPanelWrapper>\n      <Suspense fallback={<PageLoader />}>\n        <TopicTable />\n      </Suspense>\n    </>\n  );\n};\n\nexport default ListPage;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/List/TopicTable.tsx",
    "content": "import React from 'react';\nimport { SortOrder, Topic, TopicColumnsToSort } from 'generated-sources';\nimport { ColumnDef } from '@tanstack/react-table';\nimport Table, { SizeCell } from 'components/common/NewTable';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { ClusterName } from 'redux/interfaces';\nimport { useSearchParams } from 'react-router-dom';\nimport ClusterContext from 'components/contexts/ClusterContext';\nimport { useTopics } from 'lib/hooks/api/topics';\nimport { PER_PAGE } from 'lib/constants';\n\nimport { TopicTitleCell } from './TopicTitleCell';\nimport ActionsCell from './ActionsCell';\nimport BatchActionsbar from './BatchActionsBar';\n\nconst TopicTable: React.FC = () => {\n  const { clusterName } = useAppParams<{ clusterName: ClusterName }>();\n  const [searchParams] = useSearchParams();\n  const { isReadOnly } = React.useContext(ClusterContext);\n  const { data } = useTopics({\n    clusterName,\n    page: Number(searchParams.get('page') || 1),\n    perPage: Number(searchParams.get('perPage') || PER_PAGE),\n    search: searchParams.get('q') || undefined,\n    showInternal: !searchParams.has('hideInternal'),\n    orderBy: (searchParams.get('sortBy') as TopicColumnsToSort) || undefined,\n    sortOrder:\n      (searchParams.get('sortDirection')?.toUpperCase() as SortOrder) ||\n      undefined,\n  });\n\n  const topics = data?.topics || [];\n  const pageCount = data?.pageCount || 0;\n\n  const columns = React.useMemo<ColumnDef<Topic>[]>(\n    () => [\n      {\n        id: TopicColumnsToSort.NAME,\n        header: 'Topic Name',\n        accessorKey: 'name',\n        cell: TopicTitleCell,\n      },\n      {\n        id: TopicColumnsToSort.TOTAL_PARTITIONS,\n        header: 'Partitions',\n        accessorKey: 'partitionCount',\n      },\n      {\n        id: TopicColumnsToSort.OUT_OF_SYNC_REPLICAS,\n        header: 'Out of sync replicas',\n        accessorKey: 'partitions',\n        cell: ({ getValue }) => {\n          const partitions = getValue<Topic['partitions']>();\n          if (partitions === undefined || partitions.length === 0) {\n            return 0;\n          }\n          return partitions.reduce((memo, { replicas }) => {\n            const outOfSync = replicas?.filter(({ inSync }) => !inSync);\n            return memo + (outOfSync?.length || 0);\n          }, 0);\n        },\n      },\n      {\n        header: 'Replication Factor',\n        accessorKey: 'replicationFactor',\n        enableSorting: false,\n      },\n      {\n        header: 'Number of messages',\n        accessorKey: 'partitions',\n        enableSorting: false,\n        cell: ({ getValue }) => {\n          const partitions = getValue<Topic['partitions']>();\n          if (partitions === undefined || partitions.length === 0) {\n            return 0;\n          }\n          return partitions.reduce((memo, { offsetMax, offsetMin }) => {\n            return memo + (offsetMax - offsetMin);\n          }, 0);\n        },\n      },\n      {\n        id: TopicColumnsToSort.SIZE,\n        header: 'Size',\n        accessorKey: 'segmentSize',\n        cell: SizeCell,\n      },\n      {\n        id: 'actions',\n        header: '',\n        cell: ActionsCell,\n      },\n    ],\n    []\n  );\n\n  return (\n    <Table\n      data={topics}\n      pageCount={pageCount}\n      columns={columns}\n      enableSorting\n      serverSideProcessing\n      batchActionsBar={BatchActionsbar}\n      enableRowSelection={\n        !isReadOnly ? (row) => !row.original.internal : undefined\n      }\n      emptyMessage=\"No topics found\"\n    />\n  );\n};\n\nexport default TopicTable;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/List/TopicTitleCell.tsx",
    "content": "import React from 'react';\nimport { CellContext } from '@tanstack/react-table';\nimport { Tag } from 'components/common/Tag/Tag.styled';\nimport { Topic } from 'generated-sources';\nimport { NavLink } from 'react-router-dom';\n\nexport const TopicTitleCell: React.FC<CellContext<Topic, unknown>> = ({\n  row: { original },\n}) => {\n  const { internal, name } = original;\n  return (\n    <NavLink to={name} title={name}>\n      {internal && (\n        <>\n          <Tag color=\"gray\">IN</Tag>\n          &nbsp;\n        </>\n      )}\n      {name}\n    </NavLink>\n  );\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/List/__tests__/ListPage.spec.tsx",
    "content": "import React from 'react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { screen } from '@testing-library/react';\nimport ClusterContext from 'components/contexts/ClusterContext';\nimport userEvent from '@testing-library/user-event';\nimport { clusterTopicsPath } from 'lib/paths';\nimport ListPage from 'components/Topics/List/ListPage';\n\nconst clusterName = 'test-cluster';\n\njest.mock('components/Topics/List/TopicTable', () => () => <>TopicTableMock</>);\n\ndescribe('ListPage Component', () => {\n  const renderComponent = () =>\n    render(\n      <ClusterContext.Provider\n        value={{\n          isReadOnly: false,\n          hasKafkaConnectConfigured: true,\n          hasSchemaRegistryConfigured: true,\n          isTopicDeletionAllowed: true,\n        }}\n      >\n        <WithRoute path={clusterTopicsPath()}>\n          <ListPage />\n        </WithRoute>\n      </ClusterContext.Provider>,\n      { initialEntries: [clusterTopicsPath(clusterName)] }\n    );\n\n  describe('Component Render', () => {\n    beforeEach(() => {\n      renderComponent();\n    });\n    it('handles switch of Internal Topics visibility', async () => {\n      const switchInput = screen.getByLabelText('Show Internal Topics');\n      expect(switchInput).toBeInTheDocument();\n\n      expect(global.localStorage.getItem('hideInternalTopics')).toBeNull();\n      await userEvent.click(switchInput);\n      expect(global.localStorage.getItem('hideInternalTopics')).toBeTruthy();\n      await userEvent.click(switchInput);\n      expect(global.localStorage.getItem('hideInternalTopics')).toBeNull();\n    });\n\n    it('renders the TopicsTable', () => {\n      expect(screen.getByText('TopicTableMock')).toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/List/__tests__/TopicTable.spec.tsx",
    "content": "import React from 'react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { screen, within } from '@testing-library/react';\nimport { CleanUpPolicy, TopicsResponse } from 'generated-sources';\nimport { externalTopicPayload, topicsPayload } from 'lib/fixtures/topics';\nimport ClusterContext from 'components/contexts/ClusterContext';\nimport userEvent from '@testing-library/user-event';\nimport {\n  useClearTopicMessages,\n  useDeleteTopic,\n  useRecreateTopic,\n  useTopics,\n} from 'lib/hooks/api/topics';\nimport TopicTable from 'components/Topics/List/TopicTable';\nimport { clusterTopicsPath } from 'lib/paths';\n\nconst clusterName = 'test-cluster';\n\njest.mock('lib/hooks/redux', () => ({\n  ...jest.requireActual('lib/hooks/redux'),\n  useAppDispatch: jest.fn(),\n}));\n\nconst getButtonByName = (name: string) => screen.getByRole('button', { name });\n\njest.mock('lib/hooks/api/topics', () => ({\n  ...jest.requireActual('lib/hooks/api/topics'),\n  useDeleteTopic: jest.fn(),\n  useRecreateTopic: jest.fn(),\n  useTopics: jest.fn(),\n  useClearTopicMessages: jest.fn(),\n}));\n\nconst deleteTopicMock = jest.fn();\nconst recreateTopicMock = jest.fn();\nconst clearTopicMessages = jest.fn();\n\ndescribe('TopicTable Components', () => {\n  beforeEach(() => {\n    (useDeleteTopic as jest.Mock).mockImplementation(() => ({\n      mutateAsync: deleteTopicMock,\n    }));\n    (useClearTopicMessages as jest.Mock).mockImplementation(() => ({\n      mutateAsync: clearTopicMessages,\n    }));\n    (useRecreateTopic as jest.Mock).mockImplementation(() => ({\n      mutateAsync: recreateTopicMock,\n    }));\n  });\n\n  const renderComponent = (\n    currentData: TopicsResponse | undefined = undefined,\n    isReadOnly = false,\n    isTopicDeletionAllowed = true\n  ) => {\n    (useTopics as jest.Mock).mockImplementation(() => ({\n      data: currentData,\n    }));\n\n    return render(\n      <ClusterContext.Provider\n        value={{\n          isReadOnly,\n          hasKafkaConnectConfigured: true,\n          hasSchemaRegistryConfigured: true,\n          isTopicDeletionAllowed,\n        }}\n      >\n        <WithRoute path={clusterTopicsPath()}>\n          <TopicTable />\n        </WithRoute>\n      </ClusterContext.Provider>,\n      {\n        initialEntries: [clusterTopicsPath(clusterName)],\n      }\n    );\n  };\n\n  describe('without data', () => {\n    it('renders empty table when payload is undefined', () => {\n      renderComponent();\n      expect(\n        screen.getByRole('row', { name: 'No topics found' })\n      ).toBeInTheDocument();\n    });\n\n    it('renders empty table when payload is empty', () => {\n      renderComponent({ topics: [] });\n      expect(\n        screen.getByRole('row', { name: 'No topics found' })\n      ).toBeInTheDocument();\n    });\n  });\n  describe('with topics', () => {\n    it('renders correct rows', () => {\n      renderComponent({ topics: topicsPayload, pageCount: 1 });\n      expect(\n        screen.getByRole('link', { name: '__internal.topic' })\n      ).toBeInTheDocument();\n      expect(\n        screen.getByRole('row', { name: '__internal.topic 1 0 1 0 0 Bytes' })\n      ).toBeInTheDocument();\n      expect(\n        screen.getByRole('link', { name: 'external.topic' })\n      ).toBeInTheDocument();\n      expect(\n        screen.getByRole('row', { name: 'external.topic 1 0 1 0 1 KB' })\n      ).toBeInTheDocument();\n\n      expect(screen.getAllByRole('checkbox').length).toEqual(3);\n    });\n    describe('Selectable rows', () => {\n      it('renders selectable rows', () => {\n        renderComponent({ topics: topicsPayload, pageCount: 1 });\n        expect(screen.getAllByRole('checkbox').length).toEqual(3);\n        // Disable checkbox for internal topic\n        expect(screen.getAllByRole('checkbox')[1]).toBeDisabled();\n        // Disable checkbox for external topic\n        expect(screen.getAllByRole('checkbox')[2]).toBeEnabled();\n      });\n      it('does not render selectable rows for read-only cluster', () => {\n        renderComponent({ topics: topicsPayload, pageCount: 1 }, true);\n        expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();\n      });\n      describe('Batch actions bar', () => {\n        beforeEach(() => {\n          const payload = {\n            topics: [\n              externalTopicPayload,\n              { ...externalTopicPayload, name: 'test-topic' },\n            ],\n            totalPages: 1,\n          };\n          renderComponent(payload);\n          expect(screen.getAllByRole('checkbox').length).toEqual(3);\n          expect(screen.getAllByRole('checkbox')[1]).toBeEnabled();\n          expect(screen.getAllByRole('checkbox')[2]).toBeEnabled();\n        });\n        describe('when only one topic is selected', () => {\n          beforeEach(async () => {\n            await userEvent.click(screen.getAllByRole('checkbox')[1]);\n          });\n          it('renders batch actions bar', () => {\n            expect(getButtonByName('Delete selected topics')).toBeEnabled();\n            expect(getButtonByName('Copy selected topic')).toBeEnabled();\n            expect(\n              getButtonByName('Purge messages of selected topics')\n            ).toBeEnabled();\n          });\n        });\n        describe('when more then one topics are selected', () => {\n          beforeEach(async () => {\n            await userEvent.click(screen.getAllByRole('checkbox')[1]);\n            await userEvent.click(screen.getAllByRole('checkbox')[2]);\n          });\n          it('renders batch actions bar', () => {\n            expect(getButtonByName('Delete selected topics')).toBeEnabled();\n            expect(getButtonByName('Copy selected topic')).toBeDisabled();\n            expect(\n              getButtonByName('Purge messages of selected topics')\n            ).toBeEnabled();\n          });\n          it('handels delete button click', async () => {\n            const button = getButtonByName('Delete selected topics');\n            await userEvent.click(button);\n            expect(\n              screen.getByText(\n                'Are you sure you want to remove selected topics?'\n              )\n            ).toBeInTheDocument();\n            const confirmBtn = getButtonByName('Confirm');\n            expect(confirmBtn).toBeInTheDocument();\n            expect(deleteTopicMock).not.toHaveBeenCalled();\n            await userEvent.click(confirmBtn);\n            expect(deleteTopicMock).toHaveBeenCalledTimes(2);\n            expect(screen.getAllByRole('checkbox')[1]).not.toBeChecked();\n            expect(screen.getAllByRole('checkbox')[2]).not.toBeChecked();\n          });\n          it('handels purge messages button click', async () => {\n            const button = getButtonByName('Purge messages of selected topics');\n            await userEvent.click(button);\n            expect(\n              screen.getByText(\n                'Are you sure you want to purge messages of selected topics?'\n              )\n            ).toBeInTheDocument();\n            const confirmBtn = getButtonByName('Confirm');\n            expect(confirmBtn).toBeInTheDocument();\n            expect(clearTopicMessages).not.toHaveBeenCalled();\n            await userEvent.click(confirmBtn);\n            expect(clearTopicMessages).toHaveBeenCalledTimes(2);\n            expect(screen.getAllByRole('checkbox')[1]).not.toBeChecked();\n            expect(screen.getAllByRole('checkbox')[2]).not.toBeChecked();\n          });\n        });\n      });\n    });\n    describe('Action buttons', () => {\n      const expectDropdownExists = async () => {\n        const btn = screen.getByRole('button', {\n          name: 'Dropdown Toggle',\n        });\n        expect(btn).toBeEnabled();\n        await userEvent.click(btn);\n        expect(screen.getByRole('menu')).toBeInTheDocument();\n      };\n      it('renders disable action buttons for read-only cluster', () => {\n        renderComponent({ topics: topicsPayload, pageCount: 1 }, true);\n        const btns = screen.getAllByRole('button', { name: 'Dropdown Toggle' });\n        expect(btns[0]).toBeDisabled();\n        expect(btns[1]).toBeDisabled();\n      });\n      it('renders action buttons', async () => {\n        await renderComponent({ topics: topicsPayload, pageCount: 1 });\n        expect(\n          screen.getAllByRole('button', { name: 'Dropdown Toggle' }).length\n        ).toEqual(2);\n        // Internal topic action buttons are disabled\n        const internalTopicRow = screen.getByRole('row', {\n          name: '__internal.topic 1 0 1 0 0 Bytes',\n        });\n        expect(internalTopicRow).toBeInTheDocument();\n        expect(\n          within(internalTopicRow).getByRole('button', {\n            name: 'Dropdown Toggle',\n          })\n        ).toBeDisabled();\n        // External topic action buttons are enabled\n        const externalTopicRow = screen.getByRole('row', {\n          name: 'external.topic 1 0 1 0 1 KB',\n        });\n        expect(externalTopicRow).toBeInTheDocument();\n        const extBtn = within(externalTopicRow).getByRole('button', {\n          name: 'Dropdown Toggle',\n        });\n        expect(extBtn).toBeEnabled();\n        await userEvent.click(extBtn);\n        expect(screen.getByRole('menu')).toBeInTheDocument();\n      });\n      describe('and clear messages action', () => {\n        it('is visible for topic with CleanUpPolicy.DELETE', async () => {\n          renderComponent({\n            topics: [\n              {\n                ...topicsPayload[1],\n                cleanUpPolicy: CleanUpPolicy.DELETE,\n              },\n            ],\n          });\n          await expectDropdownExists();\n          const actionBtn = screen.getAllByRole('menuitem');\n          expect(actionBtn[0]).toHaveTextContent('Clear Messages');\n          expect(actionBtn[0]).not.toHaveAttribute('aria-disabled');\n        });\n        it('is disabled for topic without CleanUpPolicy.DELETE', async () => {\n          renderComponent({\n            topics: [\n              {\n                ...topicsPayload[1],\n                cleanUpPolicy: CleanUpPolicy.COMPACT,\n              },\n            ],\n          });\n          await expectDropdownExists();\n          const actionBtn = screen.getAllByRole('menuitem');\n          expect(actionBtn[0]).toHaveTextContent('Clear Messages');\n          expect(actionBtn[0]).toHaveAttribute('aria-disabled');\n        });\n        it('works as expected', async () => {\n          renderComponent({\n            topics: [\n              {\n                ...topicsPayload[1],\n                cleanUpPolicy: CleanUpPolicy.DELETE,\n              },\n            ],\n          });\n          await expectDropdownExists();\n          await userEvent.click(screen.getByText('Clear Messages'));\n          expect(\n            screen.getByText('Are you sure want to clear topic messages?')\n          ).toBeInTheDocument();\n          await userEvent.click(\n            screen.getByRole('button', { name: 'Confirm' })\n          );\n          expect(clearTopicMessages).toHaveBeenCalled();\n        });\n      });\n\n      describe('and remove topic action', () => {\n        it('is visible only when topic deletion allowed for cluster', async () => {\n          renderComponent({ topics: [topicsPayload[1]] });\n          await expectDropdownExists();\n          const actionBtn = screen.getAllByRole('menuitem');\n          expect(actionBtn[2]).toHaveTextContent('Remove Topic');\n          expect(actionBtn[2]).not.toHaveAttribute('aria-disabled');\n        });\n        it('is disabled when topic deletion is not allowed for cluster', async () => {\n          renderComponent({ topics: [topicsPayload[1]] }, false, false);\n          await expectDropdownExists();\n          const actionBtn = screen.getAllByRole('menuitem');\n          expect(actionBtn[2]).toHaveTextContent('Remove Topic');\n          expect(actionBtn[2]).toHaveAttribute('aria-disabled');\n        });\n        it('works as expected', async () => {\n          renderComponent({ topics: [topicsPayload[1]] });\n          await expectDropdownExists();\n          await userEvent.click(screen.getByText('Remove Topic'));\n          expect(screen.getByText('Confirm the action')).toBeInTheDocument();\n          await userEvent.click(\n            screen.getByRole('button', { name: 'Confirm' })\n          );\n          expect(deleteTopicMock).toHaveBeenCalled();\n        });\n      });\n      describe('and recreate topic action', () => {\n        it('works as expected', async () => {\n          renderComponent({ topics: [topicsPayload[1]] });\n          await expectDropdownExists();\n          await userEvent.click(screen.getByText('Recreate Topic'));\n          expect(screen.getByText('Confirm the action')).toBeInTheDocument();\n          await userEvent.click(\n            screen.getByRole('button', { name: 'Confirm' })\n          );\n          expect(recreateTopicMock).toHaveBeenCalled();\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/New/New.tsx",
    "content": "import React from 'react';\nimport { TopicFormData } from 'redux/interfaces';\nimport { FormProvider, useForm } from 'react-hook-form';\nimport { ClusterNameRoute, clusterTopicsPath } from 'lib/paths';\nimport TopicForm from 'components/Topics/shared/Form/TopicForm';\nimport { useLocation, useNavigate } from 'react-router-dom';\nimport { yupResolver } from '@hookform/resolvers/yup';\nimport { topicFormValidationSchema } from 'lib/yupExtended';\nimport PageHeading from 'components/common/PageHeading/PageHeading';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { useCreateTopic } from 'lib/hooks/api/topics';\n\nenum Filters {\n  NAME = 'name',\n  PARTITION_COUNT = 'partitionCount',\n  REPLICATION_FACTOR = 'replicationFactor',\n  INSYNC_REPLICAS = 'inSyncReplicas',\n  CLEANUP_POLICY = 'cleanUpPolicy',\n}\n\nconst New: React.FC = () => {\n  const { clusterName } = useAppParams<ClusterNameRoute>();\n  const methods = useForm<TopicFormData>({\n    mode: 'onChange',\n    resolver: yupResolver(topicFormValidationSchema),\n  });\n\n  const createTopic = useCreateTopic(clusterName);\n\n  const navigate = useNavigate();\n\n  const { search } = useLocation();\n  const params = new URLSearchParams(search);\n\n  const name = params.get(Filters.NAME) || '';\n  const partitionCount = params.get(Filters.PARTITION_COUNT) || 1;\n  const replicationFactor = params.get(Filters.REPLICATION_FACTOR) || 1;\n  const inSyncReplicas = params.get(Filters.INSYNC_REPLICAS) || 1;\n  const cleanUpPolicy = params.get(Filters.CLEANUP_POLICY) || 'Delete';\n\n  const onSubmit = async (data: TopicFormData) => {\n    try {\n      await createTopic.createResource(data);\n      navigate(`../${data.name}`);\n    } catch (e) {\n      // do nothing\n    }\n  };\n\n  return (\n    <>\n      <PageHeading\n        text={search ? 'Copy' : 'Create'}\n        backText=\"Topics\"\n        backTo={clusterTopicsPath(clusterName)}\n      />\n      <FormProvider {...methods}>\n        <TopicForm\n          topicName={name}\n          cleanUpPolicy={cleanUpPolicy}\n          partitionCount={Number(partitionCount)}\n          replicationFactor={Number(replicationFactor)}\n          inSyncReplicas={Number(inSyncReplicas)}\n          isSubmitting={createTopic.isLoading}\n          onSubmit={methods.handleSubmit(onSubmit)}\n        />\n      </FormProvider>\n    </>\n  );\n};\n\nexport default New;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/New/__test__/New.spec.tsx",
    "content": "import React from 'react';\nimport New from 'components/Topics/New/New';\nimport { Route, Routes } from 'react-router-dom';\nimport { act, screen } from '@testing-library/react';\nimport {\n  clusterTopicCopyPath,\n  clusterTopicNewPath,\n  clusterTopicPath,\n} from 'lib/paths';\nimport userEvent from '@testing-library/user-event';\nimport { render } from 'lib/testHelpers';\nimport { useCreateTopic } from 'lib/hooks/api/topics';\n\nconst clusterName = 'local';\nconst topicName = 'test-topic';\nconst minValue = '1';\n\nconst mockNavigate = jest.fn();\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useNavigate: () => mockNavigate,\n}));\njest.mock('lib/hooks/api/topics', () => ({\n  useCreateTopic: jest.fn(),\n}));\n\nconst renderComponent = (path: string) => {\n  render(\n    <Routes>\n      <Route path={clusterTopicNewPath()} element={<New />} />\n      <Route path={clusterTopicCopyPath()} element={<New />} />\n      <Route path={clusterTopicPath()} element=\"New topic path\" />\n    </Routes>,\n    { initialEntries: [path] }\n  );\n};\nconst createTopicMock = jest.fn();\ndescribe('New', () => {\n  beforeEach(() => {\n    (useCreateTopic as jest.Mock).mockImplementation(() => ({\n      createResource: createTopicMock,\n    }));\n  });\n  it('checks header for create new', async () => {\n    await act(() => {\n      renderComponent(clusterTopicNewPath(clusterName));\n    });\n    expect(screen.getByRole('heading', { name: 'Create' })).toBeInTheDocument();\n  });\n\n  it('checks header for copy', async () => {\n    await act(() => {\n      renderComponent(`${clusterTopicCopyPath(clusterName)}?name=test`);\n    });\n    expect(screen.getByRole('heading', { name: 'Copy' })).toBeInTheDocument();\n  });\n  it('validates form', async () => {\n    renderComponent(clusterTopicNewPath(clusterName));\n    await userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName);\n    await userEvent.clear(screen.getByPlaceholderText('Topic Name'));\n    await userEvent.tab();\n    await expect(\n      screen.getByText('Topic Name is required')\n    ).toBeInTheDocument();\n    await userEvent.type(\n      screen.getByLabelText('Number of Partitions *'),\n      minValue\n    );\n    await userEvent.clear(screen.getByLabelText('Number of Partitions *'));\n    await userEvent.tab();\n    await expect(\n      screen.getByText('Number of Partitions is required and must be a number')\n    ).toBeInTheDocument();\n\n    expect(createTopicMock).not.toHaveBeenCalled();\n    expect(mockNavigate).not.toHaveBeenCalled();\n  });\n  it('validates form invalid name', async () => {\n    renderComponent(clusterTopicNewPath(clusterName));\n    await userEvent.type(\n      screen.getByPlaceholderText('Topic Name'),\n      'Invalid,Name'\n    );\n    expect(\n      screen.getByText('Only alphanumeric, _, -, and . allowed')\n    ).toBeInTheDocument();\n  });\n  it('submits valid form', async () => {\n    renderComponent(clusterTopicNewPath(clusterName));\n    await userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName);\n    await userEvent.type(\n      screen.getByLabelText('Number of Partitions *'),\n      minValue\n    );\n    await userEvent.click(screen.getByText('Create topic'));\n    expect(createTopicMock).toHaveBeenCalledTimes(1);\n    expect(mockNavigate).toHaveBeenLastCalledWith(`../${topicName}`);\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/ConsumerGroups/TopicConsumerGroups.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const SearchWrapper = styled.div`\n  margin: 10px;\n  width: 21%;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/ConsumerGroups/TopicConsumerGroups.tsx",
    "content": "import React from 'react';\nimport { clusterConsumerGroupsPath, RouteParamsClusterTopic } from 'lib/paths';\nimport { ConsumerGroup } from 'generated-sources';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { useTopicConsumerGroups } from 'lib/hooks/api/topics';\nimport { ColumnDef } from '@tanstack/react-table';\nimport Table, { LinkCell, TagCell } from 'components/common/NewTable';\nimport Search from 'components/common/Search/Search';\n\nimport * as S from './TopicConsumerGroups.styled';\n\nconst TopicConsumerGroups: React.FC = () => {\n  const [keyword, setKeyword] = React.useState('');\n  const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();\n\n  const { data = [] } = useTopicConsumerGroups({\n    clusterName,\n    topicName,\n  });\n\n  const consumerGroups = React.useMemo(\n    () =>\n      data.filter(\n        (item) => item.groupId.toLocaleLowerCase().indexOf(keyword) > -1\n      ),\n    [data, keyword]\n  );\n\n  const columns = React.useMemo<ColumnDef<ConsumerGroup>[]>(\n    () => [\n      {\n        header: 'Consumer Group ID',\n        accessorKey: 'groupId',\n        enableSorting: false,\n        // eslint-disable-next-line react/no-unstable-nested-components\n        cell: ({ row }) => (\n          <LinkCell\n            value={row.original.groupId}\n            to={`${clusterConsumerGroupsPath(clusterName)}/${\n              row.original.groupId\n            }`}\n          />\n        ),\n      },\n      {\n        header: 'Active Consumers',\n        accessorKey: 'members',\n        enableSorting: false,\n      },\n      {\n        header: 'Consumer Lag',\n        accessorKey: 'consumerLag',\n        enableSorting: false,\n      },\n      {\n        header: 'Coordinator',\n        accessorKey: 'coordinator',\n        enableSorting: false,\n        cell: ({ getValue }) => {\n          const coordinator = getValue<ConsumerGroup['coordinator']>();\n          if (coordinator === undefined) {\n            return 0;\n          }\n          return coordinator.id;\n        },\n      },\n      {\n        header: 'State',\n        accessorKey: 'state',\n        enableSorting: false,\n        cell: TagCell,\n      },\n    ],\n    []\n  );\n  return (\n    <>\n      <S.SearchWrapper>\n        <Search\n          onChange={setKeyword}\n          placeholder=\"Search by Consumer Name\"\n          value={keyword}\n        />\n      </S.SearchWrapper>\n      <Table\n        columns={columns}\n        data={consumerGroups}\n        enableSorting\n        emptyMessage=\"No active consumer groups\"\n      />\n    </>\n  );\n};\n\nexport default TopicConsumerGroups;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/ConsumerGroups/__test__/TopicConsumerGroups.spec.tsx",
    "content": "import React from 'react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { screen } from '@testing-library/react';\nimport TopicConsumerGroups from 'components/Topics/Topic/ConsumerGroups/TopicConsumerGroups';\nimport { clusterTopicConsumerGroupsPath } from 'lib/paths';\nimport { useTopicConsumerGroups } from 'lib/hooks/api/topics';\nimport { ConsumerGroup } from 'generated-sources';\nimport { topicConsumerGroups } from 'lib/fixtures/topics';\n\nconst clusterName = 'local';\nconst topicName = 'my-topicName';\nconst path = clusterTopicConsumerGroupsPath(clusterName, topicName);\n\njest.mock('lib/hooks/api/topics', () => ({\n  useTopicConsumerGroups: jest.fn(),\n}));\n\ndescribe('TopicConsumerGroups', () => {\n  const renderComponent = async (payload?: ConsumerGroup[]) => {\n    (useTopicConsumerGroups as jest.Mock).mockImplementation(() => ({\n      data: payload,\n    }));\n\n    render(\n      <WithRoute path={clusterTopicConsumerGroupsPath()}>\n        <TopicConsumerGroups />\n      </WithRoute>,\n      { initialEntries: [path] }\n    );\n  };\n\n  it('renders empty table if consumer groups payload is empty', async () => {\n    await renderComponent([]);\n    expect(screen.getByText('No active consumer groups')).toBeInTheDocument();\n  });\n\n  it('renders empty table if consumer groups payload is undefined', async () => {\n    await renderComponent();\n    expect(screen.getByText('No active consumer groups')).toBeInTheDocument();\n  });\n\n  it('renders table of consumer groups', async () => {\n    await renderComponent(topicConsumerGroups);\n    const groupIds = topicConsumerGroups.map(({ groupId }) => groupId);\n    groupIds.forEach((groupId) =>\n      expect(screen.getByText(groupId)).toBeInTheDocument()\n    );\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZone.styled.tsx",
    "content": "import styled from 'styled-components';\n\nexport const Wrapper = styled.div`\n  margin-top: 16px;\n  padding: 16px;\n  border: 1px solid ${({ theme }) => theme.dangerZone.borderColor};\n  box-sizing: border-box;\n  width: 768px;\n\n  & > div {\n    display: flex;\n    flex-direction: column;\n    gap: 8px;\n  }\n`;\n\nexport const Title = styled.h1`\n  color: ${({ theme }) => theme.dangerZone.color.title};\n  font-size: 20px;\n  padding-bottom: 16px;\n`;\nexport const Warning = styled.div`\n  color: ${({ theme }) => theme.dangerZone.color.warningMessage};\n  font-size: 12px;\n  padding-bottom: 16px;\n`;\nexport const Form = styled.form`\n  display: flex;\n  align-items: flex-end;\n  gap: 16px;\n  & > *:first-child {\n    flex-grow: 4;\n  }\n  & > *:last-child {\n    flex-grow: 1;\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZone.tsx",
    "content": "import { ErrorMessage } from '@hookform/error-message';\nimport { Button } from 'components/common/Button/Button';\nimport Input from 'components/common/Input/Input';\nimport { FormError } from 'components/common/Input/Input.styled';\nimport { InputLabel } from 'components/common/Input/InputLabel.styled';\nimport React from 'react';\nimport { FormProvider, useForm } from 'react-hook-form';\nimport { RouteParamsClusterTopic } from 'lib/paths';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { useConfirm } from 'lib/hooks/useConfirm';\nimport {\n  useIncreaseTopicPartitionsCount,\n  useUpdateTopicReplicationFactor,\n} from 'lib/hooks/api/topics';\n\nimport * as S from './DangerZone.styled';\n\nexport interface DangerZoneProps {\n  defaultPartitions: number;\n  defaultReplicationFactor: number;\n}\n\nconst DangerZone: React.FC<DangerZoneProps> = ({\n  defaultPartitions,\n  defaultReplicationFactor,\n}) => {\n  const params = useAppParams<RouteParamsClusterTopic>();\n  const [partitions, setPartitions] = React.useState<number>(defaultPartitions);\n  const [replicationFactor, setReplicationFactor] = React.useState<number>(\n    defaultReplicationFactor\n  );\n  const increaseTopicPartitionsCount = useIncreaseTopicPartitionsCount(params);\n  const updateTopicReplicationFactor = useUpdateTopicReplicationFactor(params);\n\n  const partitionsMethods = useForm({\n    defaultValues: {\n      partitions,\n    },\n  });\n\n  const replicationFactorMethods = useForm({\n    defaultValues: {\n      replicationFactor,\n    },\n  });\n\n  const confirm = useConfirm();\n  const confirmPartitionsChange = () =>\n    confirm(\n      `Are you sure you want to increase the number of partitions?\n        Do it only if you 100% know what you are doing!`,\n      () =>\n        increaseTopicPartitionsCount.mutateAsync(\n          partitionsMethods.getValues('partitions')\n        )\n    );\n  const confirmReplicationFactorChange = () =>\n    confirm('Are you sure you want to update the replication factor?', () =>\n      updateTopicReplicationFactor.mutateAsync(\n        replicationFactorMethods.getValues('replicationFactor')\n      )\n    );\n\n  const validatePartitions = (data: { partitions: number }) => {\n    if (data.partitions < defaultPartitions) {\n      partitionsMethods.setError('partitions', {\n        type: 'manual',\n        message: 'You can only increase the number of partitions!',\n      });\n    } else {\n      setPartitions(data.partitions);\n      confirmPartitionsChange();\n    }\n  };\n\n  const validateReplicationFactor = (data: { replicationFactor: number }) => {\n    try {\n      setReplicationFactor(data.replicationFactor);\n      confirmReplicationFactorChange();\n    } catch (e) {\n      // do nothing\n    }\n  };\n\n  return (\n    <S.Wrapper>\n      <S.Title>Danger Zone</S.Title>\n      <S.Warning>\n        Change these parameters only if you are absolutely sure what you are\n        doing.\n      </S.Warning>\n      <div>\n        <FormProvider {...partitionsMethods}>\n          <S.Form\n            onSubmit={partitionsMethods.handleSubmit(validatePartitions)}\n            aria-label=\"Edit number of partitions\"\n          >\n            <div>\n              <InputLabel htmlFor=\"partitions\">\n                Number of partitions *\n              </InputLabel>\n              <Input\n                inputSize=\"M\"\n                type=\"number\"\n                id=\"partitions\"\n                name=\"partitions\"\n                hookFormOptions={{\n                  required: 'Partiotions are required',\n                }}\n                placeholder=\"Number of partitions\"\n              />\n            </div>\n            <div>\n              <Button\n                buttonType=\"primary\"\n                buttonSize=\"M\"\n                type=\"submit\"\n                disabled={!partitionsMethods.formState.isDirty}\n              >\n                Submit\n              </Button>\n            </div>\n          </S.Form>\n        </FormProvider>\n        <FormError>\n          <ErrorMessage\n            errors={partitionsMethods.formState.errors}\n            name=\"partitions\"\n          />\n        </FormError>\n        <FormProvider {...replicationFactorMethods}>\n          <S.Form\n            onSubmit={replicationFactorMethods.handleSubmit(\n              validateReplicationFactor\n            )}\n            aria-label=\"Edit replication factor\"\n          >\n            <div>\n              <InputLabel htmlFor=\"replicationFactor\">\n                Replication Factor *\n              </InputLabel>\n              <Input\n                id=\"replicationFactor\"\n                inputSize=\"M\"\n                type=\"number\"\n                placeholder=\"Replication Factor\"\n                name=\"replicationFactor\"\n                hookFormOptions={{\n                  required: 'Replication Factor are required',\n                }}\n              />\n            </div>\n            <div>\n              <Button\n                buttonType=\"primary\"\n                buttonSize=\"M\"\n                type=\"submit\"\n                disabled={!replicationFactorMethods.formState.isDirty}\n              >\n                Submit\n              </Button>\n            </div>\n          </S.Form>\n        </FormProvider>\n        <FormError>\n          <ErrorMessage\n            errors={replicationFactorMethods.formState.errors}\n            name=\"replicationFactor\"\n          />\n        </FormError>\n      </div>\n    </S.Wrapper>\n  );\n};\n\nexport default DangerZone;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/__test__/DangerZone.spec.tsx",
    "content": "import React from 'react';\nimport DangerZone, {\n  DangerZoneProps,\n} from 'components/Topics/Topic/Edit/DangerZone/DangerZone';\nimport { screen, waitFor, within } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport {\n  useIncreaseTopicPartitionsCount,\n  useUpdateTopicReplicationFactor,\n} from 'lib/hooks/api/topics';\nimport { clusterTopicPath } from 'lib/paths';\n\nconst defaultPartitions = 3;\nconst defaultReplicationFactor = 3;\n\nconst clusterName = 'testCluster';\nconst topicName = 'testTopic';\n\njest.mock('lib/hooks/api/topics', () => ({\n  useIncreaseTopicPartitionsCount: jest.fn(),\n  useUpdateTopicReplicationFactor: jest.fn(),\n}));\n\nconst renderComponent = (props?: Partial<DangerZoneProps>) =>\n  render(\n    <WithRoute path={clusterTopicPath()}>\n      <DangerZone\n        defaultPartitions={defaultPartitions}\n        defaultReplicationFactor={defaultReplicationFactor}\n        {...props}\n      />\n    </WithRoute>,\n    { initialEntries: [clusterTopicPath(clusterName, topicName)] }\n  );\n\nconst clickOnDialogSubmitButton = async () => {\n  await userEvent.click(\n    within(screen.getByRole('dialog')).getByRole('button', {\n      name: 'Confirm',\n    })\n  );\n};\n\nconst checkDialogThenPressCancel = async () => {\n  const dialog = screen.getByRole('dialog');\n  expect(screen.getByRole('dialog')).toBeInTheDocument();\n  await userEvent.click(within(dialog).getByRole('button', { name: 'Cancel' }));\n  await waitFor(() =>\n    expect(screen.queryByRole('dialog')).not.toBeInTheDocument()\n  );\n};\n\ndescribe('DangerZone', () => {\n  it('renders the component', () => {\n    renderComponent();\n\n    const numberOfPartitionsEditForm = screen.getByRole('form', {\n      name: 'Edit number of partitions',\n    });\n    expect(numberOfPartitionsEditForm).toBeInTheDocument();\n    expect(\n      within(numberOfPartitionsEditForm).getByRole('spinbutton', {\n        name: 'Number of partitions *',\n      })\n    ).toBeInTheDocument();\n    expect(\n      within(numberOfPartitionsEditForm).getByRole('button', { name: 'Submit' })\n    ).toBeInTheDocument();\n\n    const replicationFactorEditForm = screen.getByRole('form', {\n      name: 'Edit replication factor',\n    });\n    expect(replicationFactorEditForm).toBeInTheDocument();\n    expect(\n      within(replicationFactorEditForm).getByRole('spinbutton', {\n        name: 'Replication Factor *',\n      })\n    ).toBeInTheDocument();\n    expect(\n      within(replicationFactorEditForm).getByRole('button', { name: 'Submit' })\n    ).toBeInTheDocument();\n  });\n\n  it('calls increaseTopicPartitionsCount mutation', async () => {\n    const mockIncreaseTopicPartitionsCount = jest.fn();\n    (useIncreaseTopicPartitionsCount as jest.Mock).mockImplementation(() => ({\n      mutateAsync: mockIncreaseTopicPartitionsCount,\n    }));\n    renderComponent();\n    const numberOfPartitionsEditForm = screen.getByRole('form', {\n      name: 'Edit number of partitions',\n    });\n    await userEvent.type(\n      within(numberOfPartitionsEditForm).getByRole('spinbutton'),\n      '4'\n    );\n    await userEvent.click(\n      within(numberOfPartitionsEditForm).getByRole('button')\n    );\n    expect(screen.getByRole('dialog')).toBeInTheDocument();\n    await clickOnDialogSubmitButton();\n    expect(mockIncreaseTopicPartitionsCount).toHaveBeenCalledTimes(1);\n  });\n\n  it('calls updateTopicReplicationFactor', async () => {\n    const mockUpdateTopicReplicationFactor = jest.fn();\n    (useUpdateTopicReplicationFactor as jest.Mock).mockImplementation(() => ({\n      mutateAsync: mockUpdateTopicReplicationFactor,\n    }));\n    renderComponent();\n    const replicationFactorEditForm = screen.getByRole('form', {\n      name: 'Edit replication factor',\n    });\n    expect(\n      within(replicationFactorEditForm).getByRole('spinbutton', {\n        name: 'Replication Factor *',\n      })\n    ).toBeInTheDocument();\n    expect(\n      within(replicationFactorEditForm).getByRole('button', { name: 'Submit' })\n    ).toBeInTheDocument();\n\n    await userEvent.type(\n      within(replicationFactorEditForm).getByRole('spinbutton'),\n      '4'\n    );\n    await userEvent.click(\n      within(replicationFactorEditForm).getByRole('button')\n    );\n\n    expect(screen.getByRole('dialog')).toBeInTheDocument();\n    await clickOnDialogSubmitButton();\n\n    expect(mockUpdateTopicReplicationFactor).toHaveBeenCalledTimes(1);\n  });\n\n  it('should view the validation error when partition value is lower than the default passed or empty', async () => {\n    renderComponent();\n    const partitionInput = screen.getByPlaceholderText('Number of partitions');\n    const partitionInputSubmitBtn = screen.getAllByText(/submit/i)[0];\n    const value = (defaultPartitions - 4).toString();\n    expect(partitionInputSubmitBtn).toBeDisabled();\n\n    await userEvent.clear(partitionInput);\n    await userEvent.type(partitionInput, value);\n\n    expect(partitionInput).toHaveValue(+value);\n    expect(partitionInputSubmitBtn).toBeEnabled();\n\n    await userEvent.click(partitionInputSubmitBtn);\n\n    expect(\n      screen.getByText(/You can only increase the number of partitions!/i)\n    ).toBeInTheDocument();\n    await userEvent.clear(partitionInput);\n    expect(screen.getByText(/are required/i)).toBeInTheDocument();\n  });\n\n  it('should view the validation error when Replication Facto value is lower than the default passed or empty', async () => {\n    renderComponent();\n    const replicatorFactorInput =\n      screen.getByPlaceholderText('Replication Factor');\n    const replicatorFactorInputSubmitBtn = screen.getAllByText(/submit/i)[1];\n\n    await userEvent.clear(replicatorFactorInput);\n\n    expect(replicatorFactorInputSubmitBtn).toBeEnabled();\n    await userEvent.click(replicatorFactorInputSubmitBtn);\n    expect(screen.getByText(/are required/i)).toBeInTheDocument();\n    await userEvent.type(replicatorFactorInput, '1');\n    expect(screen.queryByText(/are required/i)).not.toBeInTheDocument();\n  });\n\n  it('should close the partitions dialog if he cancel button is pressed', async () => {\n    renderComponent();\n\n    const partitionInput = screen.getByPlaceholderText('Number of partitions');\n    const partitionInputSubmitBtn = screen.getAllByText(/submit/i)[0];\n\n    await userEvent.type(partitionInput, '5');\n    await userEvent.click(partitionInputSubmitBtn);\n\n    await checkDialogThenPressCancel();\n  });\n\n  it('should close the replicator dialog if he cancel button is pressed', async () => {\n    renderComponent();\n    const replicatorFactorInput =\n      screen.getByPlaceholderText('Replication Factor');\n    const replicatorFactorInputSubmitBtn = screen.getAllByText(/submit/i)[1];\n\n    await userEvent.type(replicatorFactorInput, '5');\n    await userEvent.click(replicatorFactorInputSubmitBtn);\n\n    await checkDialogThenPressCancel();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx",
    "content": "import React from 'react';\nimport { TopicConfigByName, TopicFormData } from 'redux/interfaces';\nimport { useForm, FormProvider } from 'react-hook-form';\nimport TopicForm from 'components/Topics/shared/Form/TopicForm';\nimport { RouteParamsClusterTopic } from 'lib/paths';\nimport { useNavigate } from 'react-router-dom';\nimport { yupResolver } from '@hookform/resolvers/yup';\nimport { topicFormValidationSchema } from 'lib/yupExtended';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport topicParamsTransformer from 'components/Topics/Topic/Edit/topicParamsTransformer';\nimport { MILLISECONDS_IN_WEEK } from 'lib/constants';\nimport {\n  useTopicConfig,\n  useTopicDetails,\n  useUpdateTopic,\n} from 'lib/hooks/api/topics';\nimport DangerZone from 'components/Topics/Topic/Edit/DangerZone/DangerZone';\nimport { ConfigSource } from 'generated-sources';\n\nexport const TOPIC_EDIT_FORM_DEFAULT_PROPS = {\n  partitions: 1,\n  replicationFactor: 1,\n  minInSyncReplicas: 1,\n  cleanupPolicy: 'delete',\n  retentionBytes: -1,\n  retentionMs: MILLISECONDS_IN_WEEK,\n  maxMessageBytes: 1000012,\n  customParams: [],\n};\n\nconst Edit: React.FC = () => {\n  const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();\n  const { data: topic } = useTopicDetails({ clusterName, topicName });\n  const { data: topicConfig } = useTopicConfig({ clusterName, topicName });\n  const updateTopic = useUpdateTopic({ clusterName, topicName });\n\n  const defaultValues = topicParamsTransformer(topic, topicConfig);\n\n  const methods = useForm<TopicFormData>({\n    defaultValues,\n    resolver: yupResolver(topicFormValidationSchema),\n    mode: 'onChange',\n  });\n\n  const navigate = useNavigate();\n\n  const config: TopicConfigByName = {\n    byName: {},\n  };\n\n  topicConfig?.forEach((param) => {\n    config.byName[param.name] = param;\n  });\n  const onSubmit = async (data: TopicFormData) => {\n    const filteredDirtyDefaultEntries = Object.entries(data).filter(\n      ([key, val]) => {\n        const isDirty =\n          String(val) !==\n          String(defaultValues[key as keyof typeof defaultValues]);\n\n        const isDefaultConfig =\n          config.byName[key]?.source === ConfigSource.DEFAULT_CONFIG;\n\n        // if it is changed should be sent or if it was Dynamic\n        return isDirty || !isDefaultConfig;\n      }\n    );\n\n    const newData = Object.fromEntries(filteredDirtyDefaultEntries);\n    try {\n      await updateTopic.mutateAsync(newData);\n      navigate('../');\n    } catch (e) {\n      // do nothing\n    }\n  };\n\n  return (\n    <>\n      <FormProvider {...methods}>\n        <TopicForm\n          config={config.byName}\n          topicName={topicName}\n          retentionBytes={defaultValues.retentionBytes}\n          inSyncReplicas={Number(defaultValues.minInSyncReplicas)}\n          isSubmitting={updateTopic.isLoading}\n          cleanUpPolicy={topic?.cleanUpPolicy}\n          isEditing\n          onSubmit={methods.handleSubmit(onSubmit)}\n        />\n      </FormProvider>\n      {topic && (\n        <DangerZone\n          defaultPartitions={defaultValues.partitions}\n          defaultReplicationFactor={\n            defaultValues.replicationFactor ||\n            TOPIC_EDIT_FORM_DEFAULT_PROPS.replicationFactor\n          }\n        />\n      )}\n    </>\n  );\n};\n\nexport default Edit;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx",
    "content": "import React from 'react';\nimport Edit from 'components/Topics/Topic/Edit/Edit';\nimport { screen } from '@testing-library/react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport userEvent from '@testing-library/user-event';\nimport { clusterTopicEditPath } from 'lib/paths';\nimport {\n  useTopicConfig,\n  useTopicDetails,\n  useUpdateTopic,\n} from 'lib/hooks/api/topics';\nimport { internalTopicPayload, topicConfigPayload } from 'lib/fixtures/topics';\n\nconst clusterName = 'testCluster';\nconst topicName = 'testTopic';\n\nconst mockNavigate = jest.fn();\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useNavigate: () => mockNavigate,\n}));\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useNavigate: () => mockNavigate,\n}));\n\njest.mock('components/Topics/Topic/Edit/DangerZone/DangerZone', () => () => (\n  <>DangerZone</>\n));\n\njest.mock('lib/hooks/api/topics', () => ({\n  useTopicDetails: jest.fn(),\n  useTopicConfig: jest.fn(),\n  useUpdateTopic: jest.fn(),\n}));\n\nconst updateTopicMock = jest.fn();\n\nconst renderComponent = () => {\n  const path = clusterTopicEditPath(clusterName, topicName);\n  return render(\n    <WithRoute path={clusterTopicEditPath()}>\n      <Edit />\n    </WithRoute>,\n    { initialEntries: [path] }\n  );\n};\n\ndescribe('Edit Component', () => {\n  beforeEach(() => {\n    (useTopicDetails as jest.Mock).mockImplementation(() => ({\n      data: internalTopicPayload,\n    }));\n    (useTopicConfig as jest.Mock).mockImplementation(() => ({\n      data: topicConfigPayload,\n    }));\n    (useUpdateTopic as jest.Mock).mockImplementation(() => ({\n      isLoading: false,\n      mutateAsync: updateTopicMock,\n    }));\n    renderComponent();\n  });\n\n  it('renders DangerZone component', () => {\n    expect(screen.getByText(`DangerZone`)).toBeInTheDocument();\n  });\n\n  it('submits form correctly', async () => {\n    renderComponent();\n    const btn = screen.getAllByText(/Update topic/i)[0];\n    const field = screen.getByRole('spinbutton', {\n      name: 'Min In Sync Replicas Min In Sync Replicas',\n    });\n    await userEvent.type(field, '1');\n    await userEvent.click(btn);\n    expect(updateTopicMock).toHaveBeenCalledTimes(1);\n    expect(mockNavigate).toHaveBeenCalledWith('../');\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/topicParamsTransformer.spec.ts",
    "content": "import topicParamsTransformer, {\n  getValue,\n} from 'components/Topics/Topic/Edit/topicParamsTransformer';\nimport { externalTopicPayload, topicConfigPayload } from 'lib/fixtures/topics';\nimport { TOPIC_EDIT_FORM_DEFAULT_PROPS } from 'components/Topics/Topic/Edit/Edit';\n\nconst defaultValue = 3232326;\ndescribe('getValue', () => {\n  it('returns value when field exists', () => {\n    expect(getValue(topicConfigPayload, 'min.insync.replicas')).toEqual(1);\n  });\n  it('returns default value when field does not exists', () => {\n    expect(getValue(topicConfigPayload, 'min.max.mid', defaultValue)).toEqual(\n      defaultValue\n    );\n  });\n});\n\ndescribe('topicParamsTransformer', () => {\n  it('returns default values when config payload is not defined', () => {\n    expect(topicParamsTransformer(externalTopicPayload)).toEqual(\n      TOPIC_EDIT_FORM_DEFAULT_PROPS\n    );\n  });\n  it('returns default values when topic payload is not defined', () => {\n    expect(topicParamsTransformer(undefined, topicConfigPayload)).toEqual(\n      TOPIC_EDIT_FORM_DEFAULT_PROPS\n    );\n  });\n  it('returns transformed config', () => {\n    expect(\n      topicParamsTransformer(externalTopicPayload, topicConfigPayload)\n    ).toEqual({\n      ...TOPIC_EDIT_FORM_DEFAULT_PROPS,\n      name: externalTopicPayload.name,\n    });\n  });\n  it('returns default partitions config', () => {\n    expect(\n      topicParamsTransformer(\n        { ...externalTopicPayload, partitionCount: undefined },\n        topicConfigPayload\n      ).partitions\n    ).toEqual(TOPIC_EDIT_FORM_DEFAULT_PROPS.partitions);\n  });\n  it('returns empty list of custom params', () => {\n    expect(\n      topicParamsTransformer(externalTopicPayload, topicConfigPayload)\n        .customParams\n    ).toEqual([]);\n  });\n  it('returns list of custom params', () => {\n    expect(\n      topicParamsTransformer(externalTopicPayload, [\n        { ...topicConfigPayload[0], value: 'SuperCustom' },\n      ]).customParams\n    ).toEqual([{ name: 'compression.type', value: 'SuperCustom' }]);\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Edit/topicParamsTransformer.ts",
    "content": "import {\n  MILLISECONDS_IN_WEEK,\n  TOPIC_CUSTOM_PARAMS,\n  TOPIC_CUSTOM_PARAMS_PREFIX,\n} from 'lib/constants';\nimport { TOPIC_EDIT_FORM_DEFAULT_PROPS } from 'components/Topics/Topic/Edit/Edit';\nimport { getCleanUpPolicyValue } from 'components/Topics/shared/Form/TopicForm';\nimport { Topic, TopicConfig } from 'generated-sources';\n\nexport const getValue = (\n  config: TopicConfig[],\n  fieldName: string,\n  defaultValue?: number\n) =>\n  Number(config.find(({ name }) => name === fieldName)?.value) || defaultValue;\n\nconst topicParamsTransformer = (topic?: Topic, config?: TopicConfig[]) => {\n  if (!config || !topic) {\n    return TOPIC_EDIT_FORM_DEFAULT_PROPS;\n  }\n\n  const customParams = config.reduce((acc, { name, value, defaultValue }) => {\n    if (value === defaultValue) return acc;\n    if (!TOPIC_CUSTOM_PARAMS[name]) return acc;\n    return [...acc, { name, value }];\n  }, [] as { name: string; value?: string }[]);\n\n  return {\n    ...TOPIC_EDIT_FORM_DEFAULT_PROPS,\n    name: topic.name,\n    replicationFactor: topic.replicationFactor,\n    partitions:\n      topic.partitionCount || TOPIC_EDIT_FORM_DEFAULT_PROPS.partitions,\n    cleanupPolicy:\n      getCleanUpPolicyValue(topic.cleanUpPolicy) ||\n      TOPIC_EDIT_FORM_DEFAULT_PROPS.cleanupPolicy,\n    maxMessageBytes: getValue(config, 'max.message.bytes', 1000012),\n    minInSyncReplicas: getValue(config, 'min.insync.replicas', 1),\n    retentionBytes: getValue(config, 'retention.bytes', -1),\n    retentionMs: getValue(config, 'retention.ms', MILLISECONDS_IN_WEEK),\n\n    [TOPIC_CUSTOM_PARAMS_PREFIX]: customParams,\n  };\n};\nexport default topicParamsTransformer;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/AddEditFilterContainer.tsx",
    "content": "import React from 'react';\nimport * as S from 'components/Topics/Topic/Messages/Filters/Filters.styled';\nimport { InputLabel } from 'components/common/Input/InputLabel.styled';\nimport Input from 'components/common/Input/Input';\nimport { FormProvider, Controller, useForm } from 'react-hook-form';\nimport { ErrorMessage } from '@hookform/error-message';\nimport { Button } from 'components/common/Button/Button';\nimport { FormError } from 'components/common/Input/Input.styled';\nimport { AddMessageFilters } from 'components/Topics/Topic/Messages/Filters/AddFilter';\nimport Editor from 'components/common/Editor/Editor';\nimport { yupResolver } from '@hookform/resolvers/yup';\nimport yup from 'lib/yupExtended';\n\nconst validationSchema = yup.object().shape({\n  saveFilter: yup.boolean(),\n  code: yup.string().required(),\n  name: yup.string().when('saveFilter', {\n    is: (value: boolean | undefined) => typeof value === 'undefined' || value,\n    then: (schema) => schema.required(),\n    otherwise: (schema) => schema.notRequired(),\n  }),\n});\n\nexport interface AddEditFilterContainerProps {\n  cancelBtnHandler: () => void;\n  submitBtnText: string;\n  inputDisplayNameDefaultValue?: string;\n  inputCodeDefaultValue?: string;\n  isAdd?: boolean;\n  submitCallback?: (values: AddMessageFilters) => void;\n}\n\nconst AddEditFilterContainer: React.FC<AddEditFilterContainerProps> = ({\n  cancelBtnHandler,\n  submitBtnText,\n  inputDisplayNameDefaultValue = '',\n  inputCodeDefaultValue = '',\n  submitCallback,\n  isAdd,\n}) => {\n  const methods = useForm<AddMessageFilters>({\n    mode: 'onChange',\n    resolver: yupResolver(validationSchema),\n  });\n  const {\n    handleSubmit,\n    control,\n    formState: { isDirty, isSubmitting, isValid, errors },\n    reset,\n  } = methods;\n\n  const onSubmit = React.useCallback(\n    (values: AddMessageFilters) => {\n      try {\n        submitCallback?.(values);\n        reset({ name: '', code: '', saveFilter: false });\n      } catch (e) {\n        // do nothing\n      }\n    },\n    [isAdd, reset, submitCallback]\n  );\n\n  return (\n    <FormProvider {...methods}>\n      <form onSubmit={handleSubmit(onSubmit)} aria-label=\"Filters submit Form\">\n        <div>\n          <InputLabel>Filter code</InputLabel>\n          <Controller\n            control={control}\n            name=\"code\"\n            defaultValue={inputCodeDefaultValue}\n            render={({ field: { onChange, value } }) => (\n              <Editor\n                value={value}\n                minLines={5}\n                maxLines={28}\n                onChange={onChange}\n                setOptions={{\n                  showLineNumbers: false,\n                }}\n              />\n            )}\n          />\n        </div>\n        <div>\n          <FormError>\n            <ErrorMessage errors={errors} name=\"code\" />\n          </FormError>\n        </div>\n        {isAdd && (\n          <InputLabel>\n            <input {...methods.register('saveFilter')} type=\"checkbox\" />\n            Save this filter\n          </InputLabel>\n        )}\n        <div>\n          <InputLabel>Display name</InputLabel>\n          <Input\n            inputSize=\"M\"\n            placeholder=\"Enter Name\"\n            autoComplete=\"off\"\n            name=\"name\"\n            defaultValue={inputDisplayNameDefaultValue}\n          />\n        </div>\n        <div>\n          <FormError>\n            <ErrorMessage errors={errors} name=\"name\" />\n          </FormError>\n        </div>\n        <S.FilterButtonWrapper>\n          <Button\n            buttonSize=\"M\"\n            buttonType=\"secondary\"\n            type=\"button\"\n            onClick={cancelBtnHandler}\n          >\n            Cancel\n          </Button>\n          <Button\n            buttonSize=\"M\"\n            buttonType=\"primary\"\n            type=\"submit\"\n            disabled={!isValid || isSubmitting || !isDirty}\n          >\n            {submitBtnText}\n          </Button>\n        </S.FilterButtonWrapper>\n      </form>\n    </FormProvider>\n  );\n};\n\nexport default AddEditFilterContainer;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/AddFilter.tsx",
    "content": "import React from 'react';\nimport * as S from 'components/Topics/Topic/Messages/Filters/Filters.styled';\nimport { MessageFilters } from 'components/Topics/Topic/Messages/Filters/Filters';\nimport { FilterEdit } from 'components/Topics/Topic/Messages/Filters/FilterModal';\nimport SavedFilters from 'components/Topics/Topic/Messages/Filters/SavedFilters';\nimport SavedIcon from 'components/common/Icons/SavedIcon';\nimport QuestionIcon from 'components/common/Icons/QuestionIcon';\nimport useBoolean from 'lib/hooks/useBoolean';\nimport { showAlert } from 'lib/errorHandling';\n\nimport AddEditFilterContainer from './AddEditFilterContainer';\nimport InfoModal from './InfoModal';\n\nexport interface FilterModalProps {\n  toggleIsOpen(): void;\n  filters: MessageFilters[];\n  addFilter(values: MessageFilters): void;\n  deleteFilter(index: number): void;\n  activeFilterHandler(activeFilter: MessageFilters, index: number): void;\n  toggleEditModal(): void;\n  editFilter(value: FilterEdit): void;\n  isSavedFiltersOpen: boolean;\n  onClickSavedFilters(newValue: boolean): void;\n  activeFilter?: MessageFilters;\n}\n\nexport interface AddMessageFilters extends MessageFilters {\n  saveFilter: boolean;\n}\n\nconst AddFilter: React.FC<FilterModalProps> = ({\n  toggleIsOpen,\n  filters,\n  addFilter,\n  deleteFilter,\n  activeFilterHandler,\n  toggleEditModal,\n  editFilter,\n  isSavedFiltersOpen,\n  onClickSavedFilters,\n  activeFilter,\n}) => {\n  const { value: isOpen, toggle } = useBoolean();\n\n  const onSubmit = React.useCallback(\n    async (values: AddMessageFilters) => {\n      const isFilterExists = filters.some(\n        (filter) => filter.name === values.name\n      );\n\n      if (isFilterExists) {\n        showAlert('error', {\n          id: '',\n          title: 'Validation Error',\n          message: 'Filter with the same name already exists',\n        });\n        return;\n      }\n\n      const data = { ...values };\n      if (data.saveFilter) {\n        addFilter(data);\n      } else {\n        // other case is not applying the filter\n        const dataCodeLabel =\n          data.code.length > 16 ? `${data.code.slice(0, 16)}...` : data.code;\n        data.name = data.name || dataCodeLabel;\n\n        activeFilterHandler(data, -1);\n        toggleIsOpen();\n      }\n    },\n    [activeFilterHandler, addFilter, toggleIsOpen]\n  );\n  return (\n    <>\n      <S.FilterTitle>\n        Add filter\n        <div>\n          <S.QuestionIconContainer\n            type=\"button\"\n            aria-label=\"info\"\n            onClick={toggle}\n          >\n            <QuestionIcon />\n          </S.QuestionIconContainer>\n          {isOpen && <InfoModal toggleIsOpen={toggle} />}\n        </div>\n      </S.FilterTitle>\n      {isSavedFiltersOpen ? (\n        <SavedFilters\n          deleteFilter={deleteFilter}\n          activeFilterHandler={activeFilterHandler}\n          closeModal={toggleIsOpen}\n          onGoBack={() => onClickSavedFilters(!onClickSavedFilters)}\n          filters={filters}\n          onEdit={(index: number, filter: MessageFilters) => {\n            toggleEditModal();\n            editFilter({ index, filter });\n          }}\n          activeFilter={activeFilter}\n        />\n      ) : (\n        <>\n          <S.SavedFiltersTextContainer\n            onClick={() => onClickSavedFilters(!isSavedFiltersOpen)}\n          >\n            <SavedIcon /> <S.SavedFiltersText>Saved Filters</S.SavedFiltersText>\n          </S.SavedFiltersTextContainer>\n          <AddEditFilterContainer\n            cancelBtnHandler={toggleIsOpen}\n            submitBtnText=\"Add filter\"\n            submitCallback={onSubmit}\n            isAdd\n          />\n        </>\n      )}\n    </>\n  );\n};\n\nexport default AddFilter;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/EditFilter.tsx",
    "content": "import React from 'react';\nimport { MessageFilters } from 'components/Topics/Topic/Messages/Filters/Filters';\nimport { FilterEdit } from 'components/Topics/Topic/Messages/Filters/FilterModal';\n\nimport AddEditFilterContainer from './AddEditFilterContainer';\nimport * as S from './Filters.styled';\n\nexport interface EditFilterProps {\n  editFilter: FilterEdit;\n  toggleEditModal(): void;\n  editSavedFilter(filter: FilterEdit): void;\n}\n\nconst EditFilter: React.FC<EditFilterProps> = ({\n  editFilter,\n  toggleEditModal,\n  editSavedFilter,\n}) => {\n  const onSubmit = (values: MessageFilters) => {\n    editSavedFilter({ index: editFilter.index, filter: values });\n    toggleEditModal();\n  };\n  return (\n    <>\n      <S.FilterTitle>Edit filter</S.FilterTitle>\n      <AddEditFilterContainer\n        cancelBtnHandler={() => toggleEditModal()}\n        submitBtnText=\"Save\"\n        inputDisplayNameDefaultValue={editFilter.filter.name}\n        inputCodeDefaultValue={editFilter.filter.code}\n        submitCallback={onSubmit}\n      />\n    </>\n  );\n};\n\nexport default EditFilter;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/FilterModal.tsx",
    "content": "import React from 'react';\nimport * as S from 'components/Topics/Topic/Messages/Filters/Filters.styled';\nimport {\n  ActiveMessageFilter,\n  MessageFilters,\n} from 'components/Topics/Topic/Messages/Filters/Filters';\nimport AddFilter from 'components/Topics/Topic/Messages/Filters/AddFilter';\nimport EditFilter from 'components/Topics/Topic/Messages/Filters/EditFilter';\n\nexport interface FilterModalProps {\n  toggleIsOpen(): void;\n  filters: MessageFilters[];\n  addFilter(values: MessageFilters): void;\n  deleteFilter(index: number): void;\n  activeFilterHandler(activeFilter: MessageFilters, index: number): void;\n  editSavedFilter(filter: FilterEdit): void;\n  activeFilter: ActiveMessageFilter;\n  quickEditMode?: boolean;\n}\n\nexport interface FilterEdit {\n  index: number;\n  filter: MessageFilters;\n}\n\nconst FilterModal: React.FC<FilterModalProps> = ({\n  toggleIsOpen,\n  filters,\n  addFilter,\n  deleteFilter,\n  activeFilterHandler,\n  editSavedFilter,\n  activeFilter,\n  quickEditMode = false,\n}) => {\n  const [isInEditMode, setIsInEditMode] =\n    React.useState<boolean>(quickEditMode);\n  const [isSavedFiltersOpen, setIsSavedFiltersOpen] =\n    React.useState<boolean>(false);\n\n  const toggleEditModal = () => {\n    setIsInEditMode(!isInEditMode);\n  };\n\n  const [editFilter, setEditFilter] = React.useState<FilterEdit>(() => {\n    const { index, name, code } = activeFilter;\n    return quickEditMode\n      ? { index, filter: { name, code } }\n      : { index: -1, filter: { name: '', code: '' } };\n  });\n  const editFilterHandler = (value: FilterEdit) => {\n    setEditFilter(value);\n    setIsInEditMode(!isInEditMode);\n  };\n\n  const toggleEditModalHandler = quickEditMode ? toggleIsOpen : toggleEditModal;\n\n  return (\n    <S.MessageFilterModal data-testid=\"messageFilterModal\">\n      {isInEditMode ? (\n        <EditFilter\n          editFilter={editFilter}\n          toggleEditModal={toggleEditModalHandler}\n          editSavedFilter={editSavedFilter}\n        />\n      ) : (\n        <AddFilter\n          toggleIsOpen={toggleIsOpen}\n          filters={filters}\n          addFilter={addFilter}\n          deleteFilter={deleteFilter}\n          activeFilterHandler={activeFilterHandler}\n          toggleEditModal={toggleEditModal}\n          editFilter={editFilterHandler}\n          isSavedFiltersOpen={isSavedFiltersOpen}\n          onClickSavedFilters={() => setIsSavedFiltersOpen(!isSavedFiltersOpen)}\n          activeFilter={activeFilter}\n        />\n      )}\n    </S.MessageFilterModal>\n  );\n};\n\nexport default FilterModal;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/Filters.styled.ts",
    "content": "import Input from 'components/common/Input/Input';\nimport Select from 'components/common/Select/Select';\nimport styled, { css } from 'styled-components';\nimport DatePicker from 'react-datepicker';\nimport EditIcon from 'components/common/Icons/EditIcon';\nimport closeIcon from 'components/common/Icons/CloseIcon';\n\ninterface SavedFilterProps {\n  selected: boolean;\n}\ninterface MessageLoadingProps {\n  isLive: boolean;\n}\n\ninterface MessageLoadingSpinnerProps {\n  isFetching: boolean;\n}\n\nexport const FiltersWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  padding-left: 16px;\n  padding-right: 16px;\n\n  & > div:first-child {\n    display: flex;\n    justify-content: space-between;\n    padding-top: 2px;\n    align-items: flex-end;\n  }\n`;\n\nexport const FilterInputs = styled.div`\n  display: flex;\n  gap: 8px;\n  align-items: flex-end;\n  width: 90%;\n  flex-wrap: wrap;\n`;\n\nexport const SeekTypeSelectorWrapper = styled.div`\n  display: flex;\n  & .select-wrapper {\n    width: 40% !important;\n    & > select {\n      border-radius: 4px 0 0 4px !important;\n    }\n  }\n`;\n\nexport const OffsetSelector = styled(Input)`\n  border-radius: 0 4px 4px 0 !important;\n  &::placeholder {\n    color: ${({ theme }) => theme.input.color.normal};\n  }\n`;\n\nexport const DatePickerInput = styled(DatePicker)`\n  height: 32px;\n  border: 1px ${({ theme }) => theme.select.borderColor.normal} solid;\n  border-left: none;\n  border-radius: 0 4px 4px 0;\n  font-size: 14px;\n  width: 100%;\n  padding-left: 12px;\n  background-color: ${({ theme }) => theme.input.backgroundColor.normal};\n  color: ${({ theme }) => theme.input.color.normal};\n  &::placeholder {\n    color: ${({ theme }) => theme.input.color.normal};\n  }\n\n  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;\n  background-repeat: no-repeat !important;\n  background-position-x: 96% !important;\n  background-position-y: 55% !important;\n  appearance: none !important;\n\n  &:hover {\n    cursor: pointer;\n  }\n  &:focus {\n    outline: none;\n  }\n`;\n\nexport const FiltersMetrics = styled.div`\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n  gap: 22px;\n  padding-top: 16px;\n  padding-bottom: 16px;\n`;\nexport const Message = styled.div`\n  font-size: 14px;\n  color: ${({ theme }) => theme.metrics.filters.color.normal};\n`;\nexport const Metric = styled.div`\n  color: ${({ theme }) => theme.metrics.filters.color.normal};\n  font-size: 12px;\n  display: flex;\n`;\n\nexport const MetricsIcon = styled.div`\n  color: ${({ theme }) => theme.metrics.filters.color.icon};\n  padding-right: 6px;\n  height: 12px;\n`;\n\nexport const ClearAll = styled.div`\n  color: ${({ theme }) => theme.metrics.filters.color.normal};\n  font-size: 12px;\n  cursor: pointer;\n  line-height: 32px;\n  margin-left: 8px;\n`;\n\nexport const ButtonContainer = styled.div`\n  width: 100%;\n  display: flex;\n  justify-content: center;\n  margin-top: 20px;\n`;\n\nexport const ListItem = styled.li`\n  font-size: 12px;\n  font-weight: 400;\n  margin: 4px 0;\n  line-height: 1.5;\n  color: ${({ theme }) => theme.table.td.color.normal};\n`;\n\nexport const InfoParagraph = styled.div`\n  font-size: 12px;\n  font-weight: 400;\n  line-height: 1.5;\n  margin-bottom: 10px;\n  color: ${({ theme }) => theme.table.td.color.normal};\n`;\n\nexport const MessageFilterModal = styled.div`\n  height: auto;\n  width: 560px;\n  border-radius: 8px;\n  background: ${({ theme }) => theme.modal.backgroundColor};\n  position: absolute;\n  left: 25%;\n  border: 1px solid ${({ theme }) => theme.modal.border.contrast};\n  box-shadow: ${({ theme }) => theme.modal.shadow};\n  padding: 16px;\n  z-index: 1;\n`;\n\nexport const InfoModal = styled.div`\n  height: auto;\n  width: 560px;\n  border-radius: 8px;\n  background: ${({ theme }) => theme.modal.backgroundColor};\n  position: absolute;\n  left: 25%;\n  border: 1px solid ${({ theme }) => theme.modal.border.contrast};\n  box-shadow: ${({ theme }) => theme.modal.shadow};\n  padding: 32px;\n  z-index: 1;\n`;\n\nexport const QuestionIconContainer = styled.button`\n  cursor: pointer;\n  padding: 0;\n  background: none;\n  border: none;\n`;\n\nexport const FilterTitle = styled.h3`\n  line-height: 32px;\n  font-size: 20px;\n  margin-bottom: 40px;\n  position: relative;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  color: ${({ theme }) => theme.modal.color};\n  &:after {\n    content: '';\n    width: calc(100% + 32px);\n    height: 1px;\n    position: absolute;\n    top: 40px;\n    left: -16px;\n    display: inline-block;\n    background-color: ${({ theme }) => theme.modal.border.top};\n  }\n`;\n\nexport const CreatedFilter = styled.p`\n  margin: 25px 0 10px;\n  font-size: 14px;\n  line-height: 20px;\n  color: ${({ theme }) => theme.savedFilter.color};\n`;\n\nexport const NoSavedFilter = styled.p`\n  color: ${({ theme }) => theme.savedFilter.color};\n`;\nexport const SavedFiltersContainer = styled.div`\n  overflow-y: auto;\n  height: 195px;\n  justify-content: space-around;\n  padding-left: 10px;\n`;\n\nexport const SavedFilterName = styled.div`\n  font-size: 14px;\n  line-height: 20px;\n  color: ${({ theme }) => theme.savedFilter.filterName};\n`;\n\nexport const FilterButtonWrapper = styled.div`\n  display: flex;\n  justify-content: flex-end;\n  margin-top: 10px;\n  gap: 10px;\n  padding-top: 16px;\n  position: relative;\n  &:before {\n    content: '';\n    width: calc(100% + 32px);\n    height: 1px;\n    position: absolute;\n    top: 0;\n    left: -16px;\n    display: inline-block;\n    background-color: ${({ theme }) => theme.modal.border.bottom};\n  }\n`;\n\nexport const ActiveSmartFilterWrapper = styled.div`\n  padding: 8px 0 5px;\n  display: flex;\n  gap: 10px;\n  align-items: center;\n  justify-content: flex-start;\n`;\n\nexport const DeleteSavedFilter = styled.div.attrs({ role: 'deleteIcon' })`\n  margin-top: 2px;\n  cursor: pointer;\n  color: ${({ theme }) => theme.icons.deleteIcon};\n`;\n\nexport const FilterEdit = styled.div`\n  font-weight: 500;\n  font-size: 14px;\n  line-height: 20px;\n`;\n\nexport const FilterOptions = styled.div`\n  display: none;\n  width: 50px;\n  justify-content: space-between;\n  color: ${({ theme }) => theme.editFilter.textColor};\n`;\n\nexport const SavedFilter = styled.div.attrs({\n  role: 'savedFilter',\n})<SavedFilterProps>`\n  display: flex;\n  justify-content: space-between;\n  padding-right: 5px;\n  height: 32px;\n  align-items: center;\n  cursor: pointer;\n  border-top: 1px solid ${({ theme }) => theme.panelColor.borderTop};\n  &:hover ${FilterOptions} {\n    display: flex;\n  }\n  &:hover {\n    background: ${({ theme }) => theme.layout.stuffColor};\n  }\n  background: ${({ selected, theme }) =>\n    selected ? theme.layout.stuffColor : theme.modal.backgroundColor};\n`;\n\nexport const ActiveSmartFilter = styled.div`\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  height: 32px;\n  color: ${({ theme }) => theme.activeFilter.color};\n  background: ${({ theme }) => theme.activeFilter.backgroundColor};\n  border-radius: 4px;\n  font-size: 14px;\n  line-height: 20px;\n`;\n\nexport const EditSmartFilterIcon = styled.div(\n  ({ theme: { icons } }) => css`\n    color: ${icons.editIcon.normal};\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    height: 32px;\n    width: 32px;\n    cursor: pointer;\n    border-left: 1px solid ${icons.editIcon.border};\n\n    &:hover {\n      ${EditIcon} {\n        fill: ${icons.editIcon.hover};\n      }\n    }\n\n    &:active {\n      ${EditIcon} {\n        fill: ${icons.editIcon.active};\n      }\n    }\n  `\n);\n\nexport const SmartFilterName = styled.div`\n  padding: 0 8px;\n  min-width: 32px;\n`;\n\nexport const DeleteSmartFilterIcon = styled.div(\n  ({ theme: { icons } }) => css`\n    color: ${icons.closeIcon.normal};\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    height: 32px;\n    width: 32px;\n    cursor: pointer;\n    border-left: 1px solid ${icons.closeIcon.border};\n\n    svg {\n      height: 14px;\n      width: 14px;\n    }\n\n    &:hover {\n      ${closeIcon} {\n        fill: ${icons.closeIcon.hover};\n      }\n    }\n\n    &:active {\n      ${closeIcon} {\n        fill: ${icons.closeIcon.active};\n      }\n    }\n  `\n);\n\nexport const MessageLoading = styled.div.attrs({\n  role: 'contentLoader',\n})<MessageLoadingProps>`\n  color: ${({ theme }) => theme.heading.h3.color};\n  font-size: ${({ theme }) => theme.heading.h3.fontSize};\n  display: ${({ isLive }) => (isLive ? 'flex' : 'none')};\n  justify-content: space-around;\n  width: 250px;\n`;\n\nexport const StopLoading = styled.div`\n  color: ${({ theme }) => theme.pageLoader.borderColor};\n  font-size: ${({ theme }) => theme.heading.h3.fontSize};\n  cursor: pointer;\n`;\n\nexport const MessageLoadingSpinner = styled.div<MessageLoadingSpinnerProps>`\n  display: ${({ isFetching }) => (isFetching ? 'block' : 'none')};\n  border: 3px solid ${({ theme }) => theme.pageLoader.borderColor};\n  border-bottom: 3px solid ${({ theme }) => theme.pageLoader.borderBottomColor};\n  border-radius: 50%;\n  width: 20px;\n  height: 20px;\n  animation: spin 1.3s linear infinite;\n\n  @keyframes spin {\n    0% {\n      transform: rotate(0deg);\n    }\n    100% {\n      transform: rotate(360deg);\n    }\n  }\n`;\n\nexport const SavedFiltersTextContainer = styled.div.attrs({\n  role: 'savedFilterText',\n})`\n  display: flex;\n  align-items: center;\n  cursor: pointer;\n  margin-bottom: 15px;\n`;\n\nconst textStyle = css`\n  font-size: 14px;\n  color: ${({ theme }) => theme.editFilter.textColor};\n  font-weight: 500;\n`;\n\nexport const SavedFiltersText = styled.div`\n  ${textStyle};\n  margin-left: 7px;\n`;\n\nexport const BackToCustomText = styled.div`\n  ${textStyle};\n  cursor: pointer;\n`;\n\nexport const SeekTypeSelect = styled(Select)`\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n  user-select: none;\n`;\n\nexport const Serdes = styled.div`\n  display: flex;\n  gap: 24px;\n  padding: 8px 0;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/Filters.tsx",
    "content": "import 'react-datepicker/dist/react-datepicker.css';\n\nimport {\n  MessageFilterType,\n  Partition,\n  SeekDirection,\n  SeekType,\n  SerdeUsage,\n  TopicMessage,\n  TopicMessageConsuming,\n  TopicMessageEvent,\n  TopicMessageEventTypeEnum,\n} from 'generated-sources';\nimport React, { useContext } from 'react';\nimport omitBy from 'lodash/omitBy';\nimport { useNavigate, useLocation, useSearchParams } from 'react-router-dom';\nimport MultiSelect from 'components/common/MultiSelect/MultiSelect.styled';\nimport { Option } from 'react-multi-select-component';\nimport BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';\nimport { BASE_PARAMS } from 'lib/constants';\nimport Select from 'components/common/Select/Select';\nimport { Button } from 'components/common/Button/Button';\nimport Search from 'components/common/Search/Search';\nimport FilterModal, {\n  FilterEdit,\n} from 'components/Topics/Topic/Messages/Filters/FilterModal';\nimport { SeekDirectionOptions } from 'components/Topics/Topic/Messages/Messages';\nimport TopicMessagesContext from 'components/contexts/TopicMessagesContext';\nimport useBoolean from 'lib/hooks/useBoolean';\nimport { RouteParamsClusterTopic } from 'lib/paths';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport PlusIcon from 'components/common/Icons/PlusIcon';\nimport EditIcon from 'components/common/Icons/EditIcon';\nimport CloseIcon from 'components/common/Icons/CloseIcon';\nimport ClockIcon from 'components/common/Icons/ClockIcon';\nimport ArrowDownIcon from 'components/common/Icons/ArrowDownIcon';\nimport FileIcon from 'components/common/Icons/FileIcon';\nimport { useTopicDetails } from 'lib/hooks/api/topics';\nimport { InputLabel } from 'components/common/Input/InputLabel.styled';\nimport { getSerdeOptions } from 'components/Topics/Topic/SendMessage/utils';\nimport { useSerdes } from 'lib/hooks/api/topicMessages';\n\nimport * as S from './Filters.styled';\nimport {\n  filterOptions,\n  getOffsetFromSeekToParam,\n  getSelectedPartitionsFromSeekToParam,\n  getTimestampFromSeekToParam,\n} from './utils';\n\ntype Query = Record<string, string | string[] | number>;\n\nexport interface FiltersProps {\n  phaseMessage?: string;\n  meta: TopicMessageConsuming;\n  isFetching: boolean;\n  messageEventType?: string;\n  addMessage(content: { message: TopicMessage; prepend: boolean }): void;\n  resetMessages(): void;\n  updatePhase(phase: string): void;\n  updateMeta(meta: TopicMessageConsuming): void;\n  setIsFetching(status: boolean): void;\n  setMessageType(messageType: string): void;\n}\n\nexport interface MessageFilters {\n  name: string;\n  code: string;\n}\n\nexport interface ActiveMessageFilter {\n  index: number;\n  name: string;\n  code: string;\n}\n\nconst PER_PAGE = 100;\n\nexport const SeekTypeOptions = [\n  { value: SeekType.OFFSET, label: 'Offset' },\n  { value: SeekType.TIMESTAMP, label: 'Timestamp' },\n];\n\nconst Filters: React.FC<FiltersProps> = ({\n  phaseMessage,\n  meta: { elapsedMs, bytesConsumed, messagesConsumed, filterApplyErrors },\n  isFetching,\n  addMessage,\n  resetMessages,\n  updatePhase,\n  updateMeta,\n  setIsFetching,\n  setMessageType,\n  messageEventType,\n}) => {\n  const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();\n  const location = useLocation();\n  const navigate = useNavigate();\n  const [searchParams] = useSearchParams();\n\n  const page = searchParams.get('page');\n\n  const { data: topic } = useTopicDetails({ clusterName, topicName });\n\n  const partitions = topic?.partitions || [];\n\n  const { seekDirection, isLive, changeSeekDirection } =\n    useContext(TopicMessagesContext);\n\n  const { value: isOpen, toggle } = useBoolean();\n\n  const { value: isQuickEditOpen, toggle: toggleQuickEdit } = useBoolean();\n\n  const source = React.useRef<EventSource | null>(null);\n\n  const [selectedPartitions, setSelectedPartitions] = React.useState<Option[]>(\n    getSelectedPartitionsFromSeekToParam(searchParams, partitions)\n  );\n\n  const [currentSeekType, setCurrentSeekType] = React.useState<SeekType>(\n    SeekTypeOptions.find(\n      (ele) => ele.value === (searchParams.get('seekType') as SeekType)\n    ) !== undefined\n      ? (searchParams.get('seekType') as SeekType)\n      : SeekType.OFFSET\n  );\n  const [offset, setOffset] = React.useState<string>(\n    getOffsetFromSeekToParam(searchParams)\n  );\n\n  const [timestamp, setTimestamp] = React.useState<Date | null>(\n    getTimestampFromSeekToParam(searchParams)\n  );\n  const [keySerde, setKeySerde] = React.useState<string>(\n    searchParams.get('keySerde') || ''\n  );\n  const [valueSerde, setValueSerde] = React.useState<string>(\n    searchParams.get('valueSerde') || ''\n  );\n\n  const [savedFilters, setSavedFilters] = React.useState<MessageFilters[]>(\n    JSON.parse(localStorage.getItem('savedFilters') ?? '[]')\n  );\n\n  let storageActiveFilter = localStorage.getItem('activeFilter');\n  storageActiveFilter =\n    storageActiveFilter ?? JSON.stringify({ name: '', code: '', index: -1 });\n\n  const [activeFilter, setActiveFilter] = React.useState<ActiveMessageFilter>(\n    JSON.parse(storageActiveFilter)\n  );\n\n  const [queryType, setQueryType] = React.useState<MessageFilterType>(\n    activeFilter.name\n      ? MessageFilterType.GROOVY_SCRIPT\n      : MessageFilterType.STRING_CONTAINS\n  );\n  const [query, setQuery] = React.useState<string>(searchParams.get('q') || '');\n  const [isTailing, setIsTailing] = React.useState<boolean>(isLive);\n\n  const isSeekTypeControlVisible = React.useMemo(\n    () => selectedPartitions.length > 0,\n    [selectedPartitions]\n  );\n\n  const isSubmitDisabled = React.useMemo(() => {\n    if (isSeekTypeControlVisible) {\n      return (\n        (currentSeekType === SeekType.TIMESTAMP && !timestamp) || isTailing\n      );\n    }\n\n    return false;\n  }, [isSeekTypeControlVisible, currentSeekType, timestamp, isTailing]);\n\n  const partitionMap = React.useMemo(\n    () =>\n      partitions.reduce<Record<string, Partition>>(\n        (acc, partition) => ({\n          ...acc,\n          [partition.partition]: partition,\n        }),\n        {}\n      ),\n    [partitions]\n  );\n\n  const handleClearAllFilters = () => {\n    setCurrentSeekType(SeekType.OFFSET);\n    setOffset('');\n    setTimestamp(null);\n    setQuery('');\n    changeSeekDirection(SeekDirection.FORWARD);\n    getSelectedPartitionsFromSeekToParam(searchParams, partitions);\n    setSelectedPartitions(\n      partitions.map((partition: Partition) => {\n        return {\n          value: partition.partition,\n          label: `Partition #${partition.partition.toString()}`,\n        };\n      })\n    );\n  };\n\n  const handleFiltersSubmit = (currentOffset: string) => {\n    const nextAttempt = Number(searchParams.get('attempt') || 0) + 1;\n    const props: Query = {\n      q:\n        queryType === MessageFilterType.GROOVY_SCRIPT\n          ? activeFilter.code\n          : query,\n      filterQueryType: queryType,\n      attempt: nextAttempt,\n      limit: PER_PAGE,\n      page: page || 0,\n      seekDirection,\n      keySerde: keySerde || searchParams.get('keySerde') || '',\n      valueSerde: valueSerde || searchParams.get('valueSerde') || '',\n    };\n\n    if (isSeekTypeControlVisible) {\n      switch (seekDirection) {\n        case SeekDirection.FORWARD:\n          props.seekType = SeekType.BEGINNING;\n          break;\n        case SeekDirection.BACKWARD:\n        case SeekDirection.TAILING:\n          props.seekType = SeekType.LATEST;\n          break;\n        default:\n          props.seekType = currentSeekType;\n      }\n\n      if (offset && currentSeekType === SeekType.OFFSET) {\n        props.seekType = SeekType.OFFSET;\n      }\n\n      if (timestamp && currentSeekType === SeekType.TIMESTAMP) {\n        props.seekType = SeekType.TIMESTAMP;\n      }\n\n      const isSeekTypeWithSeekTo =\n        props.seekType === SeekType.TIMESTAMP ||\n        props.seekType === SeekType.OFFSET;\n\n      if (\n        selectedPartitions.length !== partitions.length ||\n        isSeekTypeWithSeekTo\n      ) {\n        // not everything in the partition is selected\n        props.seekTo = selectedPartitions.map(({ value }) => {\n          const offsetProperty =\n            seekDirection === SeekDirection.FORWARD ? 'offsetMin' : 'offsetMax';\n          const offsetBasedSeekTo =\n            currentOffset || partitionMap[value][offsetProperty];\n          const seekToOffset =\n            currentSeekType === SeekType.OFFSET\n              ? offsetBasedSeekTo\n              : timestamp?.getTime();\n\n          return `${value}::${seekToOffset || '0'}`;\n        });\n      }\n    }\n\n    const newProps = omitBy(props, (v) => v === undefined || v === '');\n    const qs = Object.keys(newProps)\n      .map((key) => `${key}=${encodeURIComponent(newProps[key] as string)}`)\n      .join('&');\n    navigate({\n      search: `?${qs}`,\n    });\n  };\n\n  const handleSSECancel = () => {\n    if (!source.current) return;\n    setIsFetching(false);\n    source.current.close();\n  };\n\n  const addFilter = (newFilter: MessageFilters) => {\n    const filters = [...savedFilters];\n    filters.push(newFilter);\n    setSavedFilters(filters);\n    localStorage.setItem('savedFilters', JSON.stringify(filters));\n  };\n  const deleteFilter = (index: number) => {\n    const filters = [...savedFilters];\n    if (activeFilter.name && activeFilter.index === index) {\n      localStorage.removeItem('activeFilter');\n      setActiveFilter({ name: '', code: '', index: -1 });\n      setQueryType(MessageFilterType.STRING_CONTAINS);\n    }\n    filters.splice(index, 1);\n    localStorage.setItem('savedFilters', JSON.stringify(filters));\n    setSavedFilters(filters);\n  };\n  const deleteActiveFilter = () => {\n    setActiveFilter({ name: '', code: '', index: -1 });\n    localStorage.removeItem('activeFilter');\n    setQueryType(MessageFilterType.STRING_CONTAINS);\n  };\n  const activeFilterHandler = (\n    newActiveFilter: MessageFilters,\n    index: number\n  ) => {\n    localStorage.setItem(\n      'activeFilter',\n      JSON.stringify({ index, ...newActiveFilter })\n    );\n    setActiveFilter({ index, ...newActiveFilter });\n    setQueryType(MessageFilterType.GROOVY_SCRIPT);\n  };\n\n  const composeMessageFilter = (filter: FilterEdit): ActiveMessageFilter => ({\n    index: filter.index,\n    name: filter.filter.name,\n    code: filter.filter.code,\n  });\n\n  const storeAsActiveFilter = (filter: FilterEdit) => {\n    const messageFilter = JSON.stringify(composeMessageFilter(filter));\n    localStorage.setItem('activeFilter', messageFilter);\n  };\n\n  const editSavedFilter = (filter: FilterEdit) => {\n    const filters = [...savedFilters];\n    filters[filter.index] = filter.filter;\n    if (activeFilter.name && activeFilter.index === filter.index) {\n      setActiveFilter(composeMessageFilter(filter));\n      storeAsActiveFilter(filter);\n    }\n    localStorage.setItem('savedFilters', JSON.stringify(filters));\n    setSavedFilters(filters);\n  };\n\n  const editCurrentFilter = (filter: FilterEdit) => {\n    if (filter.index < 0) {\n      setActiveFilter(composeMessageFilter(filter));\n      storeAsActiveFilter(filter);\n    } else {\n      editSavedFilter(filter);\n    }\n  };\n  // eslint-disable-next-line consistent-return\n  React.useEffect(() => {\n    if (location.search?.length !== 0) {\n      const url = `${BASE_PARAMS.basePath}/api/clusters/${encodeURIComponent(\n        clusterName\n      )}/topics/${topicName}/messages${location.search}`;\n      const sse = new EventSource(url);\n\n      source.current = sse;\n      setIsFetching(true);\n\n      sse.onopen = () => {\n        resetMessages();\n        setIsFetching(true);\n      };\n      sse.onmessage = ({ data }) => {\n        const { type, message, phase, consuming }: TopicMessageEvent =\n          JSON.parse(data);\n        switch (type) {\n          case TopicMessageEventTypeEnum.MESSAGE:\n            if (message) {\n              addMessage({\n                message,\n                prepend: isLive,\n              });\n            }\n            break;\n          case TopicMessageEventTypeEnum.PHASE:\n            if (phase?.name) {\n              updatePhase(phase.name);\n            }\n            break;\n          case TopicMessageEventTypeEnum.CONSUMING:\n            if (consuming) updateMeta(consuming);\n            break;\n          case TopicMessageEventTypeEnum.DONE:\n            if (consuming && type) {\n              setMessageType(type);\n              updateMeta(consuming);\n            }\n            break;\n          default:\n        }\n      };\n\n      sse.onerror = () => {\n        setIsFetching(false);\n        sse.close();\n      };\n\n      return () => {\n        setIsFetching(false);\n        sse.close();\n      };\n    }\n  }, [\n    clusterName,\n    topicName,\n    seekDirection,\n    location,\n    addMessage,\n    resetMessages,\n    setIsFetching,\n    updateMeta,\n    updatePhase,\n  ]);\n  React.useEffect(() => {\n    if (location.search?.length === 0) {\n      handleFiltersSubmit(offset);\n    }\n  }, [\n    seekDirection,\n    queryType,\n    activeFilter,\n    currentSeekType,\n    timestamp,\n    query,\n    location,\n  ]);\n  React.useEffect(() => {\n    handleFiltersSubmit(offset);\n  }, [\n    seekDirection,\n    queryType,\n    activeFilter,\n    currentSeekType,\n    timestamp,\n    query,\n    seekDirection,\n    page,\n  ]);\n\n  React.useEffect(() => {\n    setIsTailing(isLive);\n  }, [isLive]);\n\n  const { data: serdes = {} } = useSerdes({\n    clusterName,\n    topicName,\n    use: SerdeUsage.DESERIALIZE,\n  });\n\n  return (\n    <S.FiltersWrapper>\n      <div>\n        <S.FilterInputs>\n          <div>\n            <InputLabel>Seek Type</InputLabel>\n            <S.SeekTypeSelectorWrapper>\n              <S.SeekTypeSelect\n                id=\"selectSeekType\"\n                onChange={(option) => setCurrentSeekType(option as SeekType)}\n                value={currentSeekType}\n                selectSize=\"M\"\n                minWidth=\"100px\"\n                options={SeekTypeOptions}\n                disabled={isTailing}\n              />\n\n              {currentSeekType === SeekType.OFFSET ? (\n                <S.OffsetSelector\n                  id=\"offset\"\n                  type=\"text\"\n                  inputSize=\"M\"\n                  value={offset}\n                  placeholder=\"Offset\"\n                  onChange={({ target: { value } }) => setOffset(value)}\n                  disabled={isTailing}\n                />\n              ) : (\n                <S.DatePickerInput\n                  selected={timestamp}\n                  onChange={(date: Date | null) => setTimestamp(date)}\n                  showTimeInput\n                  timeInputLabel=\"Time:\"\n                  dateFormat=\"MMM d, yyyy HH:mm\"\n                  placeholderText=\"Select timestamp\"\n                  disabled={isTailing}\n                />\n              )}\n            </S.SeekTypeSelectorWrapper>\n          </div>\n          <div>\n            <InputLabel>Partitions</InputLabel>\n            <MultiSelect\n              options={partitions.map((p) => ({\n                label: `Partition #${p.partition.toString()}`,\n                value: p.partition,\n              }))}\n              filterOptions={filterOptions}\n              value={selectedPartitions}\n              onChange={setSelectedPartitions}\n              labelledBy=\"Select partitions\"\n              disabled={isTailing}\n            />\n          </div>\n          <div>\n            <InputLabel>Key Serde</InputLabel>\n            <Select\n              id=\"selectKeySerdeOptions\"\n              aria-labelledby=\"selectKeySerdeOptions\"\n              onChange={(option) => setKeySerde(option as string)}\n              minWidth=\"170px\"\n              options={getSerdeOptions(serdes.key || [])}\n              value={searchParams.get('keySerde') as string}\n              selectSize=\"M\"\n              disabled={isTailing}\n            />\n          </div>\n          <div>\n            <InputLabel>Value Serde</InputLabel>\n            <Select\n              id=\"selectValueSerdeOptions\"\n              aria-labelledby=\"selectValueSerdeOptions\"\n              onChange={(option) => setValueSerde(option as string)}\n              options={getSerdeOptions(serdes.value || [])}\n              value={searchParams.get('valueSerde') as string}\n              minWidth=\"170px\"\n              selectSize=\"M\"\n              disabled={isTailing}\n            />\n          </div>\n          <S.ClearAll onClick={handleClearAllFilters}>Clear all</S.ClearAll>\n          <Button\n            type=\"submit\"\n            buttonType=\"secondary\"\n            buttonSize=\"M\"\n            disabled={isSubmitDisabled}\n            onClick={() =>\n              isFetching ? handleSSECancel() : handleFiltersSubmit(offset)\n            }\n            style={{ fontWeight: 500 }}\n          >\n            {isFetching ? 'Cancel' : 'Submit'}\n          </Button>\n        </S.FilterInputs>\n        <Select\n          selectSize=\"M\"\n          onChange={(option) => changeSeekDirection(option as string)}\n          value={seekDirection}\n          minWidth=\"120px\"\n          options={SeekDirectionOptions}\n          isLive={isLive}\n        />\n      </div>\n      <S.ActiveSmartFilterWrapper>\n        <Search placeholder=\"Search\" disabled={isTailing} />\n\n        <Button buttonType=\"secondary\" buttonSize=\"M\" onClick={toggle}>\n          <PlusIcon />\n          Add Filters\n        </Button>\n        {activeFilter.name && (\n          <S.ActiveSmartFilter data-testid=\"activeSmartFilter\">\n            <S.SmartFilterName>{activeFilter.name}</S.SmartFilterName>\n            <S.EditSmartFilterIcon\n              data-testid=\"editActiveSmartFilterBtn\"\n              onClick={toggleQuickEdit}\n            >\n              <EditIcon />\n            </S.EditSmartFilterIcon>\n            <S.DeleteSmartFilterIcon onClick={deleteActiveFilter}>\n              <CloseIcon />\n            </S.DeleteSmartFilterIcon>\n          </S.ActiveSmartFilter>\n        )}\n      </S.ActiveSmartFilterWrapper>\n      {isQuickEditOpen && (\n        <FilterModal\n          quickEditMode\n          activeFilter={activeFilter}\n          toggleIsOpen={toggleQuickEdit}\n          editSavedFilter={editCurrentFilter}\n          filters={[]}\n          addFilter={() => null}\n          deleteFilter={() => null}\n          activeFilterHandler={() => null}\n        />\n      )}\n\n      {isOpen && (\n        <FilterModal\n          toggleIsOpen={toggle}\n          filters={savedFilters}\n          addFilter={addFilter}\n          deleteFilter={deleteFilter}\n          activeFilterHandler={activeFilterHandler}\n          editSavedFilter={editSavedFilter}\n          activeFilter={activeFilter}\n        />\n      )}\n      <S.FiltersMetrics>\n        <S.Message>\n          {seekDirection !== SeekDirection.TAILING &&\n            isFetching &&\n            phaseMessage}\n          {!isFetching && messageEventType}\n        </S.Message>\n        <S.MessageLoading isLive={isTailing}>\n          <S.MessageLoadingSpinner isFetching={isFetching} />\n          Loading messages.\n          <S.StopLoading\n            onClick={() => {\n              handleSSECancel();\n              setIsTailing(false);\n            }}\n          >\n            Stop loading\n          </S.StopLoading>\n        </S.MessageLoading>\n        <S.Metric title=\"Elapsed Time\">\n          <S.MetricsIcon>\n            <ClockIcon />\n          </S.MetricsIcon>\n          <span>{Math.max(elapsedMs || 0, 0)} ms</span>\n        </S.Metric>\n        <S.Metric title=\"Bytes Consumed\">\n          <S.MetricsIcon>\n            <ArrowDownIcon />\n          </S.MetricsIcon>\n          <BytesFormatted value={bytesConsumed} />\n        </S.Metric>\n        <S.Metric title=\"Messages Consumed\">\n          <S.MetricsIcon>\n            <FileIcon />\n          </S.MetricsIcon>\n          <span>{messagesConsumed} messages consumed</span>\n        </S.Metric>\n        {!!filterApplyErrors && (\n          <S.Metric title=\"Errors\">\n            <span>{filterApplyErrors} errors</span>\n          </S.Metric>\n        )}\n      </S.FiltersMetrics>\n    </S.FiltersWrapper>\n  );\n};\n\nexport default Filters;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/FiltersContainer.ts",
    "content": "import { connect } from 'react-redux';\nimport { RootState } from 'redux/interfaces';\nimport {\n  addTopicMessage,\n  resetTopicMessages,\n  updateTopicMessagesMeta,\n  updateTopicMessagesPhase,\n  setTopicMessagesFetchingStatus,\n  setMessageEventType,\n} from 'redux/reducers/topicMessages/topicMessagesSlice';\nimport {\n  getTopicMessgesMeta,\n  getTopicMessgesPhase,\n  getIsTopicMessagesFetching,\n  getIsTopicMessagesType,\n} from 'redux/reducers/topicMessages/selectors';\n\nimport Filters from './Filters';\n\nconst mapStateToProps = (state: RootState) => ({\n  phaseMessage: getTopicMessgesPhase(state),\n  meta: getTopicMessgesMeta(state),\n  isFetching: getIsTopicMessagesFetching(state),\n  messageEventType: getIsTopicMessagesType(state),\n});\n\nconst mapDispatchToProps = {\n  addMessage: addTopicMessage,\n  resetMessages: resetTopicMessages,\n  updatePhase: updateTopicMessagesPhase,\n  updateMeta: updateTopicMessagesMeta,\n  setIsFetching: setTopicMessagesFetchingStatus,\n  setMessageType: setMessageEventType,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(Filters);\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/InfoModal.tsx",
    "content": "import React from 'react';\nimport * as S from 'components/Topics/Topic/Messages/Filters/Filters.styled';\nimport { Button } from 'components/common/Button/Button';\n\ninterface InfoModalProps {\n  toggleIsOpen(): void;\n}\n\nconst InfoModal: React.FC<InfoModalProps> = ({ toggleIsOpen }) => {\n  return (\n    <S.InfoModal>\n      <S.InfoParagraph>\n        <b>Variables bound to groovy context:</b> partition, timestampMs,\n        keyAsText, valueAsText, header, key (json if possible), value (json if\n        possible).\n      </S.InfoParagraph>\n      <S.InfoParagraph>\n        <b>JSON parsing logic:</b>\n      </S.InfoParagraph>\n      <S.InfoParagraph>\n        Key and Value (if they can be parsed to JSON) they are bound as JSON\n        objects, otherwise bound as nulls.\n      </S.InfoParagraph>\n      <S.InfoParagraph>\n        <b>Sample filters:</b>\n      </S.InfoParagraph>\n      <ol aria-label=\"info-list\">\n        <S.ListItem>\n          <code>keyAsText != null && keyAsText ~&quot;([Gg])roovy&quot;</code> -\n          regex for key as a string\n        </S.ListItem>\n        <S.ListItem>\n          <code>\n            value.name == &quot;iS.ListItemax&quot; && value.age &gt; 30\n          </code>{' '}\n          - in case value is json\n        </S.ListItem>\n        <S.ListItem>\n          <code>value == null && valueAsText != null</code> - search for values\n          that are not nulls and are not json\n        </S.ListItem>\n        <S.ListItem>\n          <code>\n            headers.sentBy == &quot;some system&quot; &&\n            headers[&quot;sentAt&quot;] == &quot;2020-01-01&quot;\n          </code>\n        </S.ListItem>\n        <S.ListItem>\n          multiline filters are also allowed:\n          <S.InfoParagraph>\n            <pre>\n              def name = value.name\n              <br />\n              def age = value.age\n              <br />\n              name == &quot;iliax&quot; && age == 30\n              <br />\n            </pre>\n          </S.InfoParagraph>\n        </S.ListItem>\n      </ol>\n      <S.ButtonContainer>\n        <Button\n          buttonSize=\"M\"\n          buttonType=\"secondary\"\n          type=\"button\"\n          onClick={toggleIsOpen}\n        >\n          Ok\n        </Button>\n      </S.ButtonContainer>\n    </S.InfoModal>\n  );\n};\n\nexport default InfoModal;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/SavedFilters.tsx",
    "content": "import React, { FC } from 'react';\nimport { Button } from 'components/common/Button/Button';\nimport DeleteIcon from 'components/common/Icons/DeleteIcon';\nimport { useConfirm } from 'lib/hooks/useConfirm';\n\nimport * as S from './Filters.styled';\nimport { MessageFilters } from './Filters';\n\nexport interface Props {\n  filters: MessageFilters[];\n  onEdit(index: number, filter: MessageFilters): void;\n  deleteFilter(index: number): void;\n  activeFilterHandler(activeFilter: MessageFilters, index: number): void;\n  closeModal(): void;\n  onGoBack(): void;\n  activeFilter?: MessageFilters;\n}\n\nconst SavedFilters: FC<Props> = ({\n  filters,\n  onEdit,\n  deleteFilter,\n  activeFilterHandler,\n  closeModal,\n  onGoBack,\n  activeFilter,\n}) => {\n  const [selectedFilter, setSelectedFilter] = React.useState(-1);\n  const confirm = useConfirm();\n\n  const activateFilter = () => {\n    if (selectedFilter > -1) {\n      activeFilterHandler(filters[selectedFilter], selectedFilter);\n    }\n    closeModal();\n  };\n\n  const deleteFilterHandler = (index: number) => {\n    const filterName = filters[index]?.name;\n    const isFilterSelected = activeFilter && activeFilter.name === filterName;\n\n    confirm(\n      <>\n        <p>Are you sure want to remove {filterName}?</p>\n        {isFilterSelected && (\n          <>\n            <br />\n            <p>Warning: this filter is currently selected.</p>\n          </>\n        )}\n      </>,\n      () => {\n        deleteFilter(index);\n        setSelectedFilter(-1);\n      }\n    );\n  };\n\n  return (\n    <>\n      <S.BackToCustomText onClick={onGoBack}>\n        Back To create filters\n      </S.BackToCustomText>\n      <S.SavedFiltersContainer>\n        <S.CreatedFilter>Saved filters</S.CreatedFilter>\n        {filters.length === 0 && (\n          <S.NoSavedFilter>No saved filter(s)</S.NoSavedFilter>\n        )}\n        {filters.map((filter, index) => (\n          <S.SavedFilter\n            key={Symbol(filter.name).toString()}\n            selected={selectedFilter === index}\n            onClick={() => setSelectedFilter(index)}\n          >\n            <S.SavedFilterName>{filter.name}</S.SavedFilterName>\n            <S.FilterOptions>\n              <S.FilterEdit onClick={() => onEdit(index, filter)}>\n                Edit\n              </S.FilterEdit>\n              <S.DeleteSavedFilter onClick={() => deleteFilterHandler(index)}>\n                <DeleteIcon />\n              </S.DeleteSavedFilter>\n            </S.FilterOptions>\n          </S.SavedFilter>\n        ))}\n      </S.SavedFiltersContainer>\n      <S.FilterButtonWrapper>\n        <Button\n          buttonSize=\"M\"\n          buttonType=\"secondary\"\n          type=\"button\"\n          onClick={closeModal}\n        >\n          Cancel\n        </Button>\n        <Button\n          buttonSize=\"M\"\n          buttonType=\"primary\"\n          type=\"button\"\n          onClick={activateFilter}\n          disabled={selectedFilter === -1}\n        >\n          Select filter\n        </Button>\n      </S.FilterButtonWrapper>\n    </>\n  );\n};\n\nexport default SavedFilters;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/__tests__/AddEditFilterContainer.spec.tsx",
    "content": "import React from 'react';\nimport AddEditFilterContainer, {\n  AddEditFilterContainerProps,\n} from 'components/Topics/Topic/Messages/Filters/AddEditFilterContainer';\nimport { render } from 'lib/testHelpers';\nimport { act, screen } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { MessageFilters } from 'components/Topics/Topic/Messages/Filters/Filters';\n\ndescribe('AddEditFilterContainer component', () => {\n  const defaultSubmitBtn = 'Submit Button';\n\n  const mockData: MessageFilters = {\n    name: 'mockName',\n    code: 'mockCode',\n  };\n\n  const renderComponent = (\n    props: Partial<AddEditFilterContainerProps> = {}\n  ) => {\n    return render(\n      <AddEditFilterContainer\n        cancelBtnHandler={jest.fn()}\n        submitBtnText={props.submitBtnText || defaultSubmitBtn}\n        {...props}\n      />\n    );\n  };\n\n  describe('default Component Parameters', () => {\n    it('should check the default Button text', async () => {\n      await act(() => {\n        renderComponent();\n      });\n      expect(screen.getByText(defaultSubmitBtn)).toBeInTheDocument();\n    });\n\n    it('should check whether the submit Button is disabled when the form is pristine and disabled if dirty', async () => {\n      renderComponent();\n      const submitButtonElem = screen.getByText(defaultSubmitBtn);\n      expect(submitButtonElem).toBeDisabled();\n\n      const inputs = screen.getAllByRole('textbox');\n\n      const textAreaElement = inputs[0] as HTMLTextAreaElement;\n      textAreaElement.focus();\n      await userEvent.paste('Hello World With TextArea');\n\n      const inputNameElement = inputs[1] as HTMLTextAreaElement;\n      await userEvent.type(inputNameElement, 'Hello World!');\n\n      expect(submitButtonElem).toBeEnabled();\n\n      await userEvent.clear(inputNameElement);\n      await userEvent.tab();\n\n      expect(submitButtonElem).toBeDisabled();\n    });\n\n    it('should view the error message after typing and clearing the input', async () => {\n      await act(() => {\n        renderComponent();\n      });\n      const inputs = screen.getAllByRole('textbox');\n      const user = userEvent.setup();\n      const textAreaElement = inputs[0] as HTMLTextAreaElement;\n      const inputNameElement = inputs[1];\n\n      await user.type(textAreaElement, 'Hello World With TextArea');\n      await user.type(inputNameElement, 'Hello World!');\n\n      await user.clear(inputNameElement);\n      await user.keyboard('{Control>}[KeyA]{/Control}{backspace}');\n      await user.tab();\n\n      expect(screen.getByText(/required field/i)).toBeInTheDocument();\n    });\n  });\n\n  describe('Custom setup for the component', () => {\n    it('should render the input with default data if they are passed', () => {\n      renderComponent({\n        inputDisplayNameDefaultValue: mockData.name,\n        inputCodeDefaultValue: mockData.code,\n      });\n      const inputs = screen.getAllByRole('textbox');\n      const textAreaElement = inputs[0] as HTMLTextAreaElement;\n      const inputNameElement = inputs[1];\n      expect(inputNameElement).toHaveValue(mockData.name);\n      expect(textAreaElement.value).toEqual('');\n    });\n\n    it('should test whether the cancel callback is being called', async () => {\n      const cancelCallback = jest.fn();\n      renderComponent({\n        cancelBtnHandler: cancelCallback,\n      });\n      const cancelBtnElement = screen.getByText(/cancel/i);\n\n      await userEvent.click(cancelBtnElement);\n      expect(cancelCallback).toBeCalled();\n    });\n\n    it('should test whether the submit Callback is being called', async () => {\n      const submitCallback = jest.fn();\n      renderComponent({ submitCallback });\n\n      const inputs = screen.getAllByRole('textbox');\n\n      const textAreaElement = inputs[0] as HTMLTextAreaElement;\n      textAreaElement.focus();\n      await userEvent.paste('Hello World With TextArea');\n\n      const inputNameElement = inputs[1];\n      await userEvent.type(inputNameElement, 'Hello World!');\n\n      const submitBtnElement = screen.getByText(defaultSubmitBtn);\n      expect(submitBtnElement).toBeEnabled();\n\n      await userEvent.click(submitBtnElement);\n      expect(submitCallback).toBeCalled();\n    });\n\n    it('should display the checkbox if the props is passed and initially check state', async () => {\n      renderComponent({ isAdd: true });\n      const checkbox = screen.getByRole('checkbox');\n      expect(checkbox).toBeInTheDocument();\n      expect(checkbox).not.toBeChecked();\n      await userEvent.click(checkbox);\n      expect(checkbox).toBeChecked();\n    });\n\n    it('should pass and render the correct button text', async () => {\n      const submitBtnText = 'submitBtnTextTest';\n      await act(() => {\n        renderComponent({\n          submitBtnText,\n        });\n      });\n      expect(screen.getByText(submitBtnText)).toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/__tests__/AddFilter.spec.tsx",
    "content": "import React from 'react';\nimport AddFilter, {\n  FilterModalProps,\n} from 'components/Topics/Topic/Messages/Filters/AddFilter';\nimport { render } from 'lib/testHelpers';\nimport { MessageFilters } from 'components/Topics/Topic/Messages/Filters/Filters';\nimport { act, fireEvent, screen } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\n\nconst filters: MessageFilters[] = [\n  { name: 'name', code: 'code' },\n  { name: 'name2', code: 'code2' },\n];\n\nconst editFilterMock = jest.fn();\n\nconst renderComponent = (props: Partial<FilterModalProps> = {}) =>\n  render(\n    <AddFilter\n      toggleIsOpen={jest.fn()}\n      addFilter={jest.fn()}\n      deleteFilter={jest.fn()}\n      activeFilterHandler={jest.fn()}\n      toggleEditModal={jest.fn()}\n      onClickSavedFilters={jest.fn()}\n      editFilter={editFilterMock}\n      filters={props.filters || filters}\n      isSavedFiltersOpen={false}\n      {...props}\n    />\n  );\n\ndescribe('AddFilter component', () => {\n  describe('', () => {\n    it('should test click on Saved Filters redirects to Saved components', async () => {\n      renderComponent();\n      await userEvent.click(screen.getByRole('savedFilterText'));\n      expect(screen.getByText('Saved Filters')).toBeInTheDocument();\n      expect(screen.getByRole('savedFilterText')).toBeInTheDocument();\n    });\n\n    it('info button to be in the document', async () => {\n      await act(() => {\n        renderComponent();\n      });\n      expect(screen.getByRole('button', { name: 'info' })).toBeInTheDocument();\n    });\n\n    it('renders InfoModal', async () => {\n      renderComponent();\n      await userEvent.click(screen.getByRole('button', { name: 'info' }));\n      expect(screen.getByRole('button', { name: 'Ok' })).toBeInTheDocument();\n      expect(\n        screen.getByRole('list', { name: 'info-list' })\n      ).toBeInTheDocument();\n    });\n\n    it('should test click on return to custom filter redirects to Saved Filters', async () => {\n      await act(() => {\n        renderComponent();\n      });\n      await userEvent.click(screen.getByRole('savedFilterText'));\n      expect(screen.queryByText('Saved filters')).not.toBeInTheDocument();\n      expect(screen.getByRole('savedFilterText')).toBeInTheDocument();\n    });\n  });\n\n  describe('Add new filter', () => {\n    it('adding new filter', async () => {\n      renderComponent();\n      const codeValue = 'filter code';\n      const nameValue = 'filter name';\n      const textBoxes = screen.getAllByRole('textbox');\n\n      const codeTextBox = textBoxes[0] as HTMLTextAreaElement;\n      const nameTextBox = textBoxes[1];\n\n      const addFilterBtn = screen.getByRole('button', { name: /Add filter/i });\n      expect(addFilterBtn).toBeDisabled();\n      expect(screen.getByPlaceholderText('Enter Name')).toBeInTheDocument();\n\n      codeTextBox.focus();\n      await userEvent.paste(codeValue);\n      await userEvent.type(nameTextBox, nameValue);\n\n      expect(addFilterBtn).toBeEnabled();\n      expect(codeTextBox.value).toEqual(`${codeValue}\\n\\n`);\n      expect(nameTextBox).toHaveValue(nameValue);\n    });\n\n    it('should check unSaved filter without name', async () => {\n      renderComponent();\n      const codeTextBox = screen.getAllByRole(\n        'textbox'\n      )[0] as HTMLTextAreaElement;\n      const code = 'filter code';\n      const addFilterBtn = screen.getByRole('button', { name: /Add filter/i });\n      expect(addFilterBtn).toBeDisabled();\n      expect(screen.getByPlaceholderText('Enter Name')).toBeInTheDocument();\n      codeTextBox.focus();\n      await userEvent.paste(code);\n      await userEvent.tab();\n      expect(addFilterBtn).toBeEnabled();\n      expect(codeTextBox).toHaveValue(`${code}\\n\\n`);\n    });\n\n    it('calls editFilter when edit button is clicked in saved filters', async () => {\n      await act(() => {\n        renderComponent();\n        renderComponent({ isSavedFiltersOpen: true });\n      });\n      await userEvent.click(screen.getByText('Saved Filters'));\n      const index = 0;\n      const editButton = screen.getAllByText('Edit')[index];\n      await userEvent.click(editButton);\n      const { code, name } = filters[index];\n      expect(editFilterMock).toHaveBeenCalledTimes(1);\n      expect(editFilterMock).toHaveBeenCalledWith({\n        index,\n        filter: { code, name },\n      });\n    });\n  });\n\n  describe('onSubmit with Filter being saved', () => {\n    const addFilterMock = jest.fn();\n    const activeFilterHandlerMock = jest.fn();\n    const toggleModelMock = jest.fn();\n\n    const codeValue = 'filter code';\n    const longCodeValue = 'a long filter code';\n    const nameValue = 'filter name';\n\n    afterEach(() => {\n      addFilterMock.mockClear();\n      activeFilterHandlerMock.mockClear();\n      toggleModelMock.mockClear();\n    });\n\n    describe('OnSubmit conditions with codeValue and nameValue in fields', () => {\n      beforeEach(async () => {\n        renderComponent({\n          addFilter: addFilterMock,\n          activeFilterHandler: activeFilterHandlerMock,\n          toggleIsOpen: toggleModelMock,\n        });\n        const textAreaElement = screen.getAllByRole(\n          'textbox'\n        )[0] as HTMLTextAreaElement;\n        const input = screen.getAllByRole('textbox')[1];\n        textAreaElement.focus();\n        await userEvent.paste(codeValue);\n        await userEvent.type(input, nameValue);\n      });\n\n      it('OnSubmit condition with checkbox off functionality', async () => {\n        // since both values are in it\n        const addFilterBtn = screen.getByRole('button', {\n          name: /Add filter/i,\n        });\n        expect(addFilterBtn).toBeEnabled();\n\n        await userEvent.click(addFilterBtn);\n\n        expect(activeFilterHandlerMock).toHaveBeenCalled();\n        expect(addFilterMock).not.toHaveBeenCalled();\n      });\n\n      it('OnSubmit condition with checkbox on functionality', async () => {\n        await userEvent.click(screen.getByRole('checkbox'));\n        await userEvent.click(screen.getAllByRole('button')[2]);\n\n        expect(activeFilterHandlerMock).not.toHaveBeenCalled();\n        expect(addFilterMock).toHaveBeenCalled();\n        expect(toggleModelMock).not.toHaveBeenCalled();\n      });\n    });\n\n    it('should use sliced code as the filter name if filter name is empty', async () => {\n      await act(() => {\n        renderComponent({\n          addFilter: addFilterMock,\n          activeFilterHandler: activeFilterHandlerMock,\n          toggleIsOpen: toggleModelMock,\n        });\n      });\n      const codeTextBox = screen.getAllByRole(\n        'textbox'\n      )[0] as HTMLTextAreaElement;\n      const nameTextBox = screen.getAllByRole('textbox')[1];\n      const addFilterBtn = screen.getByRole('button', { name: /Add filter/i });\n      act(() => {\n        fireEvent.input(nameTextBox, {\n          inputType: '',\n        });\n        fireEvent.input(codeTextBox, {\n          inputType: '',\n        });\n      });\n      codeTextBox.focus();\n      await userEvent.paste(longCodeValue);\n      expect(nameTextBox).toHaveValue('');\n      expect(codeTextBox).toHaveValue(`${longCodeValue}\\n\\n`);\n\n      await userEvent.click(addFilterBtn);\n\n      const filterName = `${longCodeValue.slice(0, 16)}...`;\n\n      expect(activeFilterHandlerMock).toHaveBeenCalledTimes(1);\n      expect(activeFilterHandlerMock).toHaveBeenCalledWith(\n        {\n          name: filterName,\n          code: longCodeValue,\n          saveFilter: false,\n        },\n        -1\n      );\n      expect(toggleModelMock).toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/__tests__/EditFilter.spec.tsx",
    "content": "import React from 'react';\nimport EditFilter, {\n  EditFilterProps,\n} from 'components/Topics/Topic/Messages/Filters/EditFilter';\nimport { render } from 'lib/testHelpers';\nimport { screen, within, act } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { FilterEdit } from 'components/Topics/Topic/Messages/Filters/FilterModal';\n\nconst editFilter: FilterEdit = {\n  index: 0,\n  filter: { name: 'name', code: '' },\n};\n\nconst renderComponent = (props?: Partial<EditFilterProps>) =>\n  render(\n    <EditFilter\n      toggleEditModal={jest.fn()}\n      editSavedFilter={jest.fn()}\n      editFilter={editFilter}\n      {...props}\n    />\n  );\n\ndescribe('EditFilter component', () => {\n  it('renders component', async () => {\n    await act(() => {\n      renderComponent();\n    });\n    expect(screen.getByText(/edit filter/i)).toBeInTheDocument();\n  });\n\n  it('closes editFilter modal', async () => {\n    const toggleEditModal = jest.fn();\n    renderComponent({ toggleEditModal });\n    await userEvent.click(screen.getByRole('button', { name: /Cancel/i }));\n    expect(toggleEditModal).toHaveBeenCalledTimes(1);\n  });\n\n  it('save edited fields and close modal', async () => {\n    const toggleEditModal = jest.fn();\n    const editSavedFilter = jest.fn();\n\n    renderComponent({ toggleEditModal, editSavedFilter });\n\n    const inputs = screen.getAllByRole('textbox');\n    const textAreaElement = inputs[0] as HTMLTextAreaElement;\n    const inputNameElement = inputs[1];\n    textAreaElement.focus();\n    await userEvent.paste('edited code');\n    await userEvent.type(inputNameElement, 'edited name');\n    await userEvent.click(screen.getByRole('button', { name: /Save/i }));\n    expect(toggleEditModal).toHaveBeenCalledTimes(1);\n    expect(editSavedFilter).toHaveBeenCalledTimes(1);\n  });\n\n  it('checks input values to match', async () => {\n    await act(() => {\n      renderComponent();\n    });\n    const inputs = screen.getAllByRole('textbox');\n    const textAreaElement = inputs[0] as HTMLTextAreaElement;\n    const inputNameElement = inputs[1];\n    const span = within(textAreaElement).getByText(editFilter.filter.code);\n    expect(span).toHaveValue(editFilter.filter.code);\n    expect(inputNameElement).toHaveValue(editFilter.filter.name);\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/__tests__/FilterModal.spec.tsx",
    "content": "import React from 'react';\nimport FilterModal, {\n  FilterModalProps,\n} from 'components/Topics/Topic/Messages/Filters/FilterModal';\nimport { render } from 'lib/testHelpers';\nimport {\n  ActiveMessageFilter,\n  MessageFilters,\n} from 'components/Topics/Topic/Messages/Filters/Filters';\nimport { screen, act } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\n\nconst filter = { name: 'name', code: 'code' };\nconst filters: MessageFilters[] = [filter];\nconst activeFilter: ActiveMessageFilter = { index: -1, ...filter };\n\nconst renderComponent = (props?: Partial<FilterModalProps>) =>\n  render(\n    <FilterModal\n      toggleIsOpen={jest.fn()}\n      filters={filters}\n      addFilter={jest.fn()}\n      deleteFilter={jest.fn()}\n      activeFilterHandler={jest.fn()}\n      editSavedFilter={jest.fn()}\n      activeFilter={activeFilter}\n      {...props}\n    />\n  );\n\ndescribe('FilterModal component', () => {\n  it('renders component with add filter modal', async () => {\n    await act(() => {\n      renderComponent();\n    });\n    expect(\n      screen.getByRole('heading', { name: /add filter/i, level: 3 })\n    ).toBeInTheDocument();\n  });\n  it('renders component with edit filter modal', async () => {\n    renderComponent();\n    await userEvent.click(screen.getByRole('savedFilterText'));\n    await userEvent.click(screen.getByText('Edit'));\n    expect(\n      screen.getByRole('heading', { name: /edit filter/i, level: 3 })\n    ).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/__tests__/Filters.spec.tsx",
    "content": "import React from 'react';\nimport { SeekDirectionOptions } from 'components/Topics/Topic/Messages/Messages';\nimport Filters, {\n  FiltersProps,\n  SeekTypeOptions,\n} from 'components/Topics/Topic/Messages/Filters/Filters';\nimport { EventSourceMock, render, WithRoute } from 'lib/testHelpers';\nimport { screen, within } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport TopicMessagesContext, {\n  ContextProps,\n} from 'components/contexts/TopicMessagesContext';\nimport { SeekDirection } from 'generated-sources';\nimport { clusterTopicPath } from 'lib/paths';\nimport { useTopicDetails } from 'lib/hooks/api/topics';\nimport { externalTopicPayload } from 'lib/fixtures/topics';\nimport { useSerdes } from 'lib/hooks/api/topicMessages';\nimport { serdesPayload } from 'lib/fixtures/topicMessages';\n\njest.mock('lib/hooks/api/topics', () => ({\n  useTopicDetails: jest.fn(),\n}));\n\njest.mock('lib/hooks/api/topicMessages', () => ({\n  useSerdes: jest.fn(),\n}));\n\nconst defaultContextValue: ContextProps = {\n  isLive: false,\n  seekDirection: SeekDirection.FORWARD,\n  changeSeekDirection: jest.fn(),\n};\n\njest.mock('components/common/Icons/CloseIcon', () => () => 'mock-CloseIcon');\n\nconst clusterName = 'cluster-name';\nconst topicName = 'topic-name';\n\nconst renderComponent = (\n  props: Partial<FiltersProps> = {},\n  ctx: ContextProps = defaultContextValue\n) =>\n  render(\n    <WithRoute path={clusterTopicPath()}>\n      <TopicMessagesContext.Provider value={ctx}>\n        <Filters\n          meta={{\n            filterApplyErrors: 10,\n          }}\n          isFetching={false}\n          addMessage={jest.fn()}\n          resetMessages={jest.fn()}\n          updatePhase={jest.fn()}\n          updateMeta={jest.fn()}\n          setIsFetching={jest.fn()}\n          setMessageType={jest.fn}\n          messageEventType=\"Done\"\n          {...props}\n        />\n      </TopicMessagesContext.Provider>\n    </WithRoute>,\n    { initialEntries: [clusterTopicPath(clusterName, topicName)] }\n  );\n\nbeforeEach(async () => {\n  (useTopicDetails as jest.Mock).mockImplementation(() => ({\n    data: externalTopicPayload,\n  }));\n  (useSerdes as jest.Mock).mockImplementation(() => ({\n    data: serdesPayload,\n  }));\n});\n\ndescribe('Filters component', () => {\n  Object.defineProperty(window, 'EventSource', {\n    value: EventSourceMock,\n  });\n\n  it('shows cancel button while fetching', () => {\n    renderComponent({ isFetching: true });\n    expect(screen.getByText('Cancel')).toBeInTheDocument();\n  });\n\n  it('shows submit button while fetching is over', () => {\n    renderComponent();\n    expect(screen.getByText('Submit')).toBeInTheDocument();\n  });\n\n  describe('Input elements', () => {\n    const inputValue = 'Hello World!';\n\n    beforeEach(() => {\n      renderComponent();\n    });\n\n    it('search input', async () => {\n      const searchInput = screen.getByPlaceholderText('Search');\n      expect(searchInput).toHaveValue('');\n      await userEvent.type(searchInput, inputValue);\n      expect(searchInput).toHaveValue(inputValue);\n    });\n\n    it('offset input', async () => {\n      const offsetInput = screen.getByPlaceholderText('Offset');\n      expect(offsetInput).toHaveValue('');\n      await userEvent.type(offsetInput, inputValue);\n      expect(offsetInput).toHaveValue(inputValue);\n    });\n\n    it('timestamp input', async () => {\n      const seekTypeSelect = screen.getAllByRole('listbox');\n      const option = screen.getAllByRole('option');\n\n      await userEvent.click(seekTypeSelect[0]);\n\n      await userEvent.selectOptions(seekTypeSelect[0], ['Timestamp']);\n\n      expect(option[0]).toHaveTextContent('Timestamp');\n      const timestampInput = screen.getByPlaceholderText('Select timestamp');\n      expect(timestampInput).toHaveValue('');\n\n      await userEvent.type(timestampInput, inputValue);\n\n      expect(timestampInput).toHaveValue(inputValue);\n      expect(screen.getByText('Submit')).toBeInTheDocument();\n    });\n  });\n\n  describe('Select elements', () => {\n    let seekTypeSelects: HTMLElement[];\n    let options: HTMLElement[];\n\n    const selectedDirectionOptionValue = SeekDirectionOptions[0];\n    const mockDirectionOptionSelectLabel = selectedDirectionOptionValue.label;\n    const selectTypeOptionValue = SeekTypeOptions[0];\n    const mockTypeOptionSelectLabel = selectTypeOptionValue.label;\n\n    beforeEach(() => {\n      renderComponent();\n      seekTypeSelects = screen.getAllByRole('listbox');\n      options = screen.getAllByRole('option');\n    });\n\n    it('seekType select', async () => {\n      expect(options[0]).toHaveTextContent('Offset');\n      await userEvent.click(seekTypeSelects[0]);\n      await userEvent.selectOptions(seekTypeSelects[0], [\n        mockTypeOptionSelectLabel,\n      ]);\n      expect(options[0]).toHaveTextContent(mockTypeOptionSelectLabel);\n      expect(screen.getByText('Submit')).toBeInTheDocument();\n    });\n\n    it('seekDirection select', async () => {\n      await userEvent.click(seekTypeSelects[3]);\n      await userEvent.selectOptions(seekTypeSelects[3], [\n        mockDirectionOptionSelectLabel,\n      ]);\n      expect(options[3]).toHaveTextContent(mockDirectionOptionSelectLabel);\n    });\n  });\n\n  it('stop loading when live mode is active', async () => {\n    renderComponent();\n    await userEvent.click(screen.getByText('Stop loading'));\n    const option = screen.getAllByRole('option');\n    expect(option[3]).toHaveTextContent('Oldest First');\n    expect(screen.getByText('Submit')).toBeInTheDocument();\n  });\n\n  it('renders addFilter modal', async () => {\n    renderComponent();\n    await userEvent.click(\n      screen.getByRole('button', {\n        name: /add filters/i,\n      })\n    );\n    expect(screen.getByTestId('messageFilterModal')).toBeInTheDocument();\n  });\n\n  describe('when there is active smart filter', () => {\n    beforeEach(async () => {\n      renderComponent();\n\n      await userEvent.click(\n        screen.getByRole('button', {\n          name: 'Add Filters',\n        })\n      );\n\n      const filterName = 'filter name';\n      const filterCode = 'filter code';\n\n      const messageFilterModal = screen.getByTestId('messageFilterModal');\n\n      const textBoxElements =\n        within(messageFilterModal).getAllByRole('textbox');\n\n      const textAreaElement = textBoxElements[0] as HTMLTextAreaElement;\n      const inputNameElement = textBoxElements[1];\n\n      textAreaElement.focus();\n      await userEvent.paste(filterCode);\n      await userEvent.type(inputNameElement, filterName);\n\n      expect(textAreaElement).toHaveValue(`${filterCode}\\n\\n`);\n      expect(inputNameElement).toHaveValue('filter name');\n      expect(\n        screen.getByRole('button', {\n          name: 'Add filter',\n        })\n      ).toBeEnabled();\n      await userEvent.click(\n        screen.getByRole('button', {\n          name: 'Add filter',\n        })\n      );\n      await userEvent.tab();\n    });\n\n    it('shows saved smart filter', async () => {\n      expect(screen.getByTestId('activeSmartFilter')).toBeInTheDocument();\n    });\n\n    it('delete the active smart Filter', async () => {\n      const smartFilterElement = screen.getByTestId('activeSmartFilter');\n      const deleteIcon = within(smartFilterElement).getByText('mock-CloseIcon');\n      await userEvent.click(deleteIcon);\n\n      const anotherSmartFilterElement =\n        screen.queryByTestId('activeSmartFilter');\n      expect(anotherSmartFilterElement).not.toBeInTheDocument();\n    });\n  });\n\n  describe('show errors when get an filterApplyErrors and message event type', () => {\n    it('show errors', () => {\n      renderComponent();\n      const errors = screen.getByText('10 errors');\n      expect(errors).toBeInTheDocument();\n    });\n    it('message event type when fetching is false ', () => {\n      renderComponent();\n      const messageType = screen.getByText('Done');\n      expect(messageType).toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/__tests__/Filters.styled.spec.tsx",
    "content": "import React from 'react';\nimport { render } from 'lib/testHelpers';\nimport * as S from 'components/Topics/Topic/Messages/Filters/Filters.styled';\nimport { screen } from '@testing-library/react';\nimport { theme } from 'theme/theme';\n\ndescribe('Filters Styled components', () => {\n  describe('MessageLoading component', () => {\n    it('should check the styling during live', () => {\n      render(<S.MessageLoading isLive />);\n      expect(screen.getByRole('contentLoader')).toHaveStyle({\n        color: theme.heading.h3.color,\n        'font-size': theme.heading.h3.fontSize,\n        display: 'flex',\n      });\n    });\n\n    it('should check the styling during not live', () => {\n      render(<S.MessageLoading isLive={false} />);\n      expect(screen.getByRole('contentLoader', { hidden: true })).toHaveStyle({\n        color: theme.heading.h3.color,\n        'font-size': theme.heading.h3.fontSize,\n        display: 'none',\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/__tests__/InfoModal.spec.tsx",
    "content": "import userEvent from '@testing-library/user-event';\nimport { screen } from '@testing-library/react';\nimport { render } from 'lib/testHelpers';\nimport React from 'react';\nimport InfoModal from 'components/Topics/Topic/Messages/Filters/InfoModal';\n\ndescribe('InfoModal component', () => {\n  it('closes InfoModal', async () => {\n    const toggleInfoModal = jest.fn();\n    render(<InfoModal toggleIsOpen={toggleInfoModal} />);\n    await userEvent.click(screen.getByRole('button', { name: 'Ok' }));\n    expect(toggleInfoModal).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/__tests__/SavedFilters.spec.tsx",
    "content": "import React from 'react';\nimport SavedFilters, {\n  Props,\n} from 'components/Topics/Topic/Messages/Filters/SavedFilters';\nimport { MessageFilters } from 'components/Topics/Topic/Messages/Filters/Filters';\nimport { screen, waitFor, within } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { render } from 'lib/testHelpers';\n\njest.mock('components/common/Icons/DeleteIcon', () => () => 'mock-DeleteIcon');\n\ndescribe('SavedFilter Component', () => {\n  const mockFilters: MessageFilters[] = [\n    { name: 'My Filter', code: 'code' },\n    { name: 'One More Filter', code: 'code1' },\n  ];\n\n  const setUpComponent = (props: Partial<Props> = {}) =>\n    render(\n      <SavedFilters\n        filters={props.filters || mockFilters}\n        onEdit={props.onEdit || jest.fn()}\n        closeModal={props.closeModal || jest.fn()}\n        onGoBack={props.onGoBack || jest.fn()}\n        activeFilterHandler={props.activeFilterHandler || jest.fn()}\n        deleteFilter={props.deleteFilter || jest.fn()}\n      />\n    );\n\n  const getSavedFilters = () => screen.getAllByRole('savedFilter');\n\n  it('should check the Cancel button click', async () => {\n    const cancelMock = jest.fn();\n    setUpComponent({ closeModal: cancelMock });\n    await userEvent.click(screen.getByText(/cancel/i));\n    expect(cancelMock).toHaveBeenCalled();\n  });\n\n  it('should check on go back button click', async () => {\n    const onGoBackMock = jest.fn();\n    setUpComponent({ onGoBack: onGoBackMock });\n    await userEvent.click(screen.getByText(/back to create filters/i));\n    expect(onGoBackMock).toHaveBeenCalled();\n  });\n\n  describe('Empty Filters Rendering', () => {\n    beforeEach(() => {\n      setUpComponent({ filters: [] });\n    });\n    it('should check the rendering of the empty filter', () => {\n      expect(screen.getByText(/no saved filter/i)).toBeInTheDocument();\n      expect(screen.queryByRole('savedFilter')).not.toBeInTheDocument();\n\n      const selectFilterButton = screen.getByText(/Select filter/i);\n      expect(selectFilterButton).toBeDisabled();\n    });\n  });\n\n  describe('Saved Filters Deleting Editing', () => {\n    const onEditMock = jest.fn();\n    const activeFilterMock = jest.fn();\n    const cancelMock = jest.fn();\n\n    beforeEach(() => {\n      setUpComponent({\n        onEdit: onEditMock,\n        activeFilterHandler: activeFilterMock,\n        closeModal: cancelMock,\n      });\n    });\n\n    afterEach(() => {\n      onEditMock.mockClear();\n      activeFilterMock.mockClear();\n      cancelMock.mockClear();\n    });\n\n    it('should check the normal data rendering', () => {\n      expect(getSavedFilters()).toHaveLength(mockFilters.length);\n      expect(screen.getByText(mockFilters[0].name)).toBeInTheDocument();\n      expect(screen.getByText(mockFilters[1].name)).toBeInTheDocument();\n    });\n\n    it('should check the Filter edit Button works', async () => {\n      const savedFilters = getSavedFilters();\n      await userEvent.hover(savedFilters[0]);\n      await userEvent.click(within(savedFilters[0]).getByText(/edit/i));\n      expect(onEditMock).toHaveBeenCalled();\n\n      await userEvent.hover(savedFilters[1]);\n      await userEvent.click(within(savedFilters[1]).getByText(/edit/i));\n      expect(onEditMock).toHaveBeenCalledTimes(2);\n    });\n\n    it('should check the select filter', async () => {\n      const selectFilterButton = screen.getByText(/Select filter/i);\n\n      await userEvent.click(selectFilterButton);\n      expect(activeFilterMock).not.toHaveBeenCalled();\n\n      const savedFilterElement = getSavedFilters();\n      await userEvent.click(savedFilterElement[0]);\n      await userEvent.click(selectFilterButton);\n\n      expect(activeFilterMock).toHaveBeenCalled();\n      expect(cancelMock).toHaveBeenCalled();\n    });\n  });\n\n  describe('Saved Filters Deletion', () => {\n    const deleteMock = jest.fn();\n\n    beforeEach(() => {\n      setUpComponent({ deleteFilter: deleteMock });\n    });\n\n    afterEach(() => {\n      deleteMock.mockClear();\n    });\n\n    it('Open Confirmation for the deletion modal', async () => {\n      const savedFilters = getSavedFilters();\n      const deleteIcons = screen.getAllByText('mock-DeleteIcon');\n      await userEvent.hover(savedFilters[0]);\n      await userEvent.click(deleteIcons[0]);\n      const modelDialog = screen.getByRole('dialog');\n      expect(modelDialog).toBeInTheDocument();\n      expect(\n        within(modelDialog).getByText('Are you sure want to remove My Filter?')\n      ).toBeInTheDocument();\n    });\n\n    it('Close Confirmations deletion modal with button', async () => {\n      const savedFilters = getSavedFilters();\n      const deleteIcons = screen.getAllByText('mock-DeleteIcon');\n\n      await userEvent.hover(savedFilters[0]);\n      await userEvent.click(deleteIcons[0]);\n\n      const modelDialog = screen.getByRole('dialog');\n      expect(modelDialog).toBeInTheDocument();\n      const cancelButton = within(modelDialog).getByRole('button', {\n        name: /Cancel/i,\n      });\n      await waitFor(() => userEvent.click(cancelButton));\n      expect(screen.queryByRole('dialog')).not.toBeInTheDocument();\n    });\n\n    it('Delete the saved filter', async () => {\n      const savedFilters = getSavedFilters();\n      const deleteIcons = screen.getAllByText('mock-DeleteIcon');\n\n      await userEvent.hover(savedFilters[0]);\n      await userEvent.click(deleteIcons[0]);\n\n      expect(screen.queryByRole('dialog')).toBeInTheDocument();\n      await userEvent.click(screen.getByRole('button', { name: 'Confirm' }));\n      expect(deleteMock).toHaveBeenCalledTimes(1);\n      expect(screen.queryByRole('dialog')).not.toBeInTheDocument();\n\n      const selectFilterButton = screen.getByText(/Select filter/i);\n      expect(selectFilterButton).toBeDisabled();\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/utils.ts",
    "content": "import { Partition, SeekType } from 'generated-sources';\nimport compact from 'lodash/compact';\nimport { Option } from 'react-multi-select-component';\n\nexport const filterOptions = (options: Option[], filter: string) => {\n  if (!filter) {\n    return options;\n  }\n  return options.filter(\n    ({ value }) => value.toString() && value.toString() === filter\n  );\n};\n\nexport const getOffsetFromSeekToParam = (params: URLSearchParams) => {\n  if (params.get('seekType') === SeekType.OFFSET) {\n    // seekTo format = ?seekTo=0::123,1::123,2::0\n    const offsets = params\n      .get('seekTo')\n      ?.split(',')\n      .map((item) => Number(item.split('::')[1]));\n    return String(Math.max(...(offsets || []), 0));\n  }\n\n  return '';\n};\n\nexport const getTimestampFromSeekToParam = (params: URLSearchParams) => {\n  if (params.get('seekType') === SeekType.TIMESTAMP) {\n    // seekTo format = ?seekTo=0::1627333200000,1::1627333200000\n    const offsets = params\n      .get('seekTo')\n      ?.split(',')\n      .map((item) => Number(item.split('::')[1]));\n    return new Date(Math.max(...(offsets || []), 0));\n  }\n\n  return null;\n};\n\nexport const getSelectedPartitionsFromSeekToParam = (\n  params: URLSearchParams,\n  partitions: Partition[]\n) => {\n  const seekTo = params.get('seekTo');\n\n  if (seekTo) {\n    const selectedPartitionIds = seekTo\n      .split(',')\n      .map((item) => Number(item.split('::')[0]));\n\n    return compact(\n      partitions.map(({ partition }) => {\n        if (selectedPartitionIds?.includes(partition)) {\n          return {\n            value: partition,\n            label: `Partition #${partition.toString()}`,\n          };\n        }\n\n        return undefined;\n      })\n    );\n  }\n\n  return partitions.map(({ partition }) => ({\n    value: partition,\n    label: `Partition #${partition.toString()}`,\n  }));\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Message.tsx",
    "content": "import React from 'react';\nimport useDataSaver from 'lib/hooks/useDataSaver';\nimport { TopicMessage } from 'generated-sources';\nimport MessageToggleIcon from 'components/common/Icons/MessageToggleIcon';\nimport IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';\nimport { Dropdown, DropdownItem } from 'components/common/Dropdown';\nimport { formatTimestamp } from 'lib/dateTimeHelpers';\nimport { JSONPath } from 'jsonpath-plus';\nimport Ellipsis from 'components/common/Ellipsis/Ellipsis';\nimport WarningRedIcon from 'components/common/Icons/WarningRedIcon';\nimport Tooltip from 'components/common/Tooltip/Tooltip';\n\nimport MessageContent from './MessageContent/MessageContent';\nimport * as S from './MessageContent/MessageContent.styled';\n\nexport interface PreviewFilter {\n  field: string;\n  path: string;\n}\n\nexport interface Props {\n  keyFilters: PreviewFilter[];\n  contentFilters: PreviewFilter[];\n  message: TopicMessage;\n}\n\nconst Message: React.FC<Props> = ({\n  message: {\n    timestamp,\n    timestampType,\n    offset,\n    key,\n    keySize,\n    partition,\n    content,\n    valueSize,\n    headers,\n    valueSerde,\n    keySerde,\n  },\n  keyFilters,\n  contentFilters,\n}) => {\n  const [isOpen, setIsOpen] = React.useState(false);\n  const savedMessageJson = {\n    Value: content,\n    Offset: offset,\n    Key: key,\n    Partition: partition,\n    Headers: headers,\n    Timestamp: timestamp,\n  };\n\n  const savedMessage = JSON.stringify(savedMessageJson, null, '\\t');\n  const { copyToClipboard, saveFile } = useDataSaver(\n    'topic-message',\n    savedMessage || ''\n  );\n\n  const toggleIsOpen = () => setIsOpen(!isOpen);\n\n  const [vEllipsisOpen, setVEllipsisOpen] = React.useState(false);\n\n  const getParsedJson = (jsonValue: string) => {\n    try {\n      return JSON.parse(jsonValue);\n    } catch (e) {\n      return {};\n    }\n  };\n\n  const renderFilteredJson = (\n    jsonValue?: string,\n    filters?: PreviewFilter[]\n  ) => {\n    if (!filters?.length || !jsonValue) return jsonValue;\n    const parsedJson = getParsedJson(jsonValue);\n\n    return (\n      <>\n        {filters.map((item) => {\n          return (\n            <div key={`${item.path}--${item.field}`}>\n              {item.field}:{' '}\n              {JSON.stringify(\n                JSONPath({ path: item.path, json: parsedJson, wrap: false })\n              )}\n            </div>\n          );\n        })}\n      </>\n    );\n  };\n\n  return (\n    <>\n      <S.ClickableRow\n        onMouseEnter={() => setVEllipsisOpen(true)}\n        onMouseLeave={() => setVEllipsisOpen(false)}\n        onClick={toggleIsOpen}\n      >\n        <td>\n          <IconButtonWrapper aria-hidden>\n            <MessageToggleIcon isOpen={isOpen} />\n          </IconButtonWrapper>\n        </td>\n        <td>{offset}</td>\n        <td>{partition}</td>\n        <td>\n          <div>{formatTimestamp(timestamp)}</div>\n        </td>\n        <S.DataCell title={key}>\n          <Ellipsis text={renderFilteredJson(key, keyFilters)}>\n            {keySerde === 'Fallback' && (\n              <Tooltip\n                value={<WarningRedIcon />}\n                content=\"Fallback serde was used\"\n                placement=\"left\"\n              />\n            )}\n          </Ellipsis>\n        </S.DataCell>\n        <S.DataCell title={content}>\n          <S.Metadata>\n            <S.MetadataValue>\n              <Ellipsis text={renderFilteredJson(content, contentFilters)}>\n                {valueSerde === 'Fallback' && (\n                  <Tooltip\n                    value={<WarningRedIcon />}\n                    content=\"Fallback serde was used\"\n                    placement=\"left\"\n                  />\n                )}\n              </Ellipsis>\n            </S.MetadataValue>\n          </S.Metadata>\n        </S.DataCell>\n        <td style={{ width: '5%' }}>\n          {vEllipsisOpen && (\n            <Dropdown>\n              <DropdownItem onClick={copyToClipboard}>\n                Copy to clipboard\n              </DropdownItem>\n              <DropdownItem onClick={saveFile}>Save as a file</DropdownItem>\n            </Dropdown>\n          )}\n        </td>\n      </S.ClickableRow>\n      {isOpen && (\n        <MessageContent\n          messageKey={key}\n          messageContent={content}\n          headers={headers}\n          timestamp={timestamp}\n          timestampType={timestampType}\n          keySize={keySize}\n          contentSize={valueSize}\n          keySerde={keySerde}\n          valueSerde={valueSerde}\n        />\n      )}\n    </>\n  );\n};\n\nexport default Message;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/MessageContent/MessageContent.styled.ts",
    "content": "import styled, { css } from 'styled-components';\nimport * as SEditorViewer from 'components/common/EditorViewer/EditorViewer.styled';\n\nexport const Wrapper = styled.tr`\n  background-color: ${({ theme }) => theme.topicMetaData.backgroundColor};\n  & > td {\n    padding: 16px;\n    &:first-child {\n      padding-right: 1px;\n    }\n    &:last-child {\n      padding-left: 1px;\n    }\n  }\n`;\n\nexport const Section = styled.div`\n  padding: 0 16px;\n  display: flex;\n  gap: 1px;\n  align-items: stretch;\n`;\n\nexport const ContentBox = styled.div`\n  background-color: ${({ theme }) => theme.topicMetaData.backgroundColor};\n  padding: 24px;\n  border-radius: 8px 0 0 8px;\n  flex-grow: 3;\n  display: flex;\n  flex-direction: column;\n  & nav {\n    padding-bottom: 16px;\n  }\n  ${SEditorViewer.Wrapper} {\n    flex-grow: 1;\n  }\n`;\nexport const DataCell = styled.td`\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  max-width: 350px;\n  min-width: 350px;\n`;\nexport const ClickableRow = styled.tr`\n  cursor: pointer;\n`;\nexport const MetadataWrapper = styled.div`\n  background-color: ${({ theme }) => theme.topicMetaData.backgroundColor};\n  padding: 24px;\n  border-radius: 0 8px 8px 0;\n  flex-grow: 1;\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n  min-width: 400px;\n`;\n\nexport const Metadata = styled.span`\n  display: flex;\n  gap: 35px;\n`;\n\nexport const MetadataLabel = styled.p`\n  color: ${({ theme }) => theme.topicMetaData.color.label};\n  font-size: 14px;\n  width: 80px;\n`;\n\nexport const MetadataValue = styled.div`\n  color: ${({ theme }) => theme.topicMetaData.color.value};\n  font-size: 14px;\n`;\n\nexport const MetadataMeta = styled.p`\n  color: ${({ theme }) => theme.topicMetaData.color.meta};\n  font-size: 12px;\n`;\n\nexport const Tab = styled.button<{ $active?: boolean }>(\n  ({ theme, $active }) => css`\n    background-color: ${theme.secondaryTab.backgroundColor[\n      $active ? 'active' : 'normal'\n    ]};\n    color: ${theme.secondaryTab.color[$active ? 'active' : 'normal']};\n    padding: 6px 16px;\n    height: 32px;\n    border: 1px solid ${theme.layout.stuffBorderColor};\n    cursor: pointer;\n    &:hover {\n      background-color: ${theme.secondaryTab.backgroundColor.hover};\n      color: ${theme.secondaryTab.color.hover};\n    }\n    &:first-child {\n      border-radius: 4px 0 0 4px;\n    }\n    &:last-child {\n      border-radius: 0 4px 4px 0;\n    }\n    &:not(:last-child) {\n      border-right: 0;\n    }\n  `\n);\nexport const Tabs = styled.nav``;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/MessageContent/MessageContent.tsx",
    "content": "import React from 'react';\nimport EditorViewer from 'components/common/EditorViewer/EditorViewer';\nimport BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';\nimport { SchemaType, TopicMessageTimestampTypeEnum } from 'generated-sources';\nimport { formatTimestamp } from 'lib/dateTimeHelpers';\n\nimport * as S from './MessageContent.styled';\n\ntype Tab = 'key' | 'content' | 'headers';\n\nexport interface MessageContentProps {\n  messageKey?: string;\n  messageContent?: string;\n  headers?: { [key: string]: string | undefined };\n  timestamp?: Date;\n  timestampType?: TopicMessageTimestampTypeEnum;\n  keySize?: number;\n  contentSize?: number;\n  keySerde?: string;\n  valueSerde?: string;\n}\n\nconst MessageContent: React.FC<MessageContentProps> = ({\n  messageKey,\n  messageContent,\n  headers,\n  timestamp,\n  timestampType,\n  keySize,\n  contentSize,\n  keySerde,\n  valueSerde,\n}) => {\n  const [activeTab, setActiveTab] = React.useState<Tab>('content');\n  const activeTabContent = () => {\n    switch (activeTab) {\n      case 'content':\n        return messageContent;\n      case 'key':\n        return messageKey;\n      default:\n        return JSON.stringify(headers);\n    }\n  };\n\n  const handleKeyTabClick = (e: React.MouseEvent) => {\n    e.preventDefault();\n    setActiveTab('key');\n  };\n\n  const handleContentTabClick = (e: React.MouseEvent) => {\n    e.preventDefault();\n    setActiveTab('content');\n  };\n\n  const handleHeadersTabClick = (e: React.MouseEvent) => {\n    e.preventDefault();\n    setActiveTab('headers');\n  };\n\n  const contentType =\n    messageContent && messageContent.trim().startsWith('{')\n      ? SchemaType.JSON\n      : SchemaType.PROTOBUF;\n\n  return (\n    <S.Wrapper>\n      <td colSpan={10}>\n        <S.Section>\n          <S.ContentBox>\n            <S.Tabs>\n              <S.Tab\n                type=\"button\"\n                $active={activeTab === 'key'}\n                onClick={handleKeyTabClick}\n              >\n                Key\n              </S.Tab>\n              <S.Tab\n                $active={activeTab === 'content'}\n                type=\"button\"\n                onClick={handleContentTabClick}\n              >\n                Value\n              </S.Tab>\n              <S.Tab\n                $active={activeTab === 'headers'}\n                type=\"button\"\n                onClick={handleHeadersTabClick}\n              >\n                Headers\n              </S.Tab>\n            </S.Tabs>\n            <EditorViewer\n              data={activeTabContent() || ''}\n              maxLines={28}\n              schemaType={contentType}\n            />\n          </S.ContentBox>\n          <S.MetadataWrapper>\n            <S.Metadata>\n              <S.MetadataLabel>Timestamp</S.MetadataLabel>\n              <span>\n                <S.MetadataValue>{formatTimestamp(timestamp)}</S.MetadataValue>\n                <S.MetadataMeta>Timestamp type: {timestampType}</S.MetadataMeta>\n              </span>\n            </S.Metadata>\n\n            <S.Metadata>\n              <S.MetadataLabel>Key Serde</S.MetadataLabel>\n              <span>\n                <S.MetadataValue>{keySerde}</S.MetadataValue>\n                <S.MetadataMeta>\n                  Size: <BytesFormatted value={keySize} />\n                </S.MetadataMeta>\n              </span>\n            </S.Metadata>\n\n            <S.Metadata>\n              <S.MetadataLabel>Value Serde</S.MetadataLabel>\n              <span>\n                <S.MetadataValue>{valueSerde}</S.MetadataValue>\n                <S.MetadataMeta>\n                  Size: <BytesFormatted value={contentSize} />\n                </S.MetadataMeta>\n              </span>\n            </S.Metadata>\n          </S.MetadataWrapper>\n        </S.Section>\n      </td>\n    </S.Wrapper>\n  );\n};\n\nexport default MessageContent;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/MessageContent/__tests__/MessageContent.spec.tsx",
    "content": "import { TextEncoder } from 'util';\n\nimport React from 'react';\nimport { screen } from '@testing-library/react';\nimport MessageContent, {\n  MessageContentProps,\n} from 'components/Topics/Topic/Messages/MessageContent/MessageContent';\nimport { TopicMessageTimestampTypeEnum } from 'generated-sources';\nimport userEvent from '@testing-library/user-event';\nimport { render } from 'lib/testHelpers';\nimport { theme } from 'theme/theme';\n\nconst setupWrapper = (props?: Partial<MessageContentProps>) => {\n  return (\n    <table>\n      <tbody>\n        <MessageContent\n          messageKey='\"test-key\"'\n          messageContent='{\"data\": \"test\"}'\n          headers={{ header: 'test' }}\n          timestamp={new Date(0)}\n          timestampType={TopicMessageTimestampTypeEnum.CREATE_TIME}\n          keySerde=\"SchemaRegistry\"\n          valueSerde=\"Avro\"\n          {...props}\n        />\n      </tbody>\n    </table>\n  );\n};\n\nglobal.TextEncoder = TextEncoder;\n\ndescribe('MessageContent screen', () => {\n  beforeEach(() => {\n    render(setupWrapper());\n  });\n\n  describe('Checking keySerde and valueSerde', () => {\n    it('keySerde in document', () => {\n      expect(screen.getByText('SchemaRegistry')).toBeInTheDocument();\n    });\n\n    it('valueSerde in document', () => {\n      expect(screen.getByText('Avro')).toBeInTheDocument();\n    });\n  });\n\n  describe('when switched to display the key', () => {\n    it('makes key tab active', async () => {\n      const keyTab = screen.getAllByText('Key');\n      await userEvent.click(keyTab[0]);\n      expect(keyTab[0]).toHaveStyleRule(\n        'background-color',\n        theme.secondaryTab.backgroundColor.active\n      );\n    });\n  });\n\n  describe('when switched to display the headers', () => {\n    it('makes Headers tab active', async () => {\n      await userEvent.click(screen.getByText('Headers'));\n      expect(screen.getByText('Headers')).toHaveStyleRule(\n        'background-color',\n        theme.secondaryTab.backgroundColor.active\n      );\n    });\n  });\n\n  describe('when switched to display the value', () => {\n    it('makes value tab active', async () => {\n      const contentTab = screen.getAllByText('Value');\n      await userEvent.click(contentTab[0]);\n      expect(contentTab[0]).toHaveStyleRule(\n        'background-color',\n        theme.secondaryTab.backgroundColor.active\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Messages.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const StopLoading = styled.div`\n  color: ${({ theme }) => theme.pageLoader.borderColor};\n  cursor: pointer;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/Messages.tsx",
    "content": "import React, { useCallback, useMemo, useState } from 'react';\nimport TopicMessagesContext from 'components/contexts/TopicMessagesContext';\nimport { SeekDirection, SerdeUsage } from 'generated-sources';\nimport { useSearchParams } from 'react-router-dom';\nimport { useSerdes } from 'lib/hooks/api/topicMessages';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { RouteParamsClusterTopic } from 'lib/paths';\nimport { getDefaultSerdeName } from 'components/Topics/Topic/Messages/getDefaultSerdeName';\nimport { MESSAGES_PER_PAGE } from 'lib/constants';\n\nimport MessagesTable from './MessagesTable';\nimport FiltersContainer from './Filters/FiltersContainer';\n\nexport const SeekDirectionOptionsObj = {\n  [SeekDirection.FORWARD]: {\n    value: SeekDirection.FORWARD,\n    label: 'Oldest First',\n    isLive: false,\n  },\n  [SeekDirection.BACKWARD]: {\n    value: SeekDirection.BACKWARD,\n    label: 'Newest First',\n    isLive: false,\n  },\n  [SeekDirection.TAILING]: {\n    value: SeekDirection.TAILING,\n    label: 'Live Mode',\n    isLive: true,\n  },\n};\n\nexport const SeekDirectionOptions = Object.values(SeekDirectionOptionsObj);\n\nconst Messages: React.FC = () => {\n  const [searchParams, setSearchParams] = useSearchParams();\n  const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();\n\n  const { data: serdes = {} } = useSerdes({\n    clusterName,\n    topicName,\n    use: SerdeUsage.DESERIALIZE,\n  });\n\n  React.useEffect(() => {\n    if (!searchParams.get('keySerde')) {\n      searchParams.set('keySerde', getDefaultSerdeName(serdes.key || []));\n    }\n    if (!searchParams.get('valueSerde')) {\n      searchParams.set('valueSerde', getDefaultSerdeName(serdes.value || []));\n    }\n    if (!searchParams.get('limit')) {\n      searchParams.set('limit', MESSAGES_PER_PAGE);\n    }\n    setSearchParams(searchParams);\n  }, [serdes]);\n\n  const defaultSeekValue = SeekDirectionOptions[0];\n\n  const [seekDirection, setSeekDirection] = React.useState<SeekDirection>(\n    (searchParams.get('seekDirection') as SeekDirection) ||\n      defaultSeekValue.value\n  );\n\n  const [isLive, setIsLive] = useState<boolean>(\n    SeekDirectionOptionsObj[seekDirection].isLive\n  );\n\n  const changeSeekDirection = useCallback((val: string) => {\n    switch (val) {\n      case SeekDirection.FORWARD:\n        setSeekDirection(SeekDirection.FORWARD);\n        setIsLive(SeekDirectionOptionsObj[SeekDirection.FORWARD].isLive);\n        break;\n      case SeekDirection.BACKWARD:\n        setSeekDirection(SeekDirection.BACKWARD);\n        setIsLive(SeekDirectionOptionsObj[SeekDirection.BACKWARD].isLive);\n        break;\n      case SeekDirection.TAILING:\n        setSeekDirection(SeekDirection.TAILING);\n        setIsLive(SeekDirectionOptionsObj[SeekDirection.TAILING].isLive);\n        break;\n      default:\n    }\n  }, []);\n\n  const contextValue = useMemo(\n    () => ({\n      seekDirection,\n      changeSeekDirection,\n      isLive,\n    }),\n    [seekDirection, changeSeekDirection]\n  );\n\n  return (\n    <TopicMessagesContext.Provider value={contextValue}>\n      <FiltersContainer />\n      <MessagesTable />\n    </TopicMessagesContext.Provider>\n  );\n};\n\nexport default Messages;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/MessagesTable.tsx",
    "content": "import PageLoader from 'components/common/PageLoader/PageLoader';\nimport { Table } from 'components/common/table/Table/Table.styled';\nimport TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';\nimport { TopicMessage } from 'generated-sources';\nimport React, { useContext, useState } from 'react';\nimport {\n  getTopicMessges,\n  getIsTopicMessagesFetching,\n} from 'redux/reducers/topicMessages/selectors';\nimport TopicMessagesContext from 'components/contexts/TopicMessagesContext';\nimport { useAppSelector } from 'lib/hooks/redux';\nimport { Button } from 'components/common/Button/Button';\nimport { useSearchParams } from 'react-router-dom';\nimport { MESSAGES_PER_PAGE } from 'lib/constants';\nimport * as S from 'components/common/NewTable/Table.styled';\n\nimport PreviewModal from './PreviewModal';\nimport Message, { PreviewFilter } from './Message';\n\nconst MessagesTable: React.FC = () => {\n  const [previewFor, setPreviewFor] = useState<string | null>(null);\n\n  const [keyFilters, setKeyFilters] = useState<PreviewFilter[]>([]);\n  const [contentFilters, setContentFilters] = useState<PreviewFilter[]>([]);\n\n  const [searchParams, setSearchParams] = useSearchParams();\n  const page = searchParams.get('page');\n  const { isLive } = useContext(TopicMessagesContext);\n\n  const messages = useAppSelector(getTopicMessges);\n  const isFetching = useAppSelector(getIsTopicMessagesFetching);\n\n  const isTailing = isLive && isFetching;\n\n  // Pagination is disabled in live mode, also we don't want to show the button\n  // if we are fetching the messages or if we are at the end of the topic\n  const isPaginationDisabled = isTailing || isFetching;\n\n  const isNextPageButtonDisabled =\n    isPaginationDisabled || messages.length < Number(MESSAGES_PER_PAGE);\n  const isPrevPageButtonDisabled =\n    isPaginationDisabled || !Number(searchParams.get('page'));\n\n  const handleNextPage = () => {\n    searchParams.set('page', String(Number(page || 0) + 1));\n    setSearchParams(searchParams);\n  };\n\n  const handlePrevPage = () => {\n    searchParams.set('page', String(Number(page || 0) - 1));\n    setSearchParams(searchParams);\n  };\n\n  return (\n    <div style={{ position: 'relative' }}>\n      {previewFor !== null && (\n        <PreviewModal\n          values={previewFor === 'key' ? keyFilters : contentFilters}\n          toggleIsOpen={() => setPreviewFor(null)}\n          setFilters={(payload: PreviewFilter[]) =>\n            previewFor === 'key'\n              ? setKeyFilters(payload)\n              : setContentFilters(payload)\n          }\n        />\n      )}\n      <Table isFullwidth>\n        <thead>\n          <tr>\n            <TableHeaderCell> </TableHeaderCell>\n            <TableHeaderCell title=\"Offset\" />\n            <TableHeaderCell title=\"Partition\" />\n            <TableHeaderCell title=\"Timestamp\" />\n            <TableHeaderCell\n              title=\"Key\"\n              previewText={`Preview ${\n                keyFilters.length ? `(${keyFilters.length} selected)` : ''\n              }`}\n              onPreview={() => setPreviewFor('key')}\n            />\n            <TableHeaderCell\n              title=\"Value\"\n              previewText={`Preview ${\n                contentFilters.length\n                  ? `(${contentFilters.length} selected)`\n                  : ''\n              }`}\n              onPreview={() => setPreviewFor('content')}\n            />\n            <TableHeaderCell> </TableHeaderCell>\n          </tr>\n        </thead>\n        <tbody>\n          {messages.map((message: TopicMessage) => (\n            <Message\n              key={[\n                message.offset,\n                message.timestamp,\n                message.key,\n                message.partition,\n              ].join('-')}\n              message={message}\n              keyFilters={keyFilters}\n              contentFilters={contentFilters}\n            />\n          ))}\n          {isFetching && isLive && !messages.length && (\n            <tr>\n              <td colSpan={10}>\n                <PageLoader />\n              </td>\n            </tr>\n          )}\n          {messages.length === 0 && !isFetching && (\n            <tr>\n              <td colSpan={10}>No messages found</td>\n            </tr>\n          )}\n        </tbody>\n      </Table>\n      <S.Pagination>\n        <S.Pages>\n          <Button\n            buttonType=\"secondary\"\n            buttonSize=\"L\"\n            disabled={isPrevPageButtonDisabled}\n            onClick={handlePrevPage}\n          >\n            ← Back\n          </Button>\n          <Button\n            buttonType=\"secondary\"\n            buttonSize=\"L\"\n            disabled={isNextPageButtonDisabled}\n            onClick={handleNextPage}\n          >\n            Next →\n          </Button>\n        </S.Pages>\n      </S.Pagination>\n    </div>\n  );\n};\n\nexport default MessagesTable;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/PreviewModal.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const PreviewModal = styled.div`\n  height: auto;\n  width: 560px;\n  border-radius: 8px;\n  background: ${({ theme }) => theme.modal.backgroundColor};\n  position: absolute;\n  left: 25%;\n  top: 30px; // some margin\n  border: 1px solid ${({ theme }) => theme.modal.border.contrast};\n  box-shadow: ${({ theme }) => theme.modal.shadow};\n  padding: 32px;\n  z-index: 1;\n`;\n\nexport const ButtonWrapper = styled.div`\n  width: 100%;\n  display: flex;\n  justify-content: center;\n  margin-top: 20px;\n  gap: 10px;\n`;\n\nexport const EditForm = styled.div`\n  font-weight: 500;\n  padding-bottom: 7px;\n  display: flex;\n`;\n\nexport const Field = styled.div`\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  overflow: hidden;\n  margin-right: 5px;\n  color: ${({ theme }) => theme.modal.color};\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/PreviewModal.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { Button } from 'components/common/Button/Button';\nimport { FormError } from 'components/common/Input/Input.styled';\nimport Input from 'components/common/Input/Input';\nimport { InputLabel } from 'components/common/Input/InputLabel.styled';\nimport IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';\nimport EditIcon from 'components/common/Icons/EditIcon';\nimport CancelIcon from 'components/common/Icons/CancelIcon';\n\nimport * as S from './PreviewModal.styled';\nimport { PreviewFilter } from './Message';\n\nexport interface InfoModalProps {\n  values: PreviewFilter[];\n  toggleIsOpen(): void;\n  setFilters: (payload: PreviewFilter[]) => void;\n}\n\nconst PreviewModal: React.FC<InfoModalProps> = ({\n  values,\n  toggleIsOpen,\n  setFilters,\n}) => {\n  const [field, setField] = React.useState('');\n  const [path, setPath] = React.useState('');\n  const [errors, setErrors] = React.useState<string[]>([]);\n  const [editIndex, setEditIndex] = React.useState<number | undefined>();\n\n  const handleOk = () => {\n    const newErrors = [];\n\n    if (field === '') {\n      newErrors.push('field');\n    }\n\n    if (path === '') {\n      newErrors.push('path');\n    }\n\n    if (newErrors?.length) {\n      setErrors(newErrors);\n      return;\n    }\n\n    const newValues = [...values];\n\n    if (typeof editIndex !== 'undefined') {\n      newValues.splice(editIndex, 1, { field, path });\n    } else {\n      newValues.push({ field, path });\n    }\n\n    setFilters(newValues);\n    toggleIsOpen();\n  };\n\n  const handleRemove = (filter: PreviewFilter) => {\n    const newValues = values.filter(\n      (item) => item.field !== filter.field && item.path !== filter.path\n    );\n\n    setFilters(newValues);\n  };\n\n  useEffect(() => {\n    if (values?.length && typeof editIndex !== 'undefined') {\n      setField(values[editIndex].field);\n      setPath(values[editIndex].path);\n    }\n  }, [editIndex]);\n\n  return (\n    <S.PreviewModal>\n      {values.map((item, index) => (\n        <S.EditForm key=\"index\">\n          <S.Field>\n            {' '}\n            {item.field} : {item.path}\n          </S.Field>\n          <IconButtonWrapper role=\"button\" onClick={() => setEditIndex(index)}>\n            <EditIcon />\n          </IconButtonWrapper>\n          {'  '}\n          <IconButtonWrapper role=\"button\" onClick={() => handleRemove(item)}>\n            <CancelIcon />\n          </IconButtonWrapper>\n        </S.EditForm>\n      ))}\n      <div>\n        <InputLabel htmlFor=\"previewFormField\">Field</InputLabel>\n        <Input\n          type=\"text\"\n          id=\"previewFormField\"\n          min=\"1\"\n          value={field}\n          placeholder=\"Field\"\n          onChange={({ target }) => setField(target?.value)}\n        />\n        <FormError>{errors.includes('field') && 'Field is required'}</FormError>\n      </div>\n      <div>\n        <InputLabel htmlFor=\"previewFormJsonPath\">Json path</InputLabel>\n        <Input\n          type=\"text\"\n          id=\"previewFormJsonPath\"\n          min=\"1\"\n          value={path}\n          placeholder=\"Json Path\"\n          onChange={({ target }) => setPath(target?.value)}\n        />\n        <FormError>\n          {errors.includes('path') && 'Json path is required'}\n        </FormError>\n      </div>\n      <S.ButtonWrapper>\n        <Button\n          buttonSize=\"M\"\n          buttonType=\"secondary\"\n          type=\"button\"\n          onClick={toggleIsOpen}\n        >\n          Close\n        </Button>\n        <Button\n          buttonSize=\"M\"\n          buttonType=\"secondary\"\n          type=\"button\"\n          onClick={handleOk}\n        >\n          Save\n        </Button>\n      </S.ButtonWrapper>\n    </S.PreviewModal>\n  );\n};\n\nexport default PreviewModal;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/FiltersContainer.spec.tsx",
    "content": "import React from 'react';\nimport FiltersContainer from 'components/Topics/Topic/Messages/Filters/FiltersContainer';\nimport { screen } from '@testing-library/react';\nimport { render } from 'lib/testHelpers';\n\njest.mock('components/Topics/Topic/Messages/Filters/Filters', () => () => (\n  <div>mock-Filters</div>\n));\n\ndescribe('FiltersContainer', () => {\n  it('renders Filters component', () => {\n    render(<FiltersContainer />);\n    expect(screen.getByText('mock-Filters')).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/Message.spec.tsx",
    "content": "import React from 'react';\nimport { TopicMessage, TopicMessageTimestampTypeEnum } from 'generated-sources';\nimport Message, {\n  PreviewFilter,\n  Props,\n} from 'components/Topics/Topic/Messages/Message';\nimport { screen } from '@testing-library/react';\nimport { render } from 'lib/testHelpers';\nimport userEvent from '@testing-library/user-event';\nimport { formatTimestamp } from 'lib/dateTimeHelpers';\n\nconst messageContentText = 'messageContentText';\n\nconst keyTest = '{\"payload\":{\"subreddit\":\"learnprogramming\"}}';\nconst contentTest =\n  '{\"payload\":{\"author\":\"DwaywelayTOP\",\"archived\":false,\"name\":\"t3_11jshwd\",\"id\":\"11jshwd\"}}';\njest.mock(\n  'components/Topics/Topic/Messages/MessageContent/MessageContent',\n  () => () =>\n    (\n      <tr>\n        <td>{messageContentText}</td>\n      </tr>\n    )\n);\n\ndescribe('Message component', () => {\n  const mockMessage: TopicMessage = {\n    timestamp: new Date(),\n    timestampType: TopicMessageTimestampTypeEnum.CREATE_TIME,\n    offset: 0,\n    key: 'test-key',\n    partition: 6,\n    content: '{\"data\": \"test\"}',\n    headers: { header: 'test' },\n  };\n  const mockKeyFilters: PreviewFilter = {\n    field: 'sub',\n    path: '$.payload.subreddit',\n  };\n  const mockContentFilters: PreviewFilter = {\n    field: 'author',\n    path: '$.payload.author',\n  };\n  const renderComponent = (\n    props: Partial<Props> = {\n      message: mockMessage,\n      keyFilters: [],\n      contentFilters: [],\n    }\n  ) =>\n    render(\n      <table>\n        <tbody>\n          <Message\n            message={props.message || mockMessage}\n            keyFilters={props.keyFilters || []}\n            contentFilters={props.contentFilters || []}\n          />\n        </tbody>\n      </table>\n    );\n\n  it('shows the data in the table row', () => {\n    renderComponent();\n    expect(screen.getByText(mockMessage.content as string)).toBeInTheDocument();\n    expect(screen.getByText(mockMessage.key as string)).toBeInTheDocument();\n    expect(\n      screen.getByText(formatTimestamp(mockMessage.timestamp))\n    ).toBeInTheDocument();\n    expect(screen.getByText(mockMessage.offset.toString())).toBeInTheDocument();\n    expect(\n      screen.getByText(mockMessage.partition.toString())\n    ).toBeInTheDocument();\n  });\n\n  it('check the useDataSaver functionality', () => {\n    const props = { message: { ...mockMessage } };\n    delete props.message.content;\n    renderComponent(props);\n    expect(\n      screen.queryByText(mockMessage.content as string)\n    ).not.toBeInTheDocument();\n  });\n\n  it('should check the dropdown being visible during hover', async () => {\n    renderComponent();\n    const text = 'Save as a file';\n    const trElement = screen.getByRole('row');\n    expect(screen.queryByText(text)).not.toBeInTheDocument();\n\n    await userEvent.hover(trElement);\n    expect(screen.getByText(text)).toBeInTheDocument();\n\n    await userEvent.unhover(trElement);\n    expect(screen.queryByText(text)).not.toBeInTheDocument();\n  });\n\n  it('should check open Message Content functionality', async () => {\n    renderComponent();\n    const messageToggleIcon = screen.getByRole('button', { hidden: true });\n    expect(screen.queryByText(messageContentText)).not.toBeInTheDocument();\n    await userEvent.click(messageToggleIcon);\n    expect(screen.getByText(messageContentText)).toBeInTheDocument();\n  });\n\n  it('should check if Preview filter showing for key', () => {\n    const props = {\n      message: { ...mockMessage, key: keyTest as string },\n      keyFilters: [mockKeyFilters],\n    };\n    renderComponent(props);\n    const keyFiltered = screen.getByText('sub: \"learnprogramming\"');\n    expect(keyFiltered).toBeInTheDocument();\n  });\n\n  it('should check if Preview filter showing for Value', () => {\n    const props = {\n      message: { ...mockMessage, content: contentTest as string },\n      contentFilters: [mockContentFilters],\n    };\n    renderComponent(props);\n    const keyFiltered = screen.getByText('author: \"DwaywelayTOP\"');\n    expect(keyFiltered).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/Messages.spec.tsx",
    "content": "import React from 'react';\nimport { screen, waitFor } from '@testing-library/react';\nimport { render, EventSourceMock, WithRoute } from 'lib/testHelpers';\nimport Messages, {\n  SeekDirectionOptions,\n  SeekDirectionOptionsObj,\n} from 'components/Topics/Topic/Messages/Messages';\nimport { SeekDirection, SeekType } from 'generated-sources';\nimport userEvent from '@testing-library/user-event';\nimport { clusterTopicMessagesPath } from 'lib/paths';\nimport { useSerdes } from 'lib/hooks/api/topicMessages';\nimport { serdesPayload } from 'lib/fixtures/topicMessages';\nimport { useTopicDetails } from 'lib/hooks/api/topics';\nimport { externalTopicPayload } from 'lib/fixtures/topics';\n\njest.mock('lib/hooks/api/topicMessages', () => ({\n  useSerdes: jest.fn(),\n}));\n\njest.mock('lib/hooks/api/topics', () => ({\n  useTopicDetails: jest.fn(),\n}));\n\ndescribe('Messages', () => {\n  const searchParams = `?filterQueryType=STRING_CONTAINS&attempt=0&limit=100&seekDirection=${SeekDirection.FORWARD}&seekType=${SeekType.OFFSET}&seekTo=0::9`;\n  const renderComponent = (param: string = searchParams) => {\n    const query = new URLSearchParams(param).toString();\n    const path = `${clusterTopicMessagesPath()}?${query}`;\n    return render(\n      <WithRoute path={clusterTopicMessagesPath()}>\n        <Messages />\n      </WithRoute>,\n      {\n        initialEntries: [path],\n      }\n    );\n  };\n\n  beforeEach(() => {\n    Object.defineProperty(window, 'EventSource', {\n      value: EventSourceMock,\n    });\n    (useSerdes as jest.Mock).mockImplementation(() => ({\n      data: serdesPayload,\n    }));\n    (useTopicDetails as jest.Mock).mockImplementation(() => ({\n      data: externalTopicPayload,\n    }));\n  });\n  describe('component rendering default behavior with the search params', () => {\n    beforeEach(() => {\n      renderComponent();\n    });\n    it('should check default seekDirection if it actually take the value from the url', () => {\n      expect(screen.getAllByRole('listbox')[3]).toHaveTextContent(\n        SeekDirectionOptionsObj[SeekDirection.FORWARD].label\n      );\n    });\n\n    it('should check the SeekDirection select changes with live option', async () => {\n      const seekDirectionSelect = screen.getAllByRole('listbox')[3];\n      const seekDirectionOption = screen.getAllByRole('option')[3];\n\n      expect(seekDirectionOption).toHaveTextContent(\n        SeekDirectionOptionsObj[SeekDirection.FORWARD].label\n      );\n\n      const labelValue1 = SeekDirectionOptions[1].label;\n      await userEvent.click(seekDirectionSelect);\n      await userEvent.selectOptions(seekDirectionSelect, [labelValue1]);\n      expect(seekDirectionOption).toHaveTextContent(labelValue1);\n\n      const labelValue0 = SeekDirectionOptions[0].label;\n      await userEvent.click(seekDirectionSelect);\n      await userEvent.selectOptions(seekDirectionSelect, [labelValue0]);\n      expect(seekDirectionOption).toHaveTextContent(labelValue0);\n\n      const liveOptionConf = SeekDirectionOptions[2];\n      const labelValue2 = liveOptionConf.label;\n      await userEvent.click(seekDirectionSelect);\n\n      const options = screen.getAllByRole('option');\n      const liveModeLi = options.find(\n        (option) => option.getAttribute('value') === liveOptionConf.value\n      );\n      expect(liveModeLi).toBeInTheDocument();\n      if (!liveModeLi) return; // to make TS happy\n      await userEvent.selectOptions(seekDirectionSelect, [liveModeLi]);\n      expect(seekDirectionOption).toHaveTextContent(labelValue2);\n\n      await waitFor(() => {\n        expect(screen.getByRole('contentLoader')).toBeInTheDocument();\n      });\n    });\n  });\n\n  describe('Component rendering with custom Url search params', () => {\n    it('reacts to a change of seekDirection in the url which make the select pick up different value', () => {\n      renderComponent(\n        searchParams.replace(SeekDirection.FORWARD, SeekDirection.BACKWARD)\n      );\n      expect(screen.getAllByRole('listbox')[3]).toHaveTextContent(\n        SeekDirectionOptionsObj[SeekDirection.BACKWARD].label\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/MessagesTable.spec.tsx",
    "content": "import React from 'react';\nimport { screen } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { render } from 'lib/testHelpers';\nimport MessagesTable from 'components/Topics/Topic/Messages/MessagesTable';\nimport { SeekDirection, SeekType, TopicMessage } from 'generated-sources';\nimport TopicMessagesContext, {\n  ContextProps,\n} from 'components/contexts/TopicMessagesContext';\nimport {\n  topicMessagePayload,\n  topicMessagesMetaPayload,\n} from 'redux/reducers/topicMessages/__test__/fixtures';\n\nconst mockTopicsMessages: TopicMessage[] = [{ ...topicMessagePayload }];\n\nconst mockNavigate = jest.fn();\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useNavigate: () => mockNavigate,\n}));\n\ndescribe('MessagesTable', () => {\n  const searchParams = new URLSearchParams({\n    filterQueryType: 'STRING_CONTAINS',\n    attempt: '0',\n    limit: '100',\n    seekDirection: SeekDirection.FORWARD,\n    seekType: SeekType.OFFSET,\n    seekTo: '0::9',\n  });\n  const contextValue: ContextProps = {\n    isLive: false,\n    seekDirection: SeekDirection.FORWARD,\n    changeSeekDirection: jest.fn(),\n  };\n\n  const renderComponent = (\n    params: URLSearchParams = searchParams,\n    ctx: ContextProps = contextValue,\n    messages: TopicMessage[] = [],\n    isFetching?: boolean,\n    path?: string\n  ) => {\n    const customPath = path || params.toString();\n    return render(\n      <TopicMessagesContext.Provider value={ctx}>\n        <MessagesTable />\n      </TopicMessagesContext.Provider>,\n      {\n        initialEntries: [`/messages?${customPath}`],\n        preloadedState: {\n          topicMessages: {\n            messages,\n            meta: {\n              ...topicMessagesMetaPayload,\n            },\n            isFetching: !!isFetching,\n          },\n        },\n      }\n    );\n  };\n\n  describe('Default props Setup for MessagesTable component', () => {\n    beforeEach(() => {\n      renderComponent();\n    });\n\n    it('should check the render', () => {\n      expect(screen.getByRole('table')).toBeInTheDocument();\n    });\n\n    it('should check preview buttons', async () => {\n      const previewButtons = await screen.findAllByRole('button', {\n        name: 'Preview',\n      });\n      expect(previewButtons).toHaveLength(2);\n    });\n\n    it('should show preview modal with validation', async () => {\n      await userEvent.click(screen.getAllByText('Preview')[0]);\n      expect(screen.getByPlaceholderText('Field')).toHaveValue('');\n      expect(screen.getByPlaceholderText('Json Path')).toHaveValue('');\n    });\n\n    it('should check the if no elements is rendered in the table', () => {\n      expect(screen.getByText(/No messages found/i)).toBeInTheDocument();\n    });\n  });\n\n  describe('Custom Setup with different props value', () => {\n    it('should check if next button and previous is disabled isLive Param', () => {\n      renderComponent(searchParams, { ...contextValue, isLive: true });\n      expect(screen.queryByText(/next/i)).toBeDisabled();\n      expect(screen.queryByText(/back/i)).toBeDisabled();\n    });\n\n    it('should check the display of the loader element', () => {\n      renderComponent(\n        searchParams,\n        { ...contextValue, isLive: true },\n        [],\n        true\n      );\n      expect(screen.getByRole('progressbar')).toBeInTheDocument();\n    });\n  });\n\n  describe('should render Messages table with data', () => {\n    beforeEach(() => {\n      renderComponent(searchParams, { ...contextValue }, mockTopicsMessages);\n    });\n\n    it('should check the rendering of the messages', () => {\n      expect(screen.queryByText(/No messages found/i)).not.toBeInTheDocument();\n      expect(\n        screen.getByText(mockTopicsMessages[0].content as string)\n      ).toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/PreviewModal.spec.tsx",
    "content": "import userEvent from '@testing-library/user-event';\nimport { act, screen } from '@testing-library/react';\nimport { render } from 'lib/testHelpers';\nimport React from 'react';\nimport { PreviewFilter } from 'components/Topics/Topic/Messages/Message';\nimport { serdesPayload } from 'lib/fixtures/topicMessages';\nimport { useSerdes } from 'lib/hooks/api/topicMessages';\nimport PreviewModal, {\n  InfoModalProps,\n} from 'components/Topics/Topic/Messages/PreviewModal';\n\njest.mock('components/common/Icons/CloseIcon', () => () => 'mock-CloseIcon');\n\njest.mock('lib/hooks/api/topicMessages', () => ({\n  useSerdes: jest.fn(),\n}));\n\nbeforeEach(async () => {\n  (useSerdes as jest.Mock).mockImplementation(() => ({\n    data: serdesPayload,\n  }));\n});\n\nconst toggleInfoModal = jest.fn();\nconst mockValues: PreviewFilter[] = [\n  {\n    field: '',\n    path: '',\n  },\n];\n\nconst renderComponent = (props?: Partial<InfoModalProps>) => {\n  render(\n    <PreviewModal\n      toggleIsOpen={toggleInfoModal}\n      values={mockValues}\n      setFilters={jest.fn()}\n      {...props}\n    />\n  );\n};\n\ndescribe('PreviewModal component', () => {\n  it('closes PreviewModal', async () => {\n    renderComponent();\n    await userEvent.click(screen.getByRole('button', { name: 'Close' }));\n    expect(toggleInfoModal).toHaveBeenCalledTimes(1);\n  });\n\n  it('return if empty inputs', async () => {\n    renderComponent();\n    await userEvent.click(screen.getByRole('button', { name: 'Save' }));\n    expect(screen.getByText('Json path is required')).toBeInTheDocument();\n    expect(screen.getByText('Field is required')).toBeInTheDocument();\n  });\n\n  describe('Input elements', () => {\n    const fieldValue = 'type';\n    const pathValue = 'schema.type';\n\n    beforeEach(async () => {\n      await act(() => {\n        renderComponent();\n      });\n    });\n\n    it('field input', async () => {\n      const fieldInput = screen.getByPlaceholderText('Field');\n      expect(fieldInput).toHaveValue('');\n      await userEvent.type(fieldInput, fieldValue);\n      expect(fieldInput).toHaveValue(fieldValue);\n    });\n\n    it('path input', async () => {\n      const pathInput = screen.getByPlaceholderText('Json Path');\n      expect(pathInput).toHaveValue('');\n      await userEvent.type(pathInput, pathValue);\n      expect(pathInput).toHaveValue(pathValue.toString());\n    });\n  });\n\n  describe('edit and remove functionality', () => {\n    const fieldValue = 'type new';\n    const pathValue = 'schema.type.new';\n\n    it('remove values', async () => {\n      const setFilters = jest.fn();\n      await act(() => {\n        renderComponent({ setFilters });\n      });\n      await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));\n      expect(setFilters).toHaveBeenCalledTimes(1);\n    });\n\n    it('edit values', async () => {\n      const setFilters = jest.fn();\n      const toggleIsOpen = jest.fn();\n      await act(() => {\n        renderComponent({ setFilters });\n      });\n      userEvent.click(screen.getByRole('button', { name: 'Edit' }));\n      const fieldInput = screen.getByPlaceholderText('Field');\n      userEvent.type(fieldInput, fieldValue);\n      const pathInput = screen.getByPlaceholderText('Json Path');\n      userEvent.type(pathInput, pathValue);\n      userEvent.click(screen.getByRole('button', { name: 'Save' }));\n      await act(() => {\n        renderComponent({ setFilters, toggleIsOpen });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/utils.spec.ts",
    "content": "import { Option } from 'react-multi-select-component';\nimport {\n  filterOptions,\n  getOffsetFromSeekToParam,\n  getTimestampFromSeekToParam,\n  getSelectedPartitionsFromSeekToParam,\n} from 'components/Topics/Topic/Messages/Filters/utils';\nimport { SeekType, Partition } from 'generated-sources';\n\nconst options: Option[] = [\n  {\n    value: 0,\n    label: 'Partition #0',\n  },\n  {\n    value: 1,\n    label: 'Partition #1',\n  },\n  {\n    value: 11,\n    label: 'Partition #11',\n  },\n  {\n    value: 21,\n    label: 'Partition #21',\n  },\n];\n\nlet paramsString;\nlet searchParams = new URLSearchParams(paramsString);\n\ndescribe('utils', () => {\n  describe('filterOptions', () => {\n    it('returns options if no filter is defined', () => {\n      expect(filterOptions(options, '')).toEqual(options);\n    });\n\n    it('returns filtered options', () => {\n      expect(filterOptions(options, '11')).toEqual([options[2]]);\n    });\n  });\n\n  describe('getOffsetFromSeekToParam', () => {\n    beforeEach(() => {\n      paramsString = 'seekTo=0::123,1::123,2::0';\n      searchParams = new URLSearchParams(paramsString);\n    });\n\n    it('returns nothing when param \"seekType\" is equal BEGGINING', () => {\n      searchParams.set('seekType', SeekType.BEGINNING);\n      expect(getOffsetFromSeekToParam(searchParams)).toEqual('');\n    });\n\n    it('returns nothing when param \"seekType\" is equal TIMESTAMP', () => {\n      searchParams.set('seekType', SeekType.TIMESTAMP);\n      expect(getOffsetFromSeekToParam(searchParams)).toEqual('');\n    });\n\n    it('returns correct messages list when param \"seekType\" is equal OFFSET', () => {\n      searchParams.set('seekType', SeekType.OFFSET);\n      expect(getOffsetFromSeekToParam(searchParams)).toEqual('123');\n    });\n\n    it('returns 0 when param \"seekTo\" is not defined and param \"seekType\" is equal OFFSET', () => {\n      searchParams.set('seekType', SeekType.OFFSET);\n      searchParams.delete('seekTo');\n      expect(getOffsetFromSeekToParam(searchParams)).toEqual('0');\n    });\n  });\n\n  describe('getTimestampFromSeekToParam', () => {\n    beforeEach(() => {\n      paramsString = `seekTo=0::1627333200000,1::1627333200000`;\n      searchParams = new URLSearchParams(paramsString);\n    });\n\n    it('returns null when param \"seekType\" is equal BEGGINING', () => {\n      searchParams.set('seekType', SeekType.BEGINNING);\n      expect(getTimestampFromSeekToParam(searchParams)).toEqual(null);\n    });\n    it('returns null when param \"seekType\" is equal OFFSET', () => {\n      searchParams.set('seekType', SeekType.OFFSET);\n      expect(getTimestampFromSeekToParam(searchParams)).toEqual(null);\n    });\n    it('returns correct messages list when param \"seekType\" is equal TIMESTAMP', () => {\n      searchParams.set('seekType', SeekType.TIMESTAMP);\n      expect(getTimestampFromSeekToParam(searchParams)).toEqual(\n        new Date(1627333200000)\n      );\n    });\n    it('returns default timestamp when param \"seekTo\" is empty and param \"seekType\" is equal TIMESTAMP', () => {\n      searchParams.set('seekType', SeekType.TIMESTAMP);\n      searchParams.delete('seekTo');\n      expect(getTimestampFromSeekToParam(searchParams)).toEqual(new Date(0));\n    });\n  });\n\n  describe('getSelectedPartitionsFromSeekToParam', () => {\n    const part: Partition[] = [{ partition: 42, offsetMin: 0, offsetMax: 100 }];\n\n    it('returns parsed partition from params when partition list includes selected partition', () => {\n      searchParams.set('seekTo', '42::0');\n      expect(getSelectedPartitionsFromSeekToParam(searchParams, part)).toEqual([\n        { label: 'Partition #42', value: 42 },\n      ]);\n    });\n    it('returns parsed partition from params when partition list NOT includes selected partition', () => {\n      searchParams.set('seekTo', '24::0');\n      expect(getSelectedPartitionsFromSeekToParam(searchParams, part)).toEqual(\n        []\n      );\n    });\n    it('returns partitions when param \"seekTo\" is not defined', () => {\n      searchParams.delete('seekTo');\n      expect(getSelectedPartitionsFromSeekToParam(searchParams, part)).toEqual([\n        { label: 'Partition #42', value: 42 },\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Messages/getDefaultSerdeName.ts",
    "content": "import { SerdeDescription } from 'generated-sources';\nimport { getPreferredDescription } from 'components/Topics/Topic/SendMessage/utils';\n\nexport const getDefaultSerdeName = (serdes: SerdeDescription[]) => {\n  const preffered = getPreferredDescription(serdes);\n  if (preffered) {\n    return preffered.name || '';\n  }\n  if (serdes.length > 0) {\n    return serdes[0].name || '';\n  }\n  return '';\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Overview/ActionsCell.tsx",
    "content": "import React from 'react';\nimport { Action, Partition, ResourceType } from 'generated-sources';\nimport { CellContext } from '@tanstack/react-table';\nimport ClusterContext from 'components/contexts/ClusterContext';\nimport { RouteParamsClusterTopic } from 'lib/paths';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { Dropdown } from 'components/common/Dropdown';\nimport { useClearTopicMessages, useTopicDetails } from 'lib/hooks/api/topics';\nimport { ActionDropdownItem } from 'components/common/ActionComponent';\n\nconst ActionsCell: React.FC<CellContext<Partition, unknown>> = ({ row }) => {\n  const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();\n  const { data } = useTopicDetails({ clusterName, topicName });\n  const { isReadOnly } = React.useContext(ClusterContext);\n  const { partition } = row.original;\n\n  const clearMessages = useClearTopicMessages(clusterName, [partition]);\n\n  const clearTopicMessagesHandler = async () => {\n    await clearMessages.mutateAsync(topicName);\n  };\n  const disabled =\n    data?.internal || isReadOnly || data?.cleanUpPolicy !== 'DELETE';\n  return (\n    <Dropdown disabled={disabled}>\n      <ActionDropdownItem\n        onClick={clearTopicMessagesHandler}\n        danger\n        permission={{\n          resource: ResourceType.TOPIC,\n          action: Action.MESSAGES_DELETE,\n          value: topicName,\n        }}\n      >\n        Clear Messages\n      </ActionDropdownItem>\n    </Dropdown>\n  );\n};\n\nexport default ActionsCell;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Overview/Overview.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const Replica = styled.span.attrs({ 'aria-label': 'replica-info' })<{\n  leader?: boolean;\n  outOfSync?: boolean;\n}>`\n  color: ${({ leader, outOfSync, theme }) => {\n    if (outOfSync) return theme.topicMetaData.outOfSync.color;\n    if (leader) return theme.topicMetaData.liderReplica.color;\n    return null;\n  }};\n\n  font-weight: ${({ outOfSync }) => (outOfSync ? '500' : null)};\n\n  &:after {\n    content: ', ';\n  }\n\n  &:last-child::after {\n    content: '';\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Overview/Overview.tsx",
    "content": "import React from 'react';\nimport type { Partition, Replica } from 'generated-sources';\nimport BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';\nimport Table from 'components/common/NewTable';\nimport * as Metrics from 'components/common/Metrics';\nimport { Tag } from 'components/common/Tag/Tag.styled';\nimport { RouteParamsClusterTopic } from 'lib/paths';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { useTopicDetails } from 'lib/hooks/api/topics';\nimport { ColumnDef } from '@tanstack/react-table';\n\nimport * as S from './Overview.styled';\nimport ActionsCell from './ActionsCell';\n\nconst Overview: React.FC = () => {\n  const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();\n  const { data } = useTopicDetails({ clusterName, topicName });\n\n  const messageCount = React.useMemo(\n    () =>\n      (data?.partitions || []).reduce((memo, partition) => {\n        return memo + partition.offsetMax - partition.offsetMin;\n      }, 0),\n    [data]\n  );\n  const newData = React.useMemo(() => {\n    if (!data?.partitions) return [];\n\n    return data.partitions.map((items: Partition) => {\n      return {\n        ...items,\n        messageCount: items.offsetMax - items.offsetMin,\n      };\n    });\n  }, [data?.partitions]);\n\n  const columns = React.useMemo<ColumnDef<Partition>[]>(\n    () => [\n      {\n        header: 'Partition ID',\n        enableSorting: false,\n        accessorKey: 'partition',\n      },\n      {\n        header: 'Replicas',\n        enableSorting: false,\n\n        accessorKey: 'replicas',\n        cell: ({ getValue }) => {\n          const replicas = getValue<Partition['replicas']>();\n          if (replicas === undefined || replicas.length === 0) {\n            return 0;\n          }\n          return replicas?.map(({ broker, leader, inSync }: Replica) => (\n            <S.Replica\n              leader={leader}\n              outOfSync={!inSync}\n              key={broker}\n              title={leader ? 'Leader' : ''}\n            >\n              {broker}\n            </S.Replica>\n          ));\n        },\n      },\n      {\n        header: 'First Offset',\n        enableSorting: false,\n        accessorKey: 'offsetMin',\n      },\n      { header: 'Next Offset', enableSorting: false, accessorKey: 'offsetMax' },\n      {\n        header: 'Message Count',\n        enableSorting: false,\n        accessorKey: `messageCount`,\n      },\n      {\n        header: '',\n        enableSorting: false,\n        accessorKey: 'actions',\n        cell: ActionsCell,\n      },\n    ],\n    []\n  );\n  return (\n    <>\n      <Metrics.Wrapper>\n        <Metrics.Section>\n          <Metrics.Indicator label=\"Partitions\">\n            {data?.partitionCount}\n          </Metrics.Indicator>\n          <Metrics.Indicator label=\"Replication Factor\">\n            {data?.replicationFactor}\n          </Metrics.Indicator>\n          <Metrics.Indicator\n            label=\"URP\"\n            title=\"Under replicated partitions\"\n            isAlert\n            alertType={\n              data?.underReplicatedPartitions === 0 ? 'success' : 'error'\n            }\n          >\n            {data?.underReplicatedPartitions === 0 ? (\n              <Metrics.LightText>\n                {data?.underReplicatedPartitions}\n              </Metrics.LightText>\n            ) : (\n              <Metrics.RedText>\n                {data?.underReplicatedPartitions}\n              </Metrics.RedText>\n            )}\n          </Metrics.Indicator>\n          <Metrics.Indicator\n            label=\"In Sync Replicas\"\n            isAlert\n            alertType={\n              data?.inSyncReplicas === data?.replicas ? 'success' : 'error'\n            }\n          >\n            {data?.inSyncReplicas &&\n            data?.replicas &&\n            data?.inSyncReplicas < data?.replicas ? (\n              <Metrics.RedText>{data?.inSyncReplicas}</Metrics.RedText>\n            ) : (\n              data?.inSyncReplicas\n            )}\n            <Metrics.LightText> of {data?.replicas}</Metrics.LightText>\n          </Metrics.Indicator>\n          <Metrics.Indicator label=\"Type\">\n            <Tag color=\"gray\">{data?.internal ? 'Internal' : 'External'}</Tag>\n          </Metrics.Indicator>\n          <Metrics.Indicator label=\"Segment Size\" title=\"\">\n            <BytesFormatted value={data?.segmentSize} />\n          </Metrics.Indicator>\n          <Metrics.Indicator label=\"Segment Count\">\n            {data?.segmentCount}\n          </Metrics.Indicator>\n          <Metrics.Indicator label=\"Clean Up Policy\">\n            <Tag color=\"gray\">{data?.cleanUpPolicy || 'Unknown'}</Tag>\n          </Metrics.Indicator>\n          <Metrics.Indicator label=\"Message Count\">\n            {messageCount}\n          </Metrics.Indicator>\n        </Metrics.Section>\n      </Metrics.Wrapper>\n      <Table\n        columns={columns}\n        data={newData}\n        enableSorting\n        emptyMessage=\"No Partitions found \"\n      />\n    </>\n  );\n};\n\nexport default Overview;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Overview/__test__/Overview.spec.tsx",
    "content": "import React from 'react';\nimport { screen } from '@testing-library/react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport Overview from 'components/Topics/Topic/Overview/Overview';\nimport { theme } from 'theme/theme';\nimport { CleanUpPolicy, Topic } from 'generated-sources';\nimport ClusterContext from 'components/contexts/ClusterContext';\nimport userEvent from '@testing-library/user-event';\nimport { clusterTopicPath } from 'lib/paths';\nimport { Replica } from 'components/Topics/Topic/Overview/Overview.styled';\nimport { useClearTopicMessages, useTopicDetails } from 'lib/hooks/api/topics';\nimport {\n  externalTopicPayload,\n  internalTopicPayload,\n} from 'lib/fixtures/topics';\n\nconst clusterName = 'local';\nconst topicName = 'topic';\nconst defaultContextValues = {\n  isReadOnly: false,\n  hasKafkaConnectConfigured: true,\n  hasSchemaRegistryConfigured: true,\n  isTopicDeletionAllowed: true,\n};\n\njest.mock('lib/hooks/api/topics', () => ({\n  useTopicDetails: jest.fn(),\n  useClearTopicMessages: jest.fn(),\n}));\n\nconst clearTopicMessage = jest.fn();\n\ndescribe('Overview', () => {\n  const renderComponent = (\n    topic: Topic = externalTopicPayload,\n    context = defaultContextValues\n  ) => {\n    (useTopicDetails as jest.Mock).mockImplementation(() => ({\n      data: topic,\n    }));\n    (useClearTopicMessages as jest.Mock).mockImplementation(() => ({\n      mutateAsync: clearTopicMessage,\n    }));\n    const path = clusterTopicPath(clusterName, topicName);\n    return render(\n      <WithRoute path={clusterTopicPath()}>\n        <ClusterContext.Provider value={context}>\n          <Overview />\n        </ClusterContext.Provider>\n      </WithRoute>,\n      { initialEntries: [path] }\n    );\n  };\n\n  it('at least one replica was rendered', () => {\n    renderComponent();\n    expect(screen.getByLabelText('replica-info')).toBeInTheDocument();\n  });\n\n  it('renders replica cell with props', () => {\n    render(<Replica leader />);\n    const element = screen.getByLabelText('replica-info');\n    expect(element).toBeInTheDocument();\n    expect(element).toHaveStyleRule(\n      'color',\n      theme.topicMetaData.liderReplica.color\n    );\n  });\n\n  describe('when replicas out of sync', () => {\n    it('should be the appropriate color', () => {\n      render(<Replica outOfSync />);\n      const element = screen.getByLabelText('replica-info');\n      expect(element).toBeInTheDocument();\n      expect(element).toHaveStyleRule(\n        'color',\n        theme.topicMetaData.outOfSync.color\n      );\n      expect(element).toHaveStyleRule('font-weight', '500');\n    });\n  });\n\n  describe('when it has internal flag', () => {\n    it('renders the Action button for Topic', () => {\n      renderComponent({\n        ...externalTopicPayload,\n        cleanUpPolicy: CleanUpPolicy.DELETE,\n      });\n      expect(screen.getAllByLabelText('Dropdown Toggle').length).toEqual(1);\n    });\n\n    it('does not render Partitions', () => {\n      renderComponent({ ...externalTopicPayload, partitions: [] });\n      expect(screen.getByText('No Partitions found')).toBeInTheDocument();\n    });\n  });\n\n  describe('should render circular alert', () => {\n    it('should be in document', () => {\n      renderComponent();\n      const circles = screen.getAllByRole('circle');\n      expect(circles.length).toEqual(2);\n    });\n\n    it('should be the appropriate color', () => {\n      renderComponent({\n        ...externalTopicPayload,\n        underReplicatedPartitions: 0,\n        inSyncReplicas: 1,\n        replicas: 2,\n      });\n      const circles = screen.getAllByRole('circle');\n      expect(circles[0]).toHaveStyle(\n        `fill: ${theme.circularAlert.color.success}`\n      );\n      expect(circles[1]).toHaveStyle(\n        `fill: ${theme.circularAlert.color.error}`\n      );\n    });\n  });\n\n  describe('when Clear Messages is clicked', () => {\n    it('should when Clear Messages is clicked', async () => {\n      renderComponent({\n        ...externalTopicPayload,\n        cleanUpPolicy: CleanUpPolicy.DELETE,\n      });\n\n      const clearMessagesButton = screen.getByText('Clear Messages');\n      await userEvent.click(clearMessagesButton);\n      expect(clearTopicMessage).toHaveBeenCalledTimes(1);\n    });\n  });\n\n  describe('when the table partition dropdown appearance', () => {\n    it('should check if the dropdown is disabled when it is readOnly', () => {\n      renderComponent(\n        {\n          ...externalTopicPayload,\n        },\n        { ...defaultContextValues, isReadOnly: true }\n      );\n      expect(screen.getByLabelText('Dropdown Toggle')).toBeDisabled();\n    });\n\n    it('should check if the dropdown is disabled when it is internal', () => {\n      renderComponent({\n        ...internalTopicPayload,\n      });\n      expect(screen.getByLabelText('Dropdown Toggle')).toBeDisabled();\n    });\n\n    it('should check if the dropdown is disabled when cleanUpPolicy is not DELETE', () => {\n      renderComponent({\n        ...externalTopicPayload,\n        cleanUpPolicy: CleanUpPolicy.COMPACT,\n      });\n      expect(screen.getByLabelText('Dropdown Toggle')).toBeDisabled();\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.styled.tsx",
    "content": "import styled from 'styled-components';\n\nexport const Wrapper = styled.div`\n  display: block;\n  border-radius: 6px;\n`;\n\nexport const Columns = styled.div`\n  margin: -0.75rem;\n  margin-bottom: 0.75rem;\n  display: flex;\n  flex-direction: column;\n  padding: 0.75rem;\n  gap: 8px;\n\n  @media screen and (min-width: 769px) {\n    display: flex;\n  }\n`;\nexport const Flex = styled.div`\n  display: flex;\n  flex-direction: row;\n  gap: 8px;\n  @media screen and (max-width: 1200px) {\n    flex-direction: column;\n  }\n`;\nexport const FlexItem = styled.div`\n  width: 18rem;\n  @media screen and (max-width: 1450px) {\n    width: 50%;\n  }\n  @media screen and (max-width: 1200px) {\n    width: 100%;\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx",
    "content": "import React from 'react';\nimport { useForm, Controller } from 'react-hook-form';\nimport { RouteParamsClusterTopic } from 'lib/paths';\nimport { Button } from 'components/common/Button/Button';\nimport Editor from 'components/common/Editor/Editor';\nimport Select, { SelectOption } from 'components/common/Select/Select';\nimport Switch from 'components/common/Switch/Switch';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { showAlert } from 'lib/errorHandling';\nimport { useSendMessage, useTopicDetails } from 'lib/hooks/api/topics';\nimport { InputLabel } from 'components/common/Input/InputLabel.styled';\nimport { useSerdes } from 'lib/hooks/api/topicMessages';\nimport { SerdeUsage } from 'generated-sources';\n\nimport * as S from './SendMessage.styled';\nimport {\n  getDefaultValues,\n  getPartitionOptions,\n  getSerdeOptions,\n  validateBySchema,\n} from './utils';\n\ninterface FormType {\n  key: string;\n  content: string;\n  headers: string;\n  partition: number;\n  keySerde: string;\n  valueSerde: string;\n  keepContents: boolean;\n}\n\nconst SendMessage: React.FC<{ closeSidebar: () => void }> = ({\n  closeSidebar,\n}) => {\n  const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();\n  const { data: topic } = useTopicDetails({ clusterName, topicName });\n  const { data: serdes = {} } = useSerdes({\n    clusterName,\n    topicName,\n    use: SerdeUsage.SERIALIZE,\n  });\n  const sendMessage = useSendMessage({ clusterName, topicName });\n\n  const defaultValues = React.useMemo(() => getDefaultValues(serdes), [serdes]);\n  const partitionOptions: SelectOption[] = React.useMemo(\n    () => getPartitionOptions(topic?.partitions || []),\n    [topic]\n  );\n  const {\n    handleSubmit,\n    formState: { isSubmitting },\n    control,\n    setValue,\n  } = useForm<FormType>({\n    mode: 'onChange',\n    defaultValues: {\n      ...defaultValues,\n      partition: Number(partitionOptions[0].value),\n      keepContents: false,\n    },\n  });\n\n  const submit = async ({\n    keySerde,\n    valueSerde,\n    key,\n    content,\n    headers,\n    partition,\n    keepContents,\n  }: FormType) => {\n    let errors: string[] = [];\n\n    if (keySerde) {\n      const selectedKeySerde = serdes.key?.find((k) => k.name === keySerde);\n      errors = validateBySchema(key, selectedKeySerde?.schema, 'key');\n    }\n\n    if (valueSerde) {\n      const selectedValue = serdes.value?.find((v) => v.name === valueSerde);\n      errors = [\n        ...errors,\n        ...validateBySchema(content, selectedValue?.schema, 'content'),\n      ];\n    }\n\n    let parsedHeaders;\n    if (headers) {\n      try {\n        parsedHeaders = JSON.parse(headers);\n      } catch (error) {\n        errors.push('Wrong header format');\n      }\n    }\n\n    if (errors.length > 0) {\n      showAlert('error', {\n        id: `${clusterName}-${topicName}-createTopicMessageError`,\n        title: 'Validation Error',\n        message: (\n          <ul>\n            {errors.map((e) => (\n              <li key={e}>{e}</li>\n            ))}\n          </ul>\n        ),\n      });\n      return;\n    }\n    try {\n      await sendMessage.mutateAsync({\n        key: key || null,\n        content: content || null,\n        headers: parsedHeaders,\n        partition: partition || 0,\n        keySerde,\n        valueSerde,\n      });\n      if (!keepContents) {\n        setValue('key', defaultValues.key || '');\n        setValue('content', defaultValues.content || '');\n        closeSidebar();\n      }\n    } catch (e) {\n      // do nothing\n    }\n  };\n\n  return (\n    <S.Wrapper>\n      <form onSubmit={handleSubmit(submit)}>\n        <S.Columns>\n          <S.FlexItem>\n            <InputLabel>Partition</InputLabel>\n            <Controller\n              control={control}\n              name=\"partition\"\n              render={({ field: { name, onChange, value } }) => (\n                <Select\n                  id=\"selectPartitionOptions\"\n                  aria-labelledby=\"selectPartitionOptions\"\n                  name={name}\n                  onChange={onChange}\n                  minWidth=\"100%\"\n                  options={partitionOptions}\n                  value={value}\n                />\n              )}\n            />\n          </S.FlexItem>\n          <S.Flex>\n            <S.FlexItem>\n              <InputLabel>Key Serde</InputLabel>\n              <Controller\n                control={control}\n                name=\"keySerde\"\n                render={({ field: { name, onChange, value } }) => (\n                  <Select\n                    id=\"selectKeySerdeOptions\"\n                    aria-labelledby=\"selectKeySerdeOptions\"\n                    name={name}\n                    onChange={onChange}\n                    minWidth=\"100%\"\n                    options={getSerdeOptions(serdes.key || [])}\n                    value={value}\n                  />\n                )}\n              />\n            </S.FlexItem>\n            <S.FlexItem>\n              <InputLabel>Value Serde</InputLabel>\n              <Controller\n                control={control}\n                name=\"valueSerde\"\n                render={({ field: { name, onChange, value } }) => (\n                  <Select\n                    id=\"selectValueSerdeOptions\"\n                    aria-labelledby=\"selectValueSerdeOptions\"\n                    name={name}\n                    onChange={onChange}\n                    minWidth=\"100%\"\n                    options={getSerdeOptions(serdes.value || [])}\n                    value={value}\n                  />\n                )}\n              />\n            </S.FlexItem>\n          </S.Flex>\n          <div>\n            <Controller\n              control={control}\n              name=\"keepContents\"\n              render={({ field: { name, onChange, value } }) => (\n                <Switch name={name} onChange={onChange} checked={value} />\n              )}\n            />\n            <InputLabel>Keep contents</InputLabel>\n          </div>\n        </S.Columns>\n        <S.Columns>\n          <div>\n            <InputLabel>Key</InputLabel>\n            <Controller\n              control={control}\n              name=\"key\"\n              render={({ field: { name, onChange, value } }) => (\n                <Editor\n                  readOnly={isSubmitting}\n                  name={name}\n                  onChange={onChange}\n                  value={value}\n                  height=\"40px\"\n                />\n              )}\n            />\n          </div>\n          <div>\n            <InputLabel>Value</InputLabel>\n            <Controller\n              control={control}\n              name=\"content\"\n              render={({ field: { name, onChange, value } }) => (\n                <Editor\n                  readOnly={isSubmitting}\n                  name={name}\n                  onChange={onChange}\n                  value={value}\n                  height=\"280px\"\n                />\n              )}\n            />\n          </div>\n        </S.Columns>\n        <S.Columns>\n          <div>\n            <InputLabel>Headers</InputLabel>\n            <Controller\n              control={control}\n              name=\"headers\"\n              render={({ field: { name, onChange } }) => (\n                <Editor\n                  readOnly={isSubmitting}\n                  defaultValue=\"{}\"\n                  name={name}\n                  onChange={onChange}\n                  height=\"40px\"\n                />\n              )}\n            />\n          </div>\n        </S.Columns>\n        <Button\n          buttonSize=\"M\"\n          buttonType=\"primary\"\n          type=\"submit\"\n          disabled={isSubmitting}\n        >\n          Produce Message\n        </Button>\n      </form>\n    </S.Wrapper>\n  );\n};\n\nexport default SendMessage;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/SendMessage.spec.tsx",
    "content": "import React from 'react';\nimport SendMessage from 'components/Topics/Topic/SendMessage/SendMessage';\nimport { screen, waitFor } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { clusterTopicPath } from 'lib/paths';\nimport { validateBySchema } from 'components/Topics/Topic/SendMessage/utils';\nimport { externalTopicPayload } from 'lib/fixtures/topics';\nimport { useSendMessage, useTopicDetails } from 'lib/hooks/api/topics';\nimport { useSerdes } from 'lib/hooks/api/topicMessages';\nimport { serdesPayload } from 'lib/fixtures/topicMessages';\n\nimport Mock = jest.Mock;\n\njest.mock('json-schema-faker', () => ({\n  generate: () => ({\n    f1: -93251214,\n    schema: 'enim sit in fugiat dolor',\n    f2: 'deserunt culpa sunt',\n  }),\n  option: jest.fn(),\n}));\n\njest.mock('components/Topics/Topic/SendMessage/utils', () => ({\n  ...jest.requireActual('components/Topics/Topic/SendMessage/utils'),\n  validateBySchema: jest.fn(),\n}));\n\njest.mock('lib/errorHandling', () => ({\n  ...jest.requireActual('lib/errorHandling'),\n  showServerError: jest.fn(),\n}));\n\njest.mock('lib/hooks/api/topics', () => ({\n  useTopicDetails: jest.fn(),\n  useSendMessage: jest.fn(),\n}));\n\njest.mock('lib/hooks/api/topicMessages', () => ({\n  useSerdes: jest.fn(),\n}));\n\nconst clusterName = 'testCluster';\nconst topicName = externalTopicPayload.name;\n\nconst mockOnSubmit = jest.fn();\n\nconst renderComponent = async () => {\n  const path = clusterTopicPath(clusterName, topicName);\n  await render(\n    <WithRoute path={clusterTopicPath()}>\n      <SendMessage closeSidebar={mockOnSubmit} />\n    </WithRoute>,\n    { initialEntries: [path] }\n  );\n};\n\nconst renderAndSubmitData = async (error: string[] = []) => {\n  await renderComponent();\n  await userEvent.click(screen.getAllByRole('listbox')[0]);\n\n  await userEvent.click(screen.getAllByRole('option')[1]);\n\n  (validateBySchema as Mock).mockImplementation(() => error);\n  const submitButton = screen.getByRole('button', {\n    name: 'Produce Message',\n  });\n  await waitFor(() => expect(submitButton).toBeEnabled());\n  await userEvent.click(submitButton);\n};\n\ndescribe('SendMessage', () => {\n  beforeEach(() => {\n    (useTopicDetails as jest.Mock).mockImplementation(() => ({\n      data: externalTopicPayload,\n    }));\n    (useSerdes as jest.Mock).mockImplementation(() => ({\n      data: serdesPayload,\n    }));\n  });\n\n  describe('when schema is fetched', () => {\n    it('calls sendTopicMessage on submit', async () => {\n      const sendTopicMessageMock = jest.fn();\n      (useSendMessage as jest.Mock).mockImplementation(() => ({\n        mutateAsync: sendTopicMessageMock,\n      }));\n      await renderAndSubmitData();\n      expect(sendTopicMessageMock).toHaveBeenCalledTimes(1);\n      expect(mockOnSubmit).toHaveBeenCalledTimes(1);\n    });\n\n    it('should check and view validation error message when is not valid', async () => {\n      const sendTopicMessageMock = jest.fn();\n      (useSendMessage as jest.Mock).mockImplementation(() => ({\n        mutateAsync: sendTopicMessageMock,\n      }));\n      await renderAndSubmitData(['error']);\n      expect(sendTopicMessageMock).not.toHaveBeenCalled();\n      expect(mockOnSubmit).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('when schema is empty', () => {\n    it('renders if schema is not defined', async () => {\n      await renderComponent();\n      expect(screen.getAllByRole('textbox')[0].nodeValue).toBeNull();\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/utils.spec.ts",
    "content": "import { serdesPayload } from 'lib/fixtures/topicMessages';\nimport {\n  getDefaultValues,\n  getSerdeOptions,\n  validateBySchema,\n} from 'components/Topics/Topic/SendMessage/utils';\nimport { SerdeDescription } from 'generated-sources';\n\ndescribe('SendMessage utils', () => {\n  describe('getDefaultValues', () => {\n    it('should return default values', () => {\n      const actual = getDefaultValues(serdesPayload);\n      expect(actual.keySerde).toEqual(\n        serdesPayload.key?.find((item) => item.preferred)?.name\n      );\n      expect(actual.key).not.toBeUndefined();\n      expect(actual.valueSerde).toEqual(\n        serdesPayload.value?.find((item) => item.preferred)?.name\n      );\n      expect(actual.content).not.toBeUndefined();\n    });\n    it('works even with empty serdes', () => {\n      const actual = getDefaultValues({});\n      expect(actual.keySerde).toBeUndefined();\n      expect(actual.key).toBeUndefined();\n      expect(actual.valueSerde).toBeUndefined();\n      expect(actual.content).toBeUndefined();\n    });\n  });\n  describe('getSerdeOptions', () => {\n    it('should return options', () => {\n      const options = getSerdeOptions(serdesPayload.key as SerdeDescription[]);\n      expect(options).toHaveLength(2);\n    });\n    it('should skip options without label', () => {\n      const keySerdes = serdesPayload.key as SerdeDescription[];\n      const payload = [{ ...keySerdes[0], name: undefined }, keySerdes[1]];\n      const options = getSerdeOptions(payload);\n      expect(options).toHaveLength(1);\n    });\n  });\n  describe('validateBySchema', () => {\n    const defaultSchema = '{\"type\": \"integer\", \"minimum\" : 1, \"maximum\" : 2 }';\n\n    it('should return empty error data if value is empty', () => {\n      expect(validateBySchema('', defaultSchema, 'key')).toHaveLength(0);\n    });\n\n    it('should return empty error data if schema is empty', () => {\n      expect(validateBySchema('My Value', '', 'key')).toHaveLength(0);\n    });\n\n    it('should return parsing error data if schema is not parsed with type of key', () => {\n      const schema = '{invalid';\n      expect(validateBySchema('My Value', schema, 'key')).toEqual([\n        `Error in parsing the \"key\" field schema`,\n      ]);\n    });\n    it('should return parsing error data if schema is not parsed with type of key', () => {\n      const schema = '{invalid';\n      expect(validateBySchema('My Value', schema, 'content')).toEqual([\n        `Error in parsing the \"content\" field schema`,\n      ]);\n    });\n    it('should return empty error data if schema type is string', () => {\n      const schema = `{\"type\": \"string\"}`;\n      expect(validateBySchema('My Value', schema, 'key')).toHaveLength(0);\n    });\n    it('returns errors on invalid input data', () => {\n      expect(validateBySchema('0', defaultSchema, 'key')).toEqual([\n        'Key/minimum - must be >= 1',\n      ]);\n    });\n    it('returns error on broken key value', () => {\n      expect(validateBySchema('{120', defaultSchema, 'key')).toEqual([\n        'Error in parsing the \"key\" field value',\n      ]);\n    });\n    it('returns error on broken content value', () => {\n      expect(validateBySchema('{120', defaultSchema, 'content')).toEqual([\n        'Error in parsing the \"content\" field value',\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/SendMessage/utils.ts",
    "content": "import {\n  Partition,\n  SerdeDescription,\n  TopicSerdeSuggestion,\n} from 'generated-sources';\nimport jsf from 'json-schema-faker';\nimport { compact } from 'lodash';\nimport Ajv, { DefinedError } from 'ajv/dist/2020';\nimport addFormats from 'ajv-formats';\nimport upperFirst from 'lodash/upperFirst';\n\njsf.option('fillProperties', false);\njsf.option('alwaysFakeOptionals', true);\njsf.option('failOnInvalidFormat', false);\n\nconst generateValueFromSchema = (preferred?: SerdeDescription) => {\n  if (!preferred?.schema) {\n    return undefined;\n  }\n  const parsedSchema = JSON.parse(preferred.schema);\n  const value = jsf.generate(parsedSchema);\n  return JSON.stringify(value);\n};\n\nexport const getPreferredDescription = (serdes: SerdeDescription[]) =>\n  serdes.find((s) => s.preferred);\n\nexport const getDefaultValues = (serdes: TopicSerdeSuggestion) => {\n  const keySerde = getPreferredDescription(serdes.key || []);\n  const valueSerde = getPreferredDescription(serdes.value || []);\n\n  return {\n    key: generateValueFromSchema(keySerde),\n    content: generateValueFromSchema(valueSerde),\n    headers: undefined,\n    partition: undefined,\n    keySerde: keySerde?.name,\n    valueSerde: valueSerde?.name,\n  };\n};\n\nexport const getPartitionOptions = (partitions: Partition[]) =>\n  partitions.map(({ partition }) => ({\n    label: `Partition #${partition}`,\n    value: partition,\n  }));\n\nexport const getSerdeOptions = (items: SerdeDescription[]) => {\n  const options = items.map(({ name }) => {\n    if (!name) return undefined;\n    return { label: name, value: name };\n  });\n\n  return compact(options);\n};\n\nexport const validateBySchema = (\n  value: string,\n  schema: string | undefined,\n  type: 'key' | 'content'\n) => {\n  let errors: string[] = [];\n\n  if (!value || !schema) {\n    return errors;\n  }\n\n  let parsedSchema;\n  let parsedValue;\n\n  try {\n    parsedSchema = JSON.parse(schema);\n  } catch (e) {\n    return [`Error in parsing the \"${type}\" field schema`];\n  }\n  if (parsedSchema.type === 'string') {\n    return [];\n  }\n  try {\n    parsedValue = JSON.parse(value);\n  } catch (e) {\n    return [`Error in parsing the \"${type}\" field value`];\n  }\n  try {\n    const ajv = new Ajv();\n    addFormats(ajv);\n    const validate = ajv.compile(parsedSchema);\n    validate(parsedValue);\n    if (validate.errors) {\n      errors = validate.errors.map(\n        ({ schemaPath, message }) =>\n          `${schemaPath.replace('#', upperFirst(type))} - ${message}`\n      );\n    }\n  } catch (e) {\n    const err = e as DefinedError;\n    return [`${upperFirst(type)} ${err.message}`];\n  }\n\n  return errors;\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Settings/Settings.styled.ts",
    "content": "import styled, { css } from 'styled-components';\n\nexport const Value = styled.span<{ $hasCustomValue?: boolean }>(\n  ({ $hasCustomValue }) => css`\n    font-weight: ${$hasCustomValue ? 500 : 400};\n  `\n);\n\nexport const DefaultValue = styled.span(\n  ({ theme }) => css`\n    color: ${theme.configList.color};\n    font-weight: 400;\n  `\n);\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Settings/Settings.tsx",
    "content": "import React from 'react';\nimport Table from 'components/common/NewTable';\nimport { RouteParamsClusterTopic } from 'lib/paths';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { useTopicConfig } from 'lib/hooks/api/topics';\nimport { CellContext, ColumnDef } from '@tanstack/react-table';\nimport { TopicConfig } from 'generated-sources';\n\nimport * as S from './Settings.styled';\n\nconst ValueCell: React.FC<CellContext<TopicConfig, unknown>> = ({\n  row,\n  renderValue,\n}) => {\n  const { defaultValue } = row.original;\n  const { value } = row.original;\n  const hasCustomValue = !!defaultValue && value !== defaultValue;\n\n  return (\n    <S.Value $hasCustomValue={hasCustomValue}>{renderValue<string>()}</S.Value>\n  );\n};\n\nconst DefaultValueCell: React.FC<CellContext<TopicConfig, unknown>> = ({\n  row,\n  getValue,\n}) => {\n  const defaultValue = getValue<TopicConfig['defaultValue']>();\n  const { value } = row.original;\n  const hasCustomValue = !!defaultValue && value !== defaultValue;\n  return <S.DefaultValue>{hasCustomValue && defaultValue}</S.DefaultValue>;\n};\n\nconst Settings: React.FC = () => {\n  const props = useAppParams<RouteParamsClusterTopic>();\n  const { data = [] } = useTopicConfig(props);\n\n  const columns = React.useMemo<ColumnDef<TopicConfig>[]>(\n    () => [\n      {\n        header: 'Key',\n        accessorKey: 'name',\n        cell: ValueCell,\n      },\n      {\n        header: 'Value',\n        accessorKey: 'value',\n        cell: ValueCell,\n      },\n      {\n        header: 'Default Value',\n        accessorKey: 'defaultValue',\n        cell: DefaultValueCell,\n      },\n    ],\n    []\n  );\n\n  return <Table columns={columns} data={data} />;\n};\n\nexport default Settings;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Settings/__test__/Settings.spec.tsx",
    "content": "import React from 'react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport { screen } from '@testing-library/react';\nimport Settings from 'components/Topics/Topic/Settings/Settings';\nimport { clusterTopicSettingsPath } from 'lib/paths';\nimport { topicConfigPayload } from 'lib/fixtures/topics';\nimport { useTopicConfig } from 'lib/hooks/api/topics';\n\nconst clusterName = 'Cluster_Name';\nconst topicName = 'Topic_Name';\n\njest.mock('lib/hooks/api/topics', () => ({\n  useTopicConfig: jest.fn(),\n}));\n\nconst getName = () => screen.getByText('compression.type');\nconst getValue = () => screen.getByText('producer');\n\ndescribe('Settings', () => {\n  const renderComponent = () => {\n    const path = clusterTopicSettingsPath(clusterName, topicName);\n    return render(\n      <WithRoute path={clusterTopicSettingsPath()}>\n        <Settings />\n      </WithRoute>,\n      { initialEntries: [path] }\n    );\n  };\n\n  beforeEach(() => {\n    (useTopicConfig as jest.Mock).mockImplementation(() => ({\n      data: topicConfigPayload,\n    }));\n    renderComponent();\n  });\n\n  it('renders without CustomValue', () => {\n    expect(getName()).toBeInTheDocument();\n    expect(getName()).toHaveStyle('font-weight: 400');\n    expect(getValue()).toBeInTheDocument();\n    expect(getValue()).toHaveStyle('font-weight: 400');\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Statistics/Indicators/SizeStats.tsx",
    "content": "import React from 'react';\nimport * as Metrics from 'components/common/Metrics';\nimport { TopicAnalysisSizeStats } from 'generated-sources';\nimport BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';\n\nconst SizeStats: React.FC<{\n  stats: TopicAnalysisSizeStats;\n  title: string;\n}> = ({\n  stats: { sum, min, max, avg, prctl50, prctl75, prctl95, prctl99, prctl999 },\n  title,\n}) => (\n  <Metrics.Section title={title}>\n    <Metrics.Indicator label=\"Total size\">\n      <BytesFormatted value={sum} />\n    </Metrics.Indicator>\n    <Metrics.Indicator label=\"Min size\">\n      <BytesFormatted value={min} />\n    </Metrics.Indicator>\n    <Metrics.Indicator label=\"Max size\">\n      <BytesFormatted value={max} />\n    </Metrics.Indicator>\n    <Metrics.Indicator label=\"Avg key\">\n      <BytesFormatted value={avg} />\n    </Metrics.Indicator>\n    <Metrics.Indicator label=\"Percentile 50\">\n      <BytesFormatted value={prctl50} />\n    </Metrics.Indicator>\n    <Metrics.Indicator label=\"Percentile 75\">\n      <BytesFormatted value={prctl75} />\n    </Metrics.Indicator>\n    <Metrics.Indicator label=\"Percentile 95\">\n      <BytesFormatted value={prctl95} />\n    </Metrics.Indicator>\n    <Metrics.Indicator label=\"Percentile 99\">\n      <BytesFormatted value={prctl99} />\n    </Metrics.Indicator>\n    <Metrics.Indicator label=\"Percentile 999\">\n      <BytesFormatted value={prctl999} />\n    </Metrics.Indicator>\n  </Metrics.Section>\n);\n\nexport default SizeStats;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Statistics/Indicators/Total.tsx",
    "content": "import React from 'react';\nimport * as Metrics from 'components/common/Metrics';\nimport { TopicAnalysisStats } from 'generated-sources';\nimport { formatTimestamp } from 'lib/dateTimeHelpers';\n\nconst Total: React.FC<TopicAnalysisStats> = ({\n  totalMsgs,\n  minOffset,\n  maxOffset,\n  minTimestamp,\n  maxTimestamp,\n  nullKeys,\n  nullValues,\n  approxUniqKeys,\n  approxUniqValues,\n}) => {\n  return (\n    <Metrics.Section title=\"Messages\">\n      <Metrics.Indicator label=\"Total number\">{totalMsgs}</Metrics.Indicator>\n      <Metrics.Indicator label=\"Offsets min-max\">\n        {`${minOffset} - ${maxOffset}`}\n      </Metrics.Indicator>\n      <Metrics.Indicator label=\"Timestamp min-max\">\n        {`${formatTimestamp(minTimestamp)} - ${formatTimestamp(maxTimestamp)}`}\n      </Metrics.Indicator>\n      <Metrics.Indicator label=\"Null keys\">{nullKeys}</Metrics.Indicator>\n      <Metrics.Indicator\n        label=\"Unique keys\"\n        title=\"Approximate number of unique keys\"\n      >\n        {approxUniqKeys}\n      </Metrics.Indicator>\n      <Metrics.Indicator label=\"Null values\">{nullValues}</Metrics.Indicator>\n      <Metrics.Indicator\n        label=\"Unique values\"\n        title=\"Approximate number of unique values\"\n      >\n        {approxUniqValues}\n      </Metrics.Indicator>\n    </Metrics.Section>\n  );\n};\n\nexport default Total;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Statistics/Metrics.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport {\n  useAnalyzeTopic,\n  useCancelTopicAnalysis,\n  useTopicAnalysis,\n} from 'lib/hooks/api/topics';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { RouteParamsClusterTopic } from 'lib/paths';\nimport * as Informers from 'components/common/Metrics';\nimport ProgressBar from 'components/common/ProgressBar/ProgressBar';\nimport {\n  List,\n  Label,\n} from 'components/common/PropertiesList/PropertiesList.styled';\nimport BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';\nimport { calculateTimer, formatTimestamp } from 'lib/dateTimeHelpers';\nimport { Action, ResourceType } from 'generated-sources';\nimport { ActionButton } from 'components/common/ActionComponent';\n\nimport * as S from './Statistics.styles';\nimport Total from './Indicators/Total';\nimport SizeStats from './Indicators/SizeStats';\nimport PartitionTable from './PartitionTable';\n\nconst Metrics: React.FC = () => {\n  const params = useAppParams<RouteParamsClusterTopic>();\n\n  const [isAnalyzing, setIsAnalyzing] = useState(true);\n  const analyzeTopic = useAnalyzeTopic(params);\n  const cancelTopicAnalysis = useCancelTopicAnalysis(params);\n\n  const { data } = useTopicAnalysis(params, isAnalyzing);\n\n  useEffect(() => {\n    if (data && !data.progress) {\n      setIsAnalyzing(false);\n    }\n  }, [data]);\n\n  if (!data) {\n    return null;\n  }\n\n  if (data.progress) {\n    return (\n      <S.ProgressContainer>\n        <S.ProgressPct>\n          {Math.floor(data.progress.completenessPercent || 0)}%\n        </S.ProgressPct>\n        <S.ProgressBarWrapper>\n          <ProgressBar completed={data.progress.completenessPercent || 0} />\n        </S.ProgressBarWrapper>\n        <ActionButton\n          onClick={async () => {\n            await cancelTopicAnalysis.mutateAsync();\n            setIsAnalyzing(true);\n          }}\n          buttonType=\"secondary\"\n          buttonSize=\"M\"\n          permission={{\n            resource: ResourceType.TOPIC,\n            action: Action.MESSAGES_READ,\n            value: params.topicName,\n          }}\n        >\n          Stop Analysis\n        </ActionButton>\n        <List>\n          <Label>Started at</Label>\n          <span>\n            {formatTimestamp(data.progress.startedAt, {\n              hour: 'numeric',\n              minute: 'numeric',\n              second: 'numeric',\n            })}\n          </span>\n          <Label>Passed since start</Label>\n          <span>{calculateTimer(data.progress.startedAt as number)}</span>\n          <Label>Scanned messages</Label>\n          <span>{data.progress.msgsScanned}</span>\n          <Label>Scanned size</Label>\n          <span>\n            <BytesFormatted value={data.progress.bytesScanned} />\n          </span>\n        </List>\n      </S.ProgressContainer>\n    );\n  }\n\n  if (!data.result) {\n    return null;\n  }\n\n  const totalStats = data.result.totalStats || {};\n  const partitionStats = data.result.partitionStats || [];\n\n  return (\n    <>\n      <S.ActionsBar>\n        <S.CreatedAt>{formatTimestamp(data?.result?.finishedAt)}</S.CreatedAt>\n        <ActionButton\n          onClick={async () => {\n            await analyzeTopic.mutateAsync();\n            setIsAnalyzing(true);\n          }}\n          buttonType=\"primary\"\n          buttonSize=\"S\"\n          permission={{\n            resource: ResourceType.TOPIC,\n            action: Action.MESSAGES_READ,\n            value: params.topicName,\n          }}\n        >\n          Restart Analysis\n        </ActionButton>\n      </S.ActionsBar>\n      <Informers.Wrapper>\n        <Total {...totalStats} />\n        {totalStats.keySize && (\n          <SizeStats stats={totalStats.keySize} title=\"Key size\" />\n        )}\n        {totalStats.valueSize && (\n          <SizeStats stats={totalStats.valueSize} title=\"Value size\" />\n        )}\n      </Informers.Wrapper>\n      <PartitionTable data={partitionStats} />\n    </>\n  );\n};\n\nexport default Metrics;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Statistics/PartitionInfoRow.tsx",
    "content": "import React from 'react';\nimport { Row } from '@tanstack/react-table';\nimport Heading from 'components/common/heading/Heading.styled';\nimport BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';\nimport {\n  List,\n  Label,\n} from 'components/common/PropertiesList/PropertiesList.styled';\nimport { TopicAnalysisStats } from 'generated-sources';\nimport { formatTimestamp } from 'lib/dateTimeHelpers';\n\nimport * as S from './Statistics.styles';\n\nconst PartitionInfoRow: React.FC<{ row: Row<TopicAnalysisStats> }> = ({\n  row,\n}) => {\n  const {\n    totalMsgs,\n    minTimestamp,\n    maxTimestamp,\n    nullKeys,\n    nullValues,\n    approxUniqKeys,\n    approxUniqValues,\n    keySize,\n    valueSize,\n  } = row.original;\n  return (\n    <S.PartitionInfo>\n      <div>\n        <Heading level={4}>Partition stats</Heading>\n        <List>\n          <Label>Total message</Label>\n          <span>{totalMsgs}</span>\n          <Label>Total size</Label>\n          <BytesFormatted value={(keySize?.sum || 0) + (valueSize?.sum || 0)} />\n          <Label>Min. timestamp</Label>\n          <span>{formatTimestamp(minTimestamp)}</span>\n          <Label>Max. timestamp</Label>\n          <span>{formatTimestamp(maxTimestamp)}</span>\n          <Label>Null keys amount</Label>\n          <span>{nullKeys}</span>\n          <Label>Null values amount</Label>\n          <span>{nullValues}</span>\n          <Label>Approx. unique keys amount</Label>\n          <span>{approxUniqKeys}</span>\n          <Label>Approx. unique values amount</Label>\n          <span>{approxUniqValues}</span>\n        </List>\n      </div>\n      <div>\n        <Heading level={4}>Keys sizes</Heading>\n        <List>\n          <Label>Total keys size</Label>\n          <BytesFormatted value={keySize?.sum} />\n          <Label>Min key size</Label>\n          <BytesFormatted value={keySize?.min} />\n          <Label>Max key size</Label>\n          <BytesFormatted value={keySize?.max} />\n          <Label>Avg key size</Label>\n          <BytesFormatted value={keySize?.avg} />\n          <Label>Percentile 50</Label>\n          <BytesFormatted value={keySize?.prctl50} />\n          <Label>Percentile 75</Label>\n          <BytesFormatted value={keySize?.prctl75} />\n          <Label>Percentile 95</Label>\n          <BytesFormatted value={keySize?.prctl95} />\n          <Label>Percentile 99</Label>\n          <BytesFormatted value={keySize?.prctl99} />\n          <Label>Percentile 999</Label>\n          <BytesFormatted value={keySize?.prctl999} />\n        </List>\n      </div>\n      <div>\n        <Heading level={4}>Values sizes</Heading>\n        <List>\n          <Label>Total keys size</Label>\n          <BytesFormatted value={valueSize?.sum} />\n          <Label>Min key size</Label>\n          <BytesFormatted value={valueSize?.min} />\n          <Label>Max key size</Label>\n          <BytesFormatted value={valueSize?.max} />\n          <Label>Avg key size</Label>\n          <BytesFormatted value={valueSize?.avg} />\n          <Label>Percentile 50</Label>\n          <BytesFormatted value={valueSize?.prctl50} />\n          <Label>Percentile 75</Label>\n          <BytesFormatted value={valueSize?.prctl75} />\n          <Label>Percentile 95</Label>\n          <BytesFormatted value={valueSize?.prctl95} />\n          <Label>Percentile 99</Label>\n          <BytesFormatted value={valueSize?.prctl99} />\n          <Label>Percentile 999</Label>\n          <BytesFormatted value={valueSize?.prctl999} />\n        </List>\n      </div>\n    </S.PartitionInfo>\n  );\n};\n\nexport default PartitionInfoRow;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Statistics/PartitionTable.tsx",
    "content": "import React from 'react';\nimport { TopicAnalysisStats } from 'generated-sources';\nimport { ColumnDef } from '@tanstack/react-table';\nimport Table from 'components/common/NewTable';\n\nimport PartitionInfoRow from './PartitionInfoRow';\n\nconst PartitionTable: React.FC<{ data: TopicAnalysisStats[] }> = ({ data }) => {\n  const columns = React.useMemo<ColumnDef<TopicAnalysisStats>[]>(\n    () => [\n      {\n        header: 'Partition ID',\n        accessorKey: 'partition',\n      },\n      {\n        header: 'Total Messages',\n        accessorKey: 'totalMsgs',\n      },\n      {\n        header: 'Min Offset',\n        accessorKey: 'minOffset',\n      },\n      { header: 'Max Offset', accessorKey: 'maxOffset' },\n    ],\n    []\n  );\n\n  return (\n    <Table\n      data={data}\n      columns={columns}\n      getRowCanExpand={() => true}\n      renderSubComponent={PartitionInfoRow}\n      enableSorting\n    />\n  );\n};\n\nexport default PartitionTable;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Statistics/Statistics.styles.ts",
    "content": "import { List } from 'components/common/PropertiesList/PropertiesList.styled';\nimport styled from 'styled-components';\n\nexport const ProgressContainer = styled.div`\n  padding: 1.5rem 1rem;\n  background: ${({ theme }) => theme.code.backgroundColor};\n  justify-content: center;\n  align-items: center;\n  display: flex;\n  flex-direction: column;\n  height: 300px;\n  text-align: center;\n\n  ${List} {\n    opacity: 0.5;\n  }\n`;\n\nexport const ActionsBar = styled.div`\n  display: flex;\n  justify-content: end;\n  gap: 8px;\n  padding: 10px 20px;\n  align-items: center;\n`;\n\nexport const CreatedAt = styled.div`\n  font-size: 12px;\n  line-height: 1.5;\n  color: ${({ theme }) => theme.statictics.createdAtColor};\n`;\n\nexport const PartitionInfo = styled.div`\n  display: grid;\n  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));\n  column-gap: 24px;\n`;\n\nexport const ProgressBarWrapper = styled.div`\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  width: 280px;\n`;\n\nexport const ProgressPct = styled.span`\n  font-size: 15px;\n  font-weight: bold;\n  line-height: 1.5;\n  color: ${({ theme }) => theme.statictics.progressPctColor};\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Statistics/Statistics.tsx",
    "content": "/* eslint-disable react/no-unstable-nested-components */\nimport React from 'react';\nimport { useAnalyzeTopic } from 'lib/hooks/api/topics';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { RouteParamsClusterTopic } from 'lib/paths';\nimport { QueryErrorResetBoundary } from '@tanstack/react-query';\nimport { ErrorBoundary } from 'react-error-boundary';\nimport { Action, ResourceType } from 'generated-sources';\nimport { ActionButton } from 'components/common/ActionComponent';\n\nimport * as S from './Statistics.styles';\nimport Metrics from './Metrics';\n\nconst Statistics: React.FC = () => {\n  const params = useAppParams<RouteParamsClusterTopic>();\n  const analyzeTopic = useAnalyzeTopic(params);\n\n  return (\n    <QueryErrorResetBoundary>\n      {({ reset }) => (\n        <ErrorBoundary\n          onReset={reset}\n          fallbackRender={({ resetErrorBoundary }) => (\n            <S.ProgressContainer>\n              <ActionButton\n                onClick={async () => {\n                  await analyzeTopic.mutateAsync();\n                  resetErrorBoundary();\n                }}\n                buttonType=\"primary\"\n                buttonSize=\"M\"\n                permission={{\n                  resource: ResourceType.TOPIC,\n                  action: Action.MESSAGES_READ,\n                  value: params.topicName,\n                }}\n              >\n                Start Analysis\n              </ActionButton>\n            </S.ProgressContainer>\n          )}\n        >\n          <Metrics />\n        </ErrorBoundary>\n      )}\n    </QueryErrorResetBoundary>\n  );\n};\n\nexport default Statistics;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Statistics/__test__/Metrics.spec.tsx",
    "content": "import React from 'react';\nimport { screen, waitFor } from '@testing-library/react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport Statistics from 'components/Topics/Topic/Statistics/Statistics';\nimport { clusterTopicStatisticsPath } from 'lib/paths';\nimport {\n  useTopicAnalysis,\n  useCancelTopicAnalysis,\n  useAnalyzeTopic,\n} from 'lib/hooks/api/topics';\nimport { topicStatsPayload } from 'lib/fixtures/topics';\nimport userEvent from '@testing-library/user-event';\n\nconst clusterName = 'local';\nconst topicName = 'topic';\n\njest.mock('lib/hooks/api/topics', () => ({\n  ...jest.requireActual('lib/hooks/api/topics'),\n  useTopicAnalysis: jest.fn(),\n  useCancelTopicAnalysis: jest.fn(),\n  useAnalyzeTopic: jest.fn(),\n}));\n\ndescribe('Metrics', () => {\n  const renderComponent = () => {\n    const path = clusterTopicStatisticsPath(clusterName, topicName);\n    return render(\n      <WithRoute path={clusterTopicStatisticsPath()}>\n        <Statistics />\n      </WithRoute>,\n      { initialEntries: [path] }\n    );\n  };\n\n  describe('when analysis is in progress', () => {\n    const cancelMock = jest.fn();\n\n    beforeEach(() => {\n      (useCancelTopicAnalysis as jest.Mock).mockImplementation(() => ({\n        mutateAsync: cancelMock,\n      }));\n      (useTopicAnalysis as jest.Mock).mockImplementation(() => ({\n        data: {\n          progress: {\n            ...topicStatsPayload.progress,\n            completenessPercent: undefined,\n          },\n          result: undefined,\n        },\n      }));\n      renderComponent();\n    });\n\n    it('renders Stop Analysis button', async () => {\n      const btn = screen.getByRole('button', { name: 'Stop Analysis' });\n      expect(btn).toBeInTheDocument();\n      await userEvent.click(btn);\n      expect(cancelMock).toHaveBeenCalled();\n    });\n\n    it('renders Progress bar', () => {\n      const progressbar = screen.getByRole('progressbar');\n      expect(progressbar).toBeInTheDocument();\n      expect(progressbar).toHaveStyleRule('width', '0%');\n    });\n\n    it('calculate Timer ', () => {\n      expect(screen.getByText('Passed since start')).toBeInTheDocument();\n    });\n  });\n\n  describe('when analysis is completed', () => {\n    const restartMock = jest.fn();\n    beforeEach(() => {\n      (useTopicAnalysis as jest.Mock).mockImplementation(() => ({\n        data: { ...topicStatsPayload, progress: undefined },\n      }));\n      (useAnalyzeTopic as jest.Mock).mockImplementation(() => ({\n        mutateAsync: restartMock,\n      }));\n      renderComponent();\n    });\n    it('renders metrics', async () => {\n      const btn = screen.getByRole('button', { name: 'Restart Analysis' });\n      expect(btn).toBeInTheDocument();\n      expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();\n      expect(screen.getAllByRole('group').length).toEqual(3);\n      expect(screen.getByRole('table')).toBeInTheDocument();\n    });\n    it('renders restarts analisis', async () => {\n      const btn = screen.getByRole('button', { name: 'Restart Analysis' });\n      await waitFor(() => userEvent.click(btn));\n      expect(restartMock).toHaveBeenCalled();\n    });\n    it('renders expandable table', async () => {\n      expect(screen.getByRole('table')).toBeInTheDocument();\n      const rows = screen.getAllByRole('row');\n      expect(rows.length).toEqual(3);\n      const btns = screen.getAllByRole('button', { name: 'Expand row' });\n      expect(btns.length).toEqual(2);\n      expect(screen.queryByText('Partition stats')).not.toBeInTheDocument();\n\n      await userEvent.click(btns[0]);\n      expect(screen.getAllByText('Partition stats').length).toEqual(1);\n      await userEvent.click(btns[1]);\n      expect(screen.getAllByText('Partition stats').length).toEqual(2);\n    });\n  });\n\n  it('returns empty container', () => {\n    (useTopicAnalysis as jest.Mock).mockImplementation(() => ({\n      data: undefined,\n    }));\n    renderComponent();\n    expect(screen.queryByRole('table')).not.toBeInTheDocument();\n    expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();\n  });\n  it('returns empty container', () => {\n    (useTopicAnalysis as jest.Mock).mockImplementation(() => ({\n      data: {},\n    }));\n    renderComponent();\n    expect(screen.queryByRole('table')).not.toBeInTheDocument();\n    expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Statistics/__test__/Statistics.spec.tsx",
    "content": "import React from 'react';\nimport { screen, waitFor } from '@testing-library/react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport Statistics from 'components/Topics/Topic/Statistics/Statistics';\nimport { clusterTopicStatisticsPath } from 'lib/paths';\nimport { useTopicAnalysis, useAnalyzeTopic } from 'lib/hooks/api/topics';\nimport userEvent from '@testing-library/user-event';\n\nconst clusterName = 'local';\nconst topicName = 'topic';\n\njest.mock('lib/hooks/api/topics', () => ({\n  ...jest.requireActual('lib/hooks/api/topics'),\n  useTopicAnalysis: jest.fn(),\n  useAnalyzeTopic: jest.fn(),\n}));\n\ndescribe('Statistics', () => {\n  const renderComponent = () => {\n    const path = clusterTopicStatisticsPath(clusterName, topicName);\n    return render(\n      <WithRoute path={clusterTopicStatisticsPath()}>\n        <Statistics />\n      </WithRoute>,\n      { initialEntries: [path] }\n    );\n  };\n  const startMock = jest.fn();\n  it('renders Metrics component', async () => {\n    (useTopicAnalysis as jest.Mock).mockImplementation(() => ({\n      data: { result: 1 },\n    }));\n\n    renderComponent();\n    await expect(screen.getByText('Restart Analysis')).toBeInTheDocument();\n    expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();\n  });\n  it('renders Start Analysis button', async () => {\n    jest.spyOn(console, 'error').mockImplementation(() => undefined);\n    (useAnalyzeTopic as jest.Mock).mockImplementation(() => ({\n      mutateAsync: startMock,\n    }));\n    renderComponent();\n    const btn = screen.getByRole('button', { name: 'Start Analysis' });\n    expect(btn).toBeInTheDocument();\n    await waitFor(() => userEvent.click(btn));\n    expect(startMock).toHaveBeenCalled();\n    jest.clearAllMocks();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/Topic.tsx",
    "content": "import React, { Suspense } from 'react';\nimport { NavLink, Route, Routes, useNavigate } from 'react-router-dom';\nimport {\n  clusterTopicConsumerGroupsRelativePath,\n  clusterTopicEditRelativePath,\n  clusterTopicMessagesRelativePath,\n  clusterTopicSettingsRelativePath,\n  clusterTopicsPath,\n  clusterTopicStatisticsRelativePath,\n  RouteParamsClusterTopic,\n} from 'lib/paths';\nimport ClusterContext from 'components/contexts/ClusterContext';\nimport PageHeading from 'components/common/PageHeading/PageHeading';\nimport {\n  ActionButton,\n  ActionNavLink,\n  ActionDropdownItem,\n} from 'components/common/ActionComponent';\nimport Navbar from 'components/common/Navigation/Navbar.styled';\nimport { useAppDispatch } from 'lib/hooks/redux';\nimport useAppParams from 'lib/hooks/useAppParams';\nimport { Dropdown, DropdownItemHint } from 'components/common/Dropdown';\nimport {\n  useClearTopicMessages,\n  useDeleteTopic,\n  useRecreateTopic,\n  useTopicDetails,\n} from 'lib/hooks/api/topics';\nimport { resetTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';\nimport { Action, CleanUpPolicy, ResourceType } from 'generated-sources';\nimport PageLoader from 'components/common/PageLoader/PageLoader';\nimport SlidingSidebar from 'components/common/SlidingSidebar';\nimport useBoolean from 'lib/hooks/useBoolean';\n\nimport Messages from './Messages/Messages';\nimport Overview from './Overview/Overview';\nimport Settings from './Settings/Settings';\nimport TopicConsumerGroups from './ConsumerGroups/TopicConsumerGroups';\nimport Statistics from './Statistics/Statistics';\nimport Edit from './Edit/Edit';\nimport SendMessage from './SendMessage/SendMessage';\n\nconst Topic: React.FC = () => {\n  const dispatch = useAppDispatch();\n  const {\n    value: isSidebarOpen,\n    setFalse: closeSidebar,\n    setTrue: openSidebar,\n  } = useBoolean(false);\n  const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();\n\n  const navigate = useNavigate();\n  const deleteTopic = useDeleteTopic(clusterName);\n  const recreateTopic = useRecreateTopic({ clusterName, topicName });\n  const { data } = useTopicDetails({ clusterName, topicName });\n\n  const { isReadOnly, isTopicDeletionAllowed } =\n    React.useContext(ClusterContext);\n\n  const deleteTopicHandler = async () => {\n    await deleteTopic.mutateAsync(topicName);\n    navigate(clusterTopicsPath(clusterName));\n  };\n\n  React.useEffect(() => {\n    return () => {\n      dispatch(resetTopicMessages());\n    };\n  }, []);\n  const clearMessages = useClearTopicMessages(clusterName);\n  const clearTopicMessagesHandler = async () => {\n    await clearMessages.mutateAsync(topicName);\n  };\n  const canCleanup = data?.cleanUpPolicy === CleanUpPolicy.DELETE;\n  return (\n    <>\n      <PageHeading\n        text={topicName}\n        backText=\"Topics\"\n        backTo={clusterTopicsPath(clusterName)}\n      >\n        <ActionButton\n          buttonSize=\"M\"\n          buttonType=\"primary\"\n          onClick={openSidebar}\n          disabled={isReadOnly}\n          permission={{\n            resource: ResourceType.TOPIC,\n            action: Action.MESSAGES_PRODUCE,\n            value: topicName,\n          }}\n        >\n          Produce Message\n        </ActionButton>\n        <Dropdown disabled={isReadOnly || data?.internal}>\n          <ActionDropdownItem\n            onClick={() => navigate(clusterTopicEditRelativePath)}\n            permission={{\n              resource: ResourceType.TOPIC,\n              action: Action.EDIT,\n              value: topicName,\n            }}\n          >\n            Edit settings\n            <DropdownItemHint>\n              Pay attention! This operation has\n              <br />\n              especially important consequences.\n            </DropdownItemHint>\n          </ActionDropdownItem>\n\n          <ActionDropdownItem\n            onClick={clearTopicMessagesHandler}\n            confirm=\"Are you sure want to clear topic messages?\"\n            disabled={!canCleanup}\n            danger\n            permission={{\n              resource: ResourceType.TOPIC,\n              action: Action.MESSAGES_DELETE,\n              value: topicName,\n            }}\n          >\n            Clear messages\n            <DropdownItemHint>\n              Clearing messages is only allowed for topics\n              <br />\n              with DELETE policy\n            </DropdownItemHint>\n          </ActionDropdownItem>\n\n          <ActionDropdownItem\n            onClick={recreateTopic.mutateAsync}\n            confirm={\n              <>\n                Are you sure want to recreate <b>{topicName}</b> topic?\n              </>\n            }\n            danger\n            permission={{\n              resource: ResourceType.TOPIC,\n              action: [Action.MESSAGES_READ, Action.CREATE, Action.DELETE],\n              value: topicName,\n            }}\n          >\n            Recreate Topic\n          </ActionDropdownItem>\n          <ActionDropdownItem\n            onClick={deleteTopicHandler}\n            confirm={\n              <>\n                Are you sure want to remove <b>{topicName}</b> topic?\n              </>\n            }\n            disabled={!isTopicDeletionAllowed}\n            danger\n            permission={{\n              resource: ResourceType.TOPIC,\n              action: Action.DELETE,\n              value: topicName,\n            }}\n          >\n            Remove Topic\n            {!isTopicDeletionAllowed && (\n              <DropdownItemHint>\n                The topic deletion is restricted at the broker\n                <br />\n                configuration level (delete.topic.enable = false)\n              </DropdownItemHint>\n            )}\n          </ActionDropdownItem>\n        </Dropdown>\n      </PageHeading>\n      <Navbar role=\"navigation\">\n        <NavLink\n          to=\".\"\n          className={({ isActive }) => (isActive ? 'is-active' : '')}\n          end\n        >\n          Overview\n        </NavLink>\n        <ActionNavLink\n          to={clusterTopicMessagesRelativePath}\n          className={({ isActive }) => (isActive ? 'is-active' : '')}\n          permission={{\n            resource: ResourceType.TOPIC,\n            action: Action.MESSAGES_READ,\n            value: topicName,\n          }}\n        >\n          Messages\n        </ActionNavLink>\n        <NavLink\n          to={clusterTopicConsumerGroupsRelativePath}\n          className={({ isActive }) => (isActive ? 'is-active' : '')}\n        >\n          Consumers\n        </NavLink>\n        <NavLink\n          to={clusterTopicSettingsRelativePath}\n          className={({ isActive }) => (isActive ? 'is-active' : '')}\n        >\n          Settings\n        </NavLink>\n        <NavLink\n          to={clusterTopicStatisticsRelativePath}\n          className={({ isActive }) => (isActive ? 'is-active' : '')}\n        >\n          Statistics\n        </NavLink>\n      </Navbar>\n      <Suspense fallback={<PageLoader />}>\n        <Routes>\n          <Route index element={<Overview />} />\n          <Route\n            path={clusterTopicMessagesRelativePath}\n            element={<Messages />}\n          />\n          <Route\n            path={clusterTopicSettingsRelativePath}\n            element={<Settings />}\n          />\n          <Route\n            path={clusterTopicConsumerGroupsRelativePath}\n            element={<TopicConsumerGroups />}\n          />\n          <Route\n            path={clusterTopicStatisticsRelativePath}\n            element={<Statistics />}\n          />\n          <Route path={clusterTopicEditRelativePath} element={<Edit />} />\n        </Routes>\n      </Suspense>\n      <SlidingSidebar\n        open={isSidebarOpen}\n        onClose={closeSidebar}\n        title=\"Produce Message\"\n      >\n        <Suspense fallback={<PageLoader />}>\n          <SendMessage closeSidebar={closeSidebar} />\n        </Suspense>\n      </SlidingSidebar>\n    </>\n  );\n};\n\nexport default Topic;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topic/__test__/Topic.spec.tsx",
    "content": "import React from 'react';\nimport { screen, waitFor } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport ClusterContext from 'components/contexts/ClusterContext';\nimport Details from 'components/Topics/Topic/Topic';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport {\n  clusterTopicConsumerGroupsPath,\n  clusterTopicEditRelativePath,\n  clusterTopicMessagesPath,\n  clusterTopicPath,\n  clusterTopicSettingsPath,\n  clusterTopicsPath,\n  clusterTopicStatisticsPath,\n  getNonExactPath,\n} from 'lib/paths';\nimport { CleanUpPolicy, Topic } from 'generated-sources';\nimport { externalTopicPayload } from 'lib/fixtures/topics';\nimport {\n  useClearTopicMessages,\n  useDeleteTopic,\n  useRecreateTopic,\n  useTopicDetails,\n} from 'lib/hooks/api/topics';\nimport { useAppDispatch } from 'lib/hooks/redux';\n\nconst mockNavigate = jest.fn();\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useNavigate: () => mockNavigate,\n}));\njest.mock('lib/hooks/api/topics', () => ({\n  useTopicDetails: jest.fn(),\n  useDeleteTopic: jest.fn(),\n  useRecreateTopic: jest.fn(),\n  useClearTopicMessages: jest.fn(),\n}));\n\nconst unwrapMock = jest.fn();\nconst clearTopicMessages = jest.fn();\n\njest.mock('lib/hooks/redux', () => ({\n  ...jest.requireActual('lib/hooks/redux'),\n  useAppDispatch: jest.fn(),\n}));\n\njest.mock('components/Topics/Topic/Overview/Overview', () => () => (\n  <>OverviewMock</>\n));\njest.mock('components/Topics/Topic/Messages/Messages', () => () => (\n  <>MessagesMock</>\n));\njest.mock('components/Topics/Topic/SendMessage/SendMessage', () => () => (\n  <>SendMessageMock</>\n));\njest.mock('components/Topics/Topic/Settings/Settings', () => () => (\n  <>SettingsMock</>\n));\njest.mock(\n  'components/Topics/Topic/ConsumerGroups/TopicConsumerGroups',\n  () => () => <>ConsumerGroupsMock</>\n);\njest.mock('components/Topics/Topic/Statistics/Statistics', () => () => (\n  <>StatisticsMock</>\n));\n\nconst mockDelete = jest.fn();\nconst mockRecreate = jest.fn();\nconst mockClusterName = 'local';\nconst topic: Topic = {\n  ...externalTopicPayload,\n  cleanUpPolicy: CleanUpPolicy.DELETE,\n};\nconst defaultPath = clusterTopicPath(mockClusterName, topic.name);\n\ndescribe('Details', () => {\n  const renderComponent = (isReadOnly = false, path = defaultPath) => {\n    render(\n      <ClusterContext.Provider\n        value={{\n          isReadOnly,\n          hasKafkaConnectConfigured: true,\n          hasSchemaRegistryConfigured: true,\n          isTopicDeletionAllowed: true,\n        }}\n      >\n        <WithRoute path={getNonExactPath(clusterTopicPath())}>\n          <Details />\n        </WithRoute>\n      </ClusterContext.Provider>,\n      { initialEntries: [path] }\n    );\n  };\n\n  beforeEach(async () => {\n    (useTopicDetails as jest.Mock).mockImplementation(() => ({\n      data: topic,\n    }));\n    (useDeleteTopic as jest.Mock).mockImplementation(() => ({\n      mutateAsync: mockDelete,\n    }));\n    (useRecreateTopic as jest.Mock).mockImplementation(() => ({\n      mutateAsync: mockRecreate,\n    }));\n    (useClearTopicMessages as jest.Mock).mockImplementation(() => ({\n      mutateAsync: clearTopicMessages,\n    }));\n    (useAppDispatch as jest.Mock).mockImplementation(() => () => ({\n      unwrap: unwrapMock,\n    }));\n  });\n  describe('Action Bar', () => {\n    describe('when it has readonly flag', () => {\n      it('renders disabled the Action button', () => {\n        renderComponent(true);\n        expect(\n          screen.getByRole('button', { name: 'Produce Message' })\n        ).toBeDisabled();\n      });\n    });\n\n    describe('when remove topic modal is open', () => {\n      beforeEach(async () => {\n        renderComponent();\n        const openModalButton = screen.getAllByText('Remove Topic')[0];\n        await userEvent.click(openModalButton);\n      });\n\n      it('calls deleteTopic on confirm', async () => {\n        const submitButton = screen.getAllByRole('button', {\n          name: 'Confirm',\n        })[0];\n        await userEvent.click(submitButton);\n        expect(mockDelete).toHaveBeenCalledWith(topic.name);\n      });\n      it('closes the modal when cancel button is clicked', async () => {\n        const cancelButton = screen.getAllByText('Cancel')[0];\n        await waitFor(() => userEvent.click(cancelButton));\n        expect(cancelButton).not.toBeInTheDocument();\n      });\n    });\n\n    describe('when clear messages modal is open', () => {\n      beforeEach(async () => {\n        await renderComponent();\n        const confirmButton = screen.getAllByText('Clear messages')[0];\n        await userEvent.click(confirmButton);\n      });\n\n      it('it calls clearTopicMessages on confirm', async () => {\n        const submitButton = screen.getAllByRole('button', {\n          name: 'Confirm',\n        })[0];\n        await waitFor(() => userEvent.click(submitButton));\n        expect(clearTopicMessages).toHaveBeenCalledTimes(1);\n      });\n\n      it('closes the modal when cancel button is clicked', async () => {\n        const cancelButton = screen.getAllByText('Cancel')[0];\n        await waitFor(() => userEvent.click(cancelButton));\n\n        expect(cancelButton).not.toBeInTheDocument();\n      });\n    });\n\n    describe('when edit settings is clicked', () => {\n      it('redirects to the edit page', async () => {\n        renderComponent();\n        const button = screen.getAllByText('Edit settings')[0];\n        await userEvent.click(button);\n        expect(mockNavigate).toHaveBeenCalledWith(clusterTopicEditRelativePath);\n      });\n    });\n\n    it('redirects to the correct route if topic is deleted', async () => {\n      renderComponent();\n      const deleteTopicButton = screen.getByText(/Remove topic/i);\n      await waitFor(() => userEvent.click(deleteTopicButton));\n      const submitDeleteButton = screen.getByRole('button', {\n        name: 'Confirm',\n      });\n      await userEvent.click(submitDeleteButton);\n      expect(mockNavigate).toHaveBeenCalledWith(\n        clusterTopicsPath(mockClusterName)\n      );\n    });\n\n    it('shows a confirmation popup on deleting topic messages', async () => {\n      renderComponent();\n      const clearMessagesButton = screen.getAllByText(/Clear messages/i)[0];\n      await userEvent.click(clearMessagesButton);\n\n      expect(\n        screen.getByText(/Are you sure want to clear topic messages?/i)\n      ).toBeInTheDocument();\n    });\n\n    it('shows a confirmation popup on recreating topic', async () => {\n      renderComponent();\n      const recreateTopicButton = screen.getByText(/Recreate topic/i);\n      await userEvent.click(recreateTopicButton);\n      expect(\n        screen.getByText(/Are you sure want to recreate topic?/i)\n      ).toBeInTheDocument();\n    });\n\n    it('is calling recreation function after click on Submit button', async () => {\n      renderComponent();\n      const recreateTopicButton = screen.getByText(/Recreate topic/i);\n      await userEvent.click(recreateTopicButton);\n      const confirmBtn = screen.getByRole('button', { name: /Confirm/i });\n\n      await waitFor(() => userEvent.click(confirmBtn));\n      expect(mockRecreate).toBeCalledTimes(1);\n    });\n\n    it('closes popup confirmation window after click on Cancel button', async () => {\n      renderComponent();\n      const recreateTopicButton = screen.getByText(/Recreate topic/i);\n      await userEvent.click(recreateTopicButton);\n      const cancelBtn = screen.getByRole('button', { name: /cancel/i });\n      await userEvent.click(cancelBtn);\n      expect(\n        screen.queryByText(/Are you sure want to recreate topic?/i)\n      ).not.toBeInTheDocument();\n    });\n  });\n\n  describe('Internal routing', () => {\n    const itExpectsCorrectPageRendered = (\n      path: string,\n      tab: string,\n      selector: string\n    ) => {\n      renderComponent(false, path);\n      expect(screen.getByText(tab)).toHaveClass('is-active');\n      expect(screen.getByText(selector)).toBeInTheDocument();\n    };\n\n    it('renders Overview tab by default', () => {\n      itExpectsCorrectPageRendered(defaultPath, 'Overview', 'OverviewMock');\n    });\n    it('renders Messages tabs', () => {\n      itExpectsCorrectPageRendered(\n        clusterTopicMessagesPath(),\n        'Messages',\n        'MessagesMock'\n      );\n    });\n    it('renders Consumers tab', () => {\n      itExpectsCorrectPageRendered(\n        clusterTopicConsumerGroupsPath(),\n        'Consumers',\n        'ConsumerGroupsMock'\n      );\n    });\n    it('renders Settings tab', () => {\n      itExpectsCorrectPageRendered(\n        clusterTopicSettingsPath(),\n        'Settings',\n        'SettingsMock'\n      );\n    });\n    it('renders Statistics tab', () => {\n      itExpectsCorrectPageRendered(\n        clusterTopicStatisticsPath(),\n        'Statistics',\n        'StatisticsMock'\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/Topics.tsx",
    "content": "import React from 'react';\nimport { Route, Routes } from 'react-router-dom';\nimport {\n  clusterTopicCopyRelativePath,\n  clusterTopicNewRelativePath,\n  getNonExactPath,\n  RouteParams,\n} from 'lib/paths';\nimport SuspenseQueryComponent from 'components/common/SuspenseQueryComponent/SuspenseQueryComponent';\n\nimport New from './New/New';\nimport ListPage from './List/ListPage';\nimport Topic from './Topic/Topic';\n\nconst Topics: React.FC = () => (\n  <Routes>\n    <Route index element={<ListPage />} />\n    <Route path={clusterTopicNewRelativePath} element={<New />} />\n    <Route path={clusterTopicCopyRelativePath} element={<New />} />\n    <Route\n      path={getNonExactPath(RouteParams.topicName)}\n      element={\n        <SuspenseQueryComponent>\n          <Topic />\n        </SuspenseQueryComponent>\n      }\n    />\n  </Routes>\n);\n\nexport default Topics;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/__tests__/Topics.spec.tsx",
    "content": "import React from 'react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport Topics from 'components/Topics/Topics';\nimport { screen } from '@testing-library/react';\nimport {\n  clusterTopicCopyPath,\n  clusterTopicNewPath,\n  clusterTopicPath,\n  clusterTopicsPath,\n  getNonExactPath,\n} from 'lib/paths';\n\nconst listContainer = 'My List Page';\nconst topicContainer = 'My Topic Details Page';\nconst newCopyContainer = 'My New/Copy Page';\n\njest.mock('components/Topics/List/ListPage', () => () => (\n  <div>{listContainer}</div>\n));\njest.mock('components/Topics/Topic/Topic', () => () => (\n  <div>{topicContainer}</div>\n));\njest.mock('components/Topics/New/New', () => () => (\n  <div>{newCopyContainer}</div>\n));\n\ndescribe('Topics Component', () => {\n  const clusterName = 'clusterName';\n  const topicName = 'topicName';\n  const setUpComponent = (path: string) =>\n    render(\n      <WithRoute path={getNonExactPath(clusterTopicsPath())}>\n        <Topics />\n      </WithRoute>,\n      { initialEntries: [path] }\n    );\n\n  it('should check if the page is Topics List rendered', () => {\n    setUpComponent(clusterTopicsPath(clusterName));\n    expect(screen.getByText(listContainer)).toBeInTheDocument();\n  });\n\n  it('should check if the page is  New Topic  rendered', () => {\n    setUpComponent(clusterTopicNewPath(clusterName));\n    expect(screen.getByText(newCopyContainer)).toBeInTheDocument();\n  });\n\n  it('should check if the page is Copy Topic rendered', () => {\n    setUpComponent(clusterTopicCopyPath(clusterName));\n    expect(screen.getByText(newCopyContainer)).toBeInTheDocument();\n  });\n\n  it('should check if the page is Topic page rendered', () => {\n    setUpComponent(clusterTopicPath(clusterName, topicName));\n    expect(screen.getByText(topicContainer)).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamField.tsx",
    "content": "import React, { useRef } from 'react';\nimport { ErrorMessage } from '@hookform/error-message';\nimport { TOPIC_CUSTOM_PARAMS } from 'lib/constants';\nimport { FieldArrayWithId, useFormContext, Controller } from 'react-hook-form';\nimport { TopicConfigParams, TopicFormData } from 'redux/interfaces';\nimport { InputLabel } from 'components/common/Input/InputLabel.styled';\nimport { FormError } from 'components/common/Input/Input.styled';\nimport Select from 'components/common/Select/Select';\nimport Input from 'components/common/Input/Input';\nimport IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';\nimport CloseCircleIcon from 'components/common/Icons/CloseCircleIcon';\nimport * as C from 'components/Topics/shared/Form/TopicForm.styled';\nimport { ConfigSource } from 'generated-sources';\n\nimport * as S from './CustomParams.styled';\n\nexport interface Props {\n  config?: TopicConfigParams;\n  isDisabled: boolean;\n  index: number;\n  existingFields: string[];\n  field: FieldArrayWithId<TopicFormData, 'customParams', 'id'>;\n  remove: (index: number) => void;\n  setExistingFields: React.Dispatch<React.SetStateAction<string[]>>;\n}\n\nconst CustomParamField: React.FC<Props> = ({\n  field,\n  isDisabled,\n  index,\n  remove,\n  config,\n  existingFields,\n  setExistingFields,\n}) => {\n  const {\n    formState: { errors },\n    setValue,\n    watch,\n    control,\n  } = useFormContext<TopicFormData>();\n  const nameValue = watch(`customParams.${index}.name`);\n  const prevName = useRef(nameValue);\n\n  const options = Object.keys(TOPIC_CUSTOM_PARAMS)\n    .sort()\n    .map((option) => ({\n      value: option,\n      label: option,\n      disabled:\n        (config &&\n          config[option]?.source !== ConfigSource.DYNAMIC_TOPIC_CONFIG) ||\n        existingFields.includes(option),\n    }));\n\n  React.useEffect(() => {\n    if (nameValue !== prevName.current) {\n      let newExistingFields = [...existingFields];\n      if (prevName.current) {\n        newExistingFields = newExistingFields.filter(\n          (name) => name !== prevName.current\n        );\n      }\n      prevName.current = nameValue;\n      newExistingFields.push(nameValue);\n      setExistingFields(newExistingFields);\n      setValue(`customParams.${index}.value`, TOPIC_CUSTOM_PARAMS[nameValue], {\n        shouldValidate: !!TOPIC_CUSTOM_PARAMS[nameValue],\n      });\n    }\n  }, [existingFields, index, nameValue, setExistingFields, setValue]);\n\n  return (\n    <C.Column>\n      <div>\n        <InputLabel>Custom Parameter *</InputLabel>\n        <Controller\n          control={control}\n          rules={{ required: 'Custom Parameter is required.' }}\n          name={`customParams.${index}.name`}\n          render={({ field: { value, name, onChange } }) => (\n            <Select\n              name={name}\n              placeholder=\"Select\"\n              disabled={isDisabled}\n              minWidth=\"270px\"\n              onChange={onChange}\n              value={value}\n              options={options}\n            />\n          )}\n        />\n        <FormError>\n          <ErrorMessage\n            errors={errors}\n            name={`customParams.${index}.name` as const}\n          />\n        </FormError>\n      </div>\n      <div>\n        <InputLabel>Value *</InputLabel>\n        <Input\n          name={`customParams.${index}.value` as const}\n          hookFormOptions={{\n            required: 'Value is required.',\n          }}\n          placeholder=\"Value\"\n          defaultValue={field.value}\n          autoComplete=\"off\"\n          disabled={isDisabled}\n        />\n        <FormError>\n          <ErrorMessage\n            errors={errors}\n            name={`customParams.${index}.value` as const}\n          />\n        </FormError>\n      </div>\n\n      <S.DeleteButtonWrapper>\n        <IconButtonWrapper\n          onClick={() => remove(index)}\n          onKeyDown={(e: React.KeyboardEvent) =>\n            e.code === 'Space' && remove(index)\n          }\n          title={`Delete customParam field ${index}`}\n        >\n          <CloseCircleIcon aria-hidden />\n        </IconButtonWrapper>\n      </S.DeleteButtonWrapper>\n    </C.Column>\n  );\n};\n\nexport default React.memo(CustomParamField);\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const ParamsWrapper = styled.div`\n  margin-top: 16px;\n  margin-bottom: 16px;\n`;\n\nexport const DeleteButtonWrapper = styled.div`\n  min-height: 32px;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-self: flex-start;\n  margin-top: 32px;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.tsx",
    "content": "import React from 'react';\nimport { TopicConfigParams, TopicFormData } from 'redux/interfaces';\nimport { useFieldArray, useFormContext, useWatch } from 'react-hook-form';\nimport { Button } from 'components/common/Button/Button';\nimport { TOPIC_CUSTOM_PARAMS_PREFIX } from 'lib/constants';\nimport PlusIcon from 'components/common/Icons/PlusIcon';\n\nimport CustomParamField from './CustomParamField';\nimport * as S from './CustomParams.styled';\n\nexport interface CustomParamsProps {\n  config?: TopicConfigParams;\n  isSubmitting: boolean;\n  isEditing?: boolean;\n}\n\nconst CustomParams: React.FC<CustomParamsProps> = ({\n  isSubmitting,\n  config,\n}) => {\n  const { control } = useFormContext<TopicFormData>();\n  const { fields, append, remove } = useFieldArray({\n    control,\n    name: TOPIC_CUSTOM_PARAMS_PREFIX,\n  });\n  const watchFieldArray = useWatch({\n    control,\n    name: TOPIC_CUSTOM_PARAMS_PREFIX,\n    defaultValue: fields,\n  });\n  const controlledFields = fields.map((field, index) => {\n    return {\n      ...field,\n      ...watchFieldArray[index],\n    };\n  });\n\n  const [existingFields, setExistingFields] = React.useState<string[]>([]);\n\n  const removeField = (index: number): void => {\n    setExistingFields(\n      existingFields.filter((field) => field !== controlledFields[index].name)\n    );\n    remove(index);\n  };\n\n  return (\n    <S.ParamsWrapper>\n      {controlledFields?.map((field, idx) => (\n        <CustomParamField\n          key={field.id}\n          config={config}\n          field={field}\n          remove={removeField}\n          index={idx}\n          isDisabled={isSubmitting}\n          existingFields={existingFields}\n          setExistingFields={setExistingFields}\n        />\n      ))}\n      <div>\n        <Button\n          type=\"button\"\n          buttonSize=\"M\"\n          buttonType=\"secondary\"\n          onClick={() =>\n            append({ name: '', value: '' }, { shouldFocus: false })\n          }\n        >\n          <PlusIcon />\n          Add Custom Parameter\n        </Button>\n      </div>\n    </S.ParamsWrapper>\n  );\n};\n\nexport default CustomParams;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__test__/CustomParamField.spec.tsx",
    "content": "import React, { PropsWithChildren } from 'react';\nimport { act, screen, within } from '@testing-library/react';\nimport { render } from 'lib/testHelpers';\nimport CustomParamsField, {\n  Props,\n} from 'components/Topics/shared/Form/CustomParams/CustomParamField';\nimport { FormProvider, useForm } from 'react-hook-form';\nimport userEvent from '@testing-library/user-event';\nimport { TOPIC_CUSTOM_PARAMS } from 'lib/constants';\n\nconst isDisabled = false;\nconst index = 0;\nconst existingFields: string[] = [];\nconst field = { name: 'name', value: 'value', id: 'id' };\n\nconst SPACE_KEY = ' ';\n\nconst selectOption = async (listbox: HTMLElement, option: string) => {\n  await act(() => userEvent.click(listbox));\n  await act(() => userEvent.click(screen.getByText(option)));\n};\n\ndescribe('CustomParamsField', () => {\n  const remove = jest.fn();\n  const setExistingFields = jest.fn();\n\n  const setupComponent = (props: Props) => {\n    const Wrapper: React.FC<PropsWithChildren<unknown>> = ({ children }) => {\n      const methods = useForm();\n      return <FormProvider {...methods}>{children}</FormProvider>;\n    };\n\n    return render(\n      <Wrapper>\n        <CustomParamsField {...props} />\n      </Wrapper>\n    );\n  };\n\n  afterEach(() => {\n    remove.mockClear();\n    setExistingFields.mockClear();\n  });\n\n  it('renders the component with its view correctly', () => {\n    setupComponent({\n      field,\n      isDisabled,\n      index,\n      remove,\n      existingFields,\n      setExistingFields,\n    });\n    expect(screen.getByRole('listbox')).toBeInTheDocument();\n    expect(screen.getByRole('textbox')).toBeInTheDocument();\n    expect(screen.getByRole('button')).toBeInTheDocument();\n  });\n\n  describe('core functionality works', () => {\n    it('click on button triggers remove', async () => {\n      setupComponent({\n        field,\n        isDisabled,\n        index,\n        remove,\n        existingFields,\n        setExistingFields,\n      });\n      await userEvent.click(screen.getByRole('button'));\n      expect(remove).toHaveBeenCalledTimes(1);\n    });\n\n    it('pressing space on button triggers remove', async () => {\n      setupComponent({\n        field,\n        isDisabled,\n        index,\n        remove,\n        existingFields,\n        setExistingFields,\n      });\n      await userEvent.type(screen.getByRole('button'), SPACE_KEY);\n      // userEvent.type triggers remove two times as at first it clicks on element and then presses space\n      expect(remove).toHaveBeenCalledTimes(2);\n    });\n\n    it('can select option', async () => {\n      setupComponent({\n        field,\n        isDisabled,\n        index,\n        remove,\n        existingFields,\n        setExistingFields,\n      });\n      const listbox = screen.getByRole('listbox');\n      await selectOption(listbox, 'compression.type');\n\n      const selectedOption = within(listbox).getAllByRole('option');\n      expect(selectedOption.length).toEqual(1);\n      expect(selectedOption[0]).toHaveTextContent('compression.type');\n    });\n\n    it('selecting option updates textbox value', async () => {\n      setupComponent({\n        field,\n        isDisabled,\n        index,\n        remove,\n        existingFields,\n        setExistingFields,\n      });\n      const listbox = screen.getByRole('listbox');\n      await selectOption(listbox, 'compression.type');\n\n      const textbox = screen.getByRole('textbox');\n      expect(textbox).toHaveValue(TOPIC_CUSTOM_PARAMS['compression.type']);\n    });\n\n    it('selecting option updates triggers setExistingFields', async () => {\n      setupComponent({\n        field,\n        isDisabled,\n        index,\n        remove,\n        existingFields,\n        setExistingFields,\n      });\n      const listbox = screen.getByRole('listbox');\n      await selectOption(listbox, 'compression.type');\n\n      expect(setExistingFields).toHaveBeenCalledTimes(1);\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__test__/CustomParams.spec.tsx",
    "content": "import React, { PropsWithChildren } from 'react';\nimport { act, screen, within } from '@testing-library/react';\nimport { render } from 'lib/testHelpers';\nimport CustomParams, {\n  CustomParamsProps,\n} from 'components/Topics/shared/Form/CustomParams/CustomParams';\nimport { FormProvider, useForm } from 'react-hook-form';\nimport userEvent from '@testing-library/user-event';\nimport { TOPIC_CUSTOM_PARAMS } from 'lib/constants';\n\nimport { defaultValues } from './fixtures';\n\nconst selectOption = async (listbox: HTMLElement, option: string) => {\n  await act(async () => {\n    await userEvent.click(listbox);\n  });\n  await userEvent.click(screen.getByText(option));\n};\n\nconst expectOptionIsSelected = (listbox: HTMLElement, option: string) => {\n  const selectedOption = within(listbox).getAllByRole('option');\n  expect(selectedOption.length).toEqual(1);\n  expect(selectedOption[0]).toHaveTextContent(option);\n};\n\nconst expectOptionAvailability = async (\n  listbox: HTMLElement,\n  option: string,\n  disabled: boolean\n) => {\n  await act(async () => {\n    await userEvent.click(listbox);\n  });\n  const selectedOptions = within(listbox).getAllByText(option).reverse();\n  // its either two or one nodes, we only need last one\n  const selectedOption = selectedOptions[0];\n\n  if (disabled) {\n    expect(selectedOption).toHaveAttribute('disabled');\n  } else {\n    expect(selectedOption).toBeEnabled();\n  }\n\n  expect(selectedOption).toHaveStyleRule(\n    'cursor',\n    disabled ? 'not-allowed' : 'pointer'\n  );\n  await act(async () => {\n    await userEvent.click(listbox);\n  });\n};\n\nconst renderComponent = (props: CustomParamsProps, defaults = {}) => {\n  const Wrapper: React.FC<PropsWithChildren<unknown>> = ({ children }) => {\n    const methods = useForm({ defaultValues: defaults });\n    return <FormProvider {...methods}>{children}</FormProvider>;\n  };\n\n  return render(\n    <Wrapper>\n      <CustomParams {...props} />\n    </Wrapper>\n  );\n};\n\ndescribe('CustomParams', () => {\n  it('renders with props', () => {\n    renderComponent({ isSubmitting: false });\n\n    const button = screen.getByRole('button');\n    expect(button).toBeInTheDocument();\n    expect(button).toHaveTextContent('Add Custom Parameter');\n  });\n\n  it('has defaultValues when they are set', () => {\n    renderComponent({ isSubmitting: false }, defaultValues);\n\n    expect(\n      screen.getByRole('option', { name: defaultValues.customParams[0].name })\n    ).toBeInTheDocument();\n    expect(screen.getByRole('textbox')).toHaveValue(\n      defaultValues.customParams[0].value\n    );\n  });\n\n  describe('works with user inputs correctly', () => {\n    let button: HTMLButtonElement;\n\n    beforeEach(async () => {\n      renderComponent({ isSubmitting: false });\n      button = screen.getByRole('button');\n      await act(async () => {\n        await userEvent.click(button);\n      });\n    });\n\n    it('button click creates custom param fieldset', async () => {\n      const listbox = screen.getByRole('listbox');\n      expect(listbox).toBeInTheDocument();\n\n      const textbox = screen.getByRole('textbox');\n      expect(textbox).toBeInTheDocument();\n    });\n\n    it('can select option', async () => {\n      const listbox = screen.getByRole('listbox');\n\n      await selectOption(listbox, 'compression.type');\n      expectOptionIsSelected(listbox, 'compression.type');\n      await expectOptionAvailability(listbox, 'compression.type', true);\n\n      const textbox = screen.getByRole('textbox');\n      expect(textbox).toHaveValue(TOPIC_CUSTOM_PARAMS['compression.type']);\n    });\n\n    it('when selected option changes disabled options update correctly', async () => {\n      const listbox = screen.getByRole('listbox');\n\n      await selectOption(listbox, 'compression.type');\n      expectOptionIsSelected(listbox, 'compression.type');\n      await expectOptionAvailability(listbox, 'compression.type', true);\n\n      await selectOption(listbox, 'delete.retention.ms');\n      await expectOptionAvailability(listbox, 'delete.retention.ms', true);\n      await expectOptionAvailability(listbox, 'compression.type', false);\n    });\n\n    it('multiple button clicks create multiple fieldsets', async () => {\n      await act(async () => {\n        await userEvent.click(button);\n      });\n      await act(async () => {\n        await userEvent.click(button);\n      });\n\n      const listboxes = screen.getAllByRole('listbox');\n      expect(listboxes.length).toBe(3);\n\n      const textboxes = screen.getAllByRole('textbox');\n      expect(textboxes.length).toBe(3);\n    });\n\n    it(\"can't select already selected option\", async () => {\n      await act(async () => {\n        await userEvent.click(button);\n      });\n\n      const listboxes = screen.getAllByRole('listbox');\n\n      const firstListbox = listboxes[0];\n      await selectOption(firstListbox, 'compression.type');\n      await expectOptionAvailability(firstListbox, 'compression.type', true);\n\n      const secondListbox = listboxes[1];\n      await expectOptionAvailability(secondListbox, 'compression.type', true);\n    });\n\n    it('when fieldset with selected custom property type is deleted disabled options update correctly', async () => {\n      await act(async () => {\n        await userEvent.click(button);\n      });\n      await act(async () => {\n        await userEvent.click(button);\n      });\n\n      const listboxes = screen.getAllByRole('listbox');\n\n      const firstListbox = listboxes[0];\n      await selectOption(firstListbox, 'compression.type');\n      await expectOptionAvailability(firstListbox, 'compression.type', true);\n\n      const secondListbox = listboxes[1];\n      await selectOption(secondListbox, 'delete.retention.ms');\n      await expectOptionAvailability(\n        secondListbox,\n        'delete.retention.ms',\n        true\n      );\n\n      const thirdListbox = listboxes[2];\n      await selectOption(thirdListbox, 'file.delete.delay.ms');\n      await expectOptionAvailability(\n        thirdListbox,\n        'file.delete.delay.ms',\n        true\n      );\n\n      const deleteSecondFieldsetButton = screen.getByTitle(\n        'Delete customParam field 1'\n      );\n      await act(async () => {\n        await userEvent.click(deleteSecondFieldsetButton);\n      });\n      expect(secondListbox).not.toBeInTheDocument();\n\n      await expectOptionAvailability(\n        firstListbox,\n        'delete.retention.ms',\n        false\n      );\n      await expectOptionAvailability(\n        thirdListbox,\n        'delete.retention.ms',\n        false\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__test__/fixtures.ts",
    "content": "export const defaultValues = {\n  partitions: 1,\n  replicationFactor: 1,\n  minInSyncReplicas: 1,\n  cleanupPolicy: 'delete',\n  retentionBytes: -1,\n  maxMessageBytes: 1000012,\n  name: 'TestCustomParamEdit',\n  customParams: [\n    {\n      name: 'delete.retention.ms',\n      value: '86400001',\n    },\n  ],\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/shared/Form/TimeToRetain.tsx",
    "content": "import React from 'react';\nimport prettyMilliseconds from 'pretty-ms';\nimport { useFormContext } from 'react-hook-form';\nimport { ErrorMessage } from '@hookform/error-message';\nimport { MILLISECONDS_IN_WEEK, MILLISECONDS_IN_SECOND } from 'lib/constants';\nimport { InputLabel } from 'components/common/Input/InputLabel.styled';\nimport Input from 'components/common/Input/Input';\nimport { FormError } from 'components/common/Input/Input.styled';\n\nimport * as S from './TopicForm.styled';\nimport TimeToRetainBtns from './TimeToRetainBtns';\n\ninterface Props {\n  isSubmitting: boolean;\n}\n\nconst TimeToRetain: React.FC<Props> = ({ isSubmitting }) => {\n  const {\n    watch,\n    formState: { errors },\n  } = useFormContext();\n  const defaultValue = MILLISECONDS_IN_WEEK;\n  const name = 'retentionMs';\n  const watchedValue = watch(name, defaultValue.toString());\n\n  const valueHint = React.useMemo(() => {\n    const value = parseInt(watchedValue, 10);\n    return value >= MILLISECONDS_IN_SECOND ? prettyMilliseconds(value) : false;\n  }, [watchedValue]);\n\n  return (\n    <>\n      <S.Label>\n        <InputLabel htmlFor=\"timeToRetain\">\n          Time to retain data (in ms)\n        </InputLabel>\n        {valueHint && <span>{valueHint}</span>}\n      </S.Label>\n      <Input\n        id=\"timeToRetain\"\n        type=\"number\"\n        placeholder=\" Time to retain data (in ms)\"\n        name={name}\n        hookFormOptions={{\n          min: { value: -1, message: 'must be greater than or equal to -1' },\n        }}\n        disabled={isSubmitting}\n      />\n\n      <FormError>\n        <ErrorMessage errors={errors} name={name} />\n      </FormError>\n\n      <TimeToRetainBtns name={name} value={watchedValue} />\n    </>\n  );\n};\n\nexport default TimeToRetain;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/shared/Form/TimeToRetainBtn.tsx",
    "content": "import React from 'react';\nimport { useFormContext } from 'react-hook-form';\nimport { MILLISECONDS_IN_WEEK } from 'lib/constants';\n\nimport * as S from './TopicForm.styled';\n\nexport interface Props {\n  inputName: string;\n  text: string;\n  value: number;\n}\n\nconst TimeToRetainBtn: React.FC<Props> = ({ inputName, text, value }) => {\n  const { setValue, watch } = useFormContext();\n  const watchedValue = watch(inputName, MILLISECONDS_IN_WEEK.toString());\n\n  return (\n    <S.Button\n      isActive={parseFloat(watchedValue) === value}\n      type=\"button\"\n      onClick={() =>\n        setValue(inputName, value, {\n          shouldDirty: true,\n        })\n      }\n    >\n      {text}\n    </S.Button>\n  );\n};\n\nexport default TimeToRetainBtn;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/shared/Form/TimeToRetainBtns.tsx",
    "content": "import React from 'react';\nimport { MILLISECONDS_IN_DAY } from 'lib/constants';\nimport styled from 'styled-components';\n\nimport TimeToRetainBtn from './TimeToRetainBtn';\n\nexport interface Props {\n  name: string;\n  value: string;\n}\n\nconst TimeToRetainBtnsWrapper = styled.div`\n  display: flex;\n  gap: 8px;\n  padding-top: 8px;\n`;\n\nconst TimeToRetainBtns: React.FC<Props> = ({ name }) => (\n  <TimeToRetainBtnsWrapper>\n    <TimeToRetainBtn\n      text=\"12 hours\"\n      inputName={name}\n      value={MILLISECONDS_IN_DAY / 2}\n    />\n    <TimeToRetainBtn\n      text=\"1 day\"\n      inputName={name}\n      value={MILLISECONDS_IN_DAY}\n    />\n    <TimeToRetainBtn\n      text=\"2 days\"\n      inputName={name}\n      value={MILLISECONDS_IN_DAY * 2}\n    />\n    <TimeToRetainBtn\n      text=\"7 days\"\n      inputName={name}\n      value={MILLISECONDS_IN_DAY * 7}\n    />\n    <TimeToRetainBtn\n      text=\"4 weeks\"\n      inputName={name}\n      value={MILLISECONDS_IN_DAY * 7 * 4}\n    />\n  </TimeToRetainBtnsWrapper>\n);\n\nexport default TimeToRetainBtns;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.styled.ts",
    "content": "import styled from 'styled-components';\nimport Input from 'components/common/Input/Input';\n\nexport const Column = styled.div`\n  display: flex;\n  justify-content: flex-start;\n  gap: 8px;\n  margin-bottom: 16px;\n`;\n\nexport const NameField = styled.div`\n  flex-grow: 1;\n`;\n\nexport const CustomParamsHeading = styled.h4`\n  font-weight: 500;\n  color: ${({ theme }) => theme.heading.h4};\n`;\n\nexport const MessageSizeInput = styled(Input)`\n  min-width: 195px;\n`;\n\nexport const Label = styled.div`\n  display: flex;\n  gap: 16px;\n  align-items: center;\n\n  & > span {\n    font-size: 12px;\n    color: ${({ theme }) => theme.topicFormLabel.color};\n  }\n`;\n\nexport const Button = styled.button<{ isActive: boolean }>`\n  background-color: ${({ theme, ...props }) =>\n    props.isActive\n      ? theme.chips.backgroundColor.active\n      : theme.chips.backgroundColor.normal};\n  color: ${({ theme, ...props }) =>\n    props.isActive ? theme.chips.color.active : theme.chips.color.normal};\n  height: 24px;\n  padding: 0 5px;\n  min-width: 51px;\n  border: none;\n  border-radius: 6px;\n  &:hover {\n    cursor: pointer;\n  }\n`;\n\nexport const ButtonWrapper = styled.div`\n  display: flex;\n  gap: 10px;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx",
    "content": "import React from 'react';\nimport { useFormContext, Controller } from 'react-hook-form';\nimport { NOT_SET, BYTES_IN_GB } from 'lib/constants';\nimport { ClusterName, TopicConfigParams, TopicName } from 'redux/interfaces';\nimport { ErrorMessage } from '@hookform/error-message';\nimport Select, { SelectOption } from 'components/common/Select/Select';\nimport Input from 'components/common/Input/Input';\nimport { Button } from 'components/common/Button/Button';\nimport { InputLabel } from 'components/common/Input/InputLabel.styled';\nimport { FormError } from 'components/common/Input/Input.styled';\nimport { StyledForm } from 'components/common/Form/Form.styled';\nimport { clusterTopicPath } from 'lib/paths';\nimport { useNavigate } from 'react-router-dom';\nimport useAppParams from 'lib/hooks/useAppParams';\n\nimport CustomParams from './CustomParams/CustomParams';\nimport TimeToRetain from './TimeToRetain';\nimport * as S from './TopicForm.styled';\n\nexport interface Props {\n  config?: TopicConfigParams;\n  topicName?: TopicName;\n  partitionCount?: number;\n  replicationFactor?: number;\n  inSyncReplicas?: number;\n  retentionBytes?: number;\n  cleanUpPolicy?: string;\n  isEditing?: boolean;\n  isSubmitting: boolean;\n  onSubmit: (e: React.BaseSyntheticEvent) => Promise<void>;\n}\n\nconst CleanupPolicyOptions: Array<SelectOption> = [\n  { value: 'delete', label: 'Delete' },\n  { value: 'compact', label: 'Compact' },\n  { value: 'compact,delete', label: 'Compact,Delete' },\n];\n\nexport const getCleanUpPolicyValue = (cleanUpPolicy?: string) => {\n  if (!cleanUpPolicy) return undefined;\n\n  return CleanupPolicyOptions.find((option: SelectOption) => {\n    return (\n      option.value.toString().replace(/,/g, '_') ===\n      cleanUpPolicy?.toLowerCase()\n    );\n  })?.value.toString();\n};\n\nconst RetentionBytesOptions: Array<SelectOption> = [\n  { value: NOT_SET, label: 'Not Set' },\n  { value: BYTES_IN_GB, label: '1 GB' },\n  { value: BYTES_IN_GB * 10, label: '10 GB' },\n  { value: BYTES_IN_GB * 20, label: '20 GB' },\n  { value: BYTES_IN_GB * 50, label: '50 GB' },\n];\n\nconst TopicForm: React.FC<Props> = ({\n  config,\n  retentionBytes,\n  topicName,\n  isEditing,\n  isSubmitting,\n  onSubmit,\n  cleanUpPolicy,\n}) => {\n  const {\n    control,\n    formState: { errors, isDirty, isValid },\n    reset,\n  } = useFormContext();\n  const navigate = useNavigate();\n  const { clusterName } = useAppParams<{ clusterName: ClusterName }>();\n  const getCleanUpPolicy =\n    getCleanUpPolicyValue(cleanUpPolicy) || CleanupPolicyOptions[0].value;\n\n  const getRetentionBytes =\n    RetentionBytesOptions.find((option: SelectOption) => {\n      return option.value === retentionBytes;\n    })?.value || RetentionBytesOptions[0].value;\n\n  const onCancel = () => {\n    reset();\n    navigate(clusterTopicPath(clusterName, topicName));\n  };\n\n  return (\n    <StyledForm onSubmit={onSubmit} aria-label=\"topic form\">\n      <fieldset disabled={isSubmitting}>\n        <fieldset disabled={isEditing}>\n          <S.Column>\n            <S.NameField>\n              <InputLabel htmlFor=\"topicFormName\">Topic Name *</InputLabel>\n              <Input\n                id=\"topicFormName\"\n                autoFocus\n                name=\"name\"\n                placeholder=\"Topic Name\"\n                defaultValue={topicName}\n                autoComplete=\"off\"\n              />\n              <FormError>\n                <ErrorMessage errors={errors} name=\"name\" />\n              </FormError>\n            </S.NameField>\n          </S.Column>\n\n          <S.Column>\n            {!isEditing && (\n              <div>\n                <InputLabel htmlFor=\"topicFormNumberOfPartitions\">\n                  Number of Partitions *\n                </InputLabel>\n                <Input\n                  id=\"topicFormNumberOfPartitions\"\n                  type=\"number\"\n                  placeholder=\"Number of Partitions\"\n                  min=\"1\"\n                  name=\"partitions\"\n                  positiveOnly\n                  integerOnly\n                />\n                <FormError>\n                  <ErrorMessage errors={errors} name=\"partitions\" />\n                </FormError>\n              </div>\n            )}\n\n            <div>\n              <InputLabel\n                id=\"topicFormCleanupPolicyLabel\"\n                htmlFor=\"topicFormCleanupPolicy\"\n              >\n                Cleanup policy\n              </InputLabel>\n              <Controller\n                defaultValue={CleanupPolicyOptions[0].value}\n                control={control}\n                name=\"cleanupPolicy\"\n                render={({ field: { name, onChange } }) => (\n                  <Select\n                    id=\"topicFormCleanupPolicy\"\n                    aria-labelledby=\"topicFormCleanupPolicyLabel\"\n                    name={name}\n                    value={getCleanUpPolicy}\n                    onChange={onChange}\n                    minWidth=\"250px\"\n                    options={CleanupPolicyOptions}\n                  />\n                )}\n              />\n            </div>\n          </S.Column>\n        </fieldset>\n\n        <S.Column>\n          <div>\n            <InputLabel htmlFor=\"topicFormMinInSyncReplicas\">\n              Min In Sync Replicas\n            </InputLabel>\n            <Input\n              id=\"topicFormMinInSyncReplicas\"\n              type=\"number\"\n              placeholder=\"Min In Sync Replicas\"\n              min=\"1\"\n              name=\"minInSyncReplicas\"\n              positiveOnly\n              integerOnly\n            />\n            <FormError>\n              <ErrorMessage errors={errors} name=\"minInSyncReplicas\" />\n            </FormError>\n          </div>\n          {!isEditing && (\n            <div>\n              <InputLabel htmlFor=\"topicFormReplicationFactor\">\n                Replication Factor\n              </InputLabel>\n              <Input\n                id=\"topicFormReplicationFactor\"\n                type=\"number\"\n                placeholder=\"Replication Factor\"\n                min=\"1\"\n                name=\"replicationFactor\"\n                positiveOnly\n                integerOnly\n              />\n              <FormError>\n                <ErrorMessage errors={errors} name=\"replicationFactor\" />\n              </FormError>\n            </div>\n          )}\n        </S.Column>\n\n        <S.Column>\n          <div>\n            <TimeToRetain isSubmitting={isSubmitting} />\n          </div>\n        </S.Column>\n\n        <S.Column>\n          <div>\n            <InputLabel\n              id=\"topicFormRetentionBytesLabel\"\n              htmlFor=\"topicFormRetentionBytes\"\n            >\n              Max size on disk in GB\n            </InputLabel>\n            <Controller\n              control={control}\n              name=\"retentionBytes\"\n              defaultValue={RetentionBytesOptions[0].value}\n              render={({ field: { name, onChange } }) => (\n                <Select\n                  id=\"topicFormRetentionBytes\"\n                  aria-labelledby=\"topicFormRetentionBytesLabel\"\n                  name={name}\n                  value={getRetentionBytes}\n                  onChange={onChange}\n                  minWidth=\"100%\"\n                  options={RetentionBytesOptions}\n                />\n              )}\n            />\n          </div>\n\n          <div>\n            <InputLabel htmlFor=\"topicFormMaxMessageBytes\">\n              Maximum message size in bytes\n            </InputLabel>\n            <S.MessageSizeInput\n              id=\"topicFormMaxMessageBytes\"\n              type=\"number\"\n              placeholder=\"Maximum message size\"\n              min=\"1\"\n              name=\"maxMessageBytes\"\n              positiveOnly\n              integerOnly\n            />\n            <FormError>\n              <ErrorMessage errors={errors} name=\"maxMessageBytes\" />\n            </FormError>\n          </div>\n        </S.Column>\n\n        <S.CustomParamsHeading>Custom parameters</S.CustomParamsHeading>\n        <CustomParams\n          config={config}\n          isSubmitting={isSubmitting}\n          isEditing={isEditing}\n        />\n        <S.ButtonWrapper>\n          <Button\n            type=\"button\"\n            buttonType=\"secondary\"\n            buttonSize=\"L\"\n            onClick={onCancel}\n          >\n            Cancel\n          </Button>\n          <Button\n            type=\"submit\"\n            buttonType=\"primary\"\n            buttonSize=\"L\"\n            disabled={!isValid || isSubmitting || !isDirty}\n          >\n            {isEditing ? 'Update topic' : 'Create topic'}\n          </Button>\n        </S.ButtonWrapper>\n      </fieldset>\n    </StyledForm>\n  );\n};\n\nexport default TopicForm;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TimeToRetainBtn.spec.tsx",
    "content": "import React, { PropsWithChildren } from 'react';\nimport { render } from 'lib/testHelpers';\nimport { screen } from '@testing-library/react';\nimport TimeToRetainBtn, {\n  Props,\n} from 'components/Topics/shared/Form/TimeToRetainBtn';\nimport { useForm, FormProvider } from 'react-hook-form';\nimport { theme } from 'theme/theme';\nimport userEvent from '@testing-library/user-event';\n\ndescribe('TimeToRetainBtn', () => {\n  const defaultProps: Props = {\n    inputName: 'defaultPropsInputName',\n    text: 'defaultPropsText',\n    value: 0,\n  };\n  const Wrapper: React.FC<PropsWithChildren<unknown>> = ({ children }) => {\n    const methods = useForm();\n    return <FormProvider {...methods}>{children}</FormProvider>;\n  };\n  const SetUpComponent = (props: Partial<Props> = {}) => {\n    const { inputName, text, value } = props;\n    render(\n      <Wrapper>\n        <TimeToRetainBtn\n          inputName={inputName || defaultProps.inputName}\n          text={text || defaultProps.text}\n          value={value || defaultProps.value}\n        />\n      </Wrapper>\n    );\n  };\n\n  describe('Component rendering with its Default Props Setups', () => {\n    beforeEach(() => {\n      SetUpComponent();\n    });\n    it('should test the component rendering on the screen', () => {\n      expect(screen.getByRole('button')).toBeInTheDocument();\n      expect(screen.getByText(defaultProps.text)).toBeInTheDocument();\n    });\n    it('should test the non active state of the button and its styling', () => {\n      const buttonElement = screen.getByRole('button');\n      expect(buttonElement).toHaveStyle(\n        `background-color:${theme.chips.backgroundColor.normal}`\n      );\n      expect(buttonElement).toHaveStyle(`border:none`);\n    });\n    it('should test the non active state with click becoming active', async () => {\n      const buttonElement = screen.getByRole('button');\n      await userEvent.click(buttonElement);\n      expect(buttonElement).toHaveStyle(\n        `background-color:${theme.chips.backgroundColor.active}`\n      );\n      expect(buttonElement).toHaveStyle(`border:none`);\n    });\n  });\n\n  describe('Component rendering with its Default Props Setups', () => {\n    it('should test the active state of the button and its styling', () => {\n      SetUpComponent({ value: 604800000 });\n      const buttonElement = screen.getByRole('button');\n      expect(buttonElement).toHaveStyle(\n        `background-color:${theme.button.secondary.invertedColors.normal}`\n      );\n      expect(buttonElement).toHaveStyle(`border:none`);\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TimeToRetainBtns.spec.tsx",
    "content": "import React, { PropsWithChildren } from 'react';\nimport { render } from 'lib/testHelpers';\nimport { screen } from '@testing-library/react';\nimport TimeToRetainBtns, {\n  Props,\n} from 'components/Topics/shared/Form/TimeToRetainBtns';\nimport { FormProvider, useForm } from 'react-hook-form';\n\ndescribe('TimeToRetainBtns', () => {\n  const defaultProps: Props = {\n    name: 'defaultPropsTestingName',\n    value: 'defaultPropsValue',\n  };\n  const Wrapper: React.FC<PropsWithChildren<unknown>> = ({ children }) => {\n    const methods = useForm();\n    return <FormProvider {...methods}>{children}</FormProvider>;\n  };\n  const SetUpComponent = (props: Props = defaultProps) => {\n    const { name, value } = props;\n\n    render(\n      <Wrapper>\n        <TimeToRetainBtns name={name} value={value} />\n      </Wrapper>\n    );\n  };\n\n  it('should test the normal view rendering of the component', () => {\n    SetUpComponent();\n    expect(screen.getAllByRole('button')).toHaveLength(5);\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.spec.tsx",
    "content": "import React, { PropsWithChildren } from 'react';\nimport { render } from 'lib/testHelpers';\nimport { screen } from '@testing-library/dom';\nimport { FormProvider, useForm } from 'react-hook-form';\nimport TopicForm, { Props } from 'components/Topics/shared/Form/TopicForm';\nimport userEvent from '@testing-library/user-event';\nimport { act } from 'react-dom/test-utils';\n\nconst isSubmitting = false;\nconst onSubmit = jest.fn();\n\nconst renderComponent = (props: Props = { isSubmitting, onSubmit }) => {\n  const Wrapper: React.FC<PropsWithChildren<unknown>> = ({ children }) => {\n    const methods = useForm();\n    return <FormProvider {...methods}>{children}</FormProvider>;\n  };\n\n  return render(\n    <Wrapper>\n      <TopicForm {...props} />\n    </Wrapper>\n  );\n};\n\nconst expectByRoleAndNameToBeInDocument = (\n  role: string,\n  accessibleName: string\n) => {\n  expect(screen.getByRole(role, { name: accessibleName })).toBeInTheDocument();\n};\n\ndescribe('TopicForm', () => {\n  it('renders', async () => {\n    await act(async () => {\n      renderComponent();\n    });\n\n    expectByRoleAndNameToBeInDocument('textbox', 'Topic Name *');\n\n    expectByRoleAndNameToBeInDocument('spinbutton', 'Number of Partitions *');\n    expectByRoleAndNameToBeInDocument('spinbutton', 'Replication Factor');\n\n    expectByRoleAndNameToBeInDocument('spinbutton', 'Min In Sync Replicas');\n    expectByRoleAndNameToBeInDocument('listbox', 'Cleanup policy');\n\n    expectByRoleAndNameToBeInDocument(\n      'spinbutton',\n      'Time to retain data (in ms)'\n    );\n    expectByRoleAndNameToBeInDocument('button', '12 hours');\n    expectByRoleAndNameToBeInDocument('button', '2 days');\n    expectByRoleAndNameToBeInDocument('button', '7 days');\n    expectByRoleAndNameToBeInDocument('button', '4 weeks');\n\n    expectByRoleAndNameToBeInDocument('listbox', 'Max size on disk in GB');\n    expectByRoleAndNameToBeInDocument(\n      'spinbutton',\n      'Maximum message size in bytes'\n    );\n\n    expectByRoleAndNameToBeInDocument('heading', 'Custom parameters');\n\n    expectByRoleAndNameToBeInDocument('button', 'Create topic');\n  });\n\n  it('submits', async () => {\n    await act(async () => {\n      renderComponent({\n        isSubmitting,\n        onSubmit: onSubmit.mockImplementation((e) => e.preventDefault()),\n      });\n    });\n\n    await userEvent.type(\n      screen.getByPlaceholderText('Topic Name'),\n      'topicName'\n    );\n    await userEvent.click(screen.getByRole('button', { name: 'Create topic' }));\n\n    expect(onSubmit).toBeCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.styled.spec.tsx",
    "content": "import React from 'react';\nimport { render } from 'lib/testHelpers';\nimport * as S from 'components/Topics/shared/Form/TopicForm.styled';\nimport { screen } from '@testing-library/react';\nimport { theme } from 'theme/theme';\n\ndescribe('TopicForm styled components', () => {\n  describe('Button', () => {\n    it('should check the button styling in isActive state', () => {\n      render(<S.Button isActive />);\n      const button = screen.getByRole('button');\n      expect(button).toHaveStyle({\n        border: `none`,\n        backgroundColor: theme.chips.backgroundColor.active,\n      });\n    });\n\n    it('should check the button styling in non Active state', () => {\n      render(<S.Button isActive={false} />);\n      const button = screen.getByRole('button');\n      expect(button).toHaveStyle({\n        border: `none`,\n        backgroundColor: theme.chips.backgroundColor.normal,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Version/Version.styled.ts",
    "content": "import styled, { css } from 'styled-components';\n\nexport const Wrapper = styled.div`\n  display: flex;\n  align-items: baseline;\n`;\n\nconst textStyle = css`\n  font-family: Inter, sans-serif;\n  font-style: normal;\n  font-weight: normal;\n  font-size: 14px;\n  line-height: 20px;\n`;\n\nexport const CurrentVersion = styled.span(\n  ({ theme }) => css`\n    ${textStyle};\n    color: ${theme.version.currentVersion.color};\n    margin-left: 0.25rem;\n  `\n);\n\nexport const OutdatedWarning = styled.span`\n  ${textStyle}\n`;\n\nexport const CurrentCommitLink = styled.a(\n  ({ theme }) => css`\n    ${textStyle};\n    color: ${theme.version.commitLink.color};\n    margin-left: 0.25rem;\n    &:hover {\n      color: ${theme.version.commitLink.color};\n    }\n  `\n);\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Version/Version.tsx",
    "content": "import React from 'react';\nimport WarningIcon from 'components/common/Icons/WarningIcon';\nimport { gitCommitPath } from 'lib/paths';\nimport { useLatestVersion } from 'lib/hooks/api/latestVersion';\nimport { formatTimestamp } from 'lib/dateTimeHelpers';\n\nimport * as S from './Version.styled';\n\nconst Version: React.FC = () => {\n  const { data: latestVersionInfo = {} } = useLatestVersion();\n  const { buildTime, commitId, isLatestRelease, version } =\n    latestVersionInfo.build;\n  const { versionTag } = latestVersionInfo?.latestRelease || '';\n\n  const currentVersion =\n    isLatestRelease && version?.match(versionTag)\n      ? versionTag\n      : formatTimestamp(buildTime);\n\n  return (\n    <S.Wrapper>\n      {!isLatestRelease && (\n        <S.OutdatedWarning\n          title={`Your app version is outdated. Latest version is ${\n            versionTag || 'UNKNOWN'\n          }`}\n        >\n          <WarningIcon />\n        </S.OutdatedWarning>\n      )}\n\n      {commitId && (\n        <div>\n          <S.CurrentCommitLink\n            title=\"Current commit\"\n            target=\"__blank\"\n            href={gitCommitPath(commitId)}\n          >\n            {commitId}\n          </S.CurrentCommitLink>\n        </div>\n      )}\n      <S.CurrentVersion>{currentVersion}</S.CurrentVersion>\n    </S.Wrapper>\n  );\n};\n\nexport default Version;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Version/__tests__/Version.spec.tsx",
    "content": "import React from 'react';\nimport { screen } from '@testing-library/dom';\nimport Version from 'components/Version/Version';\nimport { render } from 'lib/testHelpers';\nimport { useLatestVersion } from 'lib/hooks/api/latestVersion';\nimport {\n  deprecatedVersionPayload,\n  latestVersionPayload,\n} from 'lib/fixtures/latestVersion';\n\njest.mock('lib/hooks/api/latestVersion', () => ({\n  useLatestVersion: jest.fn(),\n}));\ndescribe('Version Component', () => {\n  const commitId = '96a577a';\n\n  describe('render latest version', () => {\n    beforeEach(() => {\n      (useLatestVersion as jest.Mock).mockImplementation(() => ({\n        data: latestVersionPayload,\n      }));\n    });\n    it('renders latest release version as current version', async () => {\n      render(<Version />);\n      expect(screen.getByText(commitId)).toBeInTheDocument();\n    });\n\n    it('should not show warning icon if it is last release', async () => {\n      render(<Version />);\n      expect(screen.queryByRole('img')).not.toBeInTheDocument();\n    });\n  });\n\n  it('show warning icon if it is not last release', async () => {\n    (useLatestVersion as jest.Mock).mockImplementation(() => ({\n      data: deprecatedVersionPayload,\n    }));\n    render(<Version />);\n    expect(screen.getByRole('img')).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Version/__tests__/compareVersions.spec.ts",
    "content": "import compareVersions from 'components/Version/compareVersions';\n\nconst runTests = (dataSet: [string, string, number][]) => {\n  dataSet.forEach(([v1, v2, expected]) => {\n    expect(compareVersions(v1, v2)).toEqual(expected);\n  });\n};\n\ndescribe('compareVersions function', () => {\n  it('single-segment versions', () => {\n    runTests([\n      ['10', '9', 1],\n      ['10', '10', 0],\n      ['9', '10', -1],\n    ]);\n  });\n\n  it('two-segment versions', () => {\n    runTests([\n      ['10.8', '10.4', 1],\n      ['10.1', '10.2', -1],\n      ['10.1', '10.1', 0],\n    ]);\n  });\n\n  it('three-segment versions', () => {\n    runTests([\n      ['10.1.1', '10.2.2', -1],\n      ['10.1.8', '10.0.4', 1],\n      ['10.0.1', '10.0.1', 0],\n    ]);\n  });\n\n  it('different number of digits in same group', () => {\n    runTests([\n      ['11.0.10', '11.0.2', 1],\n      ['11.0.2', '11.0.10', -1],\n    ]);\n  });\n\n  it('different number of digits in different groups', () => {\n    expect(compareVersions('11.1.10', '11.0')).toEqual(1);\n  });\n\n  it('different number of digits', () => {\n    runTests([\n      ['1.1.1', '1', 1],\n      ['1.0.0', '1', 0],\n      ['1.0', '1.4.1', -1],\n    ]);\n  });\n\n  it('ignore non-numeric characters', () => {\n    runTests([\n      ['1.0.0-alpha.1', '1.0.0-alpha', 0],\n      ['1.0.0-rc', '1.0.0', 0],\n      ['1.0.0-alpha', '1', 0],\n      ['v1.0.0', '1.0.0', 0],\n    ]);\n  });\n\n  it('returns valid result (negative test cases)', () => {\n    expect(compareVersions()).toEqual(0);\n    expect(compareVersions('v0.0.0')).toEqual(0);\n    // @ts-expect-error first arg is number\n    expect(compareVersions(123, 'v0.0.0')).toEqual(0);\n    // @ts-expect-error second arg is number\n    expect(compareVersions('v0.0.0', 123)).toEqual(0);\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/Version/compareVersions.ts",
    "content": "const split = (v: string): string[] => {\n  const c = v.replace('v', '').split('-')[0];\n  return c.split('.');\n};\n\nconst compareVersions = (v1?: string, v2?: string): number => {\n  if (!v1 || !v2) return 0;\n\n  // try..catch - is our safeguard for strange git tags (or usecases without network)\n  try {\n    const s1 = split(v1);\n    const s2 = split(v2);\n\n    for (let i = 0; i < Math.max(s1.length, s2.length); i += 1) {\n      const n1 = parseInt(s1[i] || '0', 10);\n      const n2 = parseInt(s2[i] || '0', 10);\n\n      if (n1 > n2) return 1;\n      if (n2 > n1) return -1;\n    }\n\n    return 0;\n  } catch (_) {\n    return 0;\n  }\n};\n\nexport default compareVersions;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/__tests__/App.spec.tsx",
    "content": "import React from 'react';\nimport { screen } from '@testing-library/react';\nimport App from 'components/App';\nimport { render } from 'lib/testHelpers';\nimport { useGetUserInfo } from 'lib/hooks/api/roles';\nimport { useAppInfo } from 'lib/hooks/api/appConfig';\n\njest.mock('components/Nav/Nav', () => () => <div>Navigation</div>);\n\njest.mock('components/Version/Version', () => () => <div>Version</div>);\n\njest.mock('components/NavBar/NavBar', () => () => <div>NavBar</div>);\n\njest.mock('lib/hooks/api/roles', () => ({\n  useGetUserInfo: jest.fn(),\n}));\njest.mock('lib/hooks/api/appConfig', () => ({\n  useAppInfo: jest.fn(),\n}));\n\ndescribe('App', () => {\n  beforeEach(() => {\n    (useGetUserInfo as jest.Mock).mockImplementation(() => ({\n      data: {},\n    }));\n    (useAppInfo as jest.Mock).mockImplementation(() => ({\n      data: {},\n    }));\n\n    render(<App />, {\n      initialEntries: ['/'],\n    });\n  });\n\n  it('Renders navigation', async () => {\n    expect(screen.getByText('Navigation')).toBeInTheDocument();\n  });\n\n  it('Renders NavBar', async () => {\n    expect(screen.getByText('NavBar')).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ActionComponent/ActionButton/ActionButton.tsx",
    "content": "import React from 'react';\nimport { Props as ButtonProps } from 'components/common/Button/Button';\nimport { ActionComponentProps } from 'components/common/ActionComponent/ActionComponent';\nimport { Action } from 'generated-sources';\nimport ActionPermissionButton from 'components/common/ActionComponent/ActionButton/ActionPermissionButton/ActionPermissionButton';\nimport ActionCreateButton from 'components/common/ActionComponent/ActionButton//ActionCreateButton/ActionCreateButton';\n\ninterface Props extends ActionComponentProps, ButtonProps {}\n\nconst ActionButton: React.FC<Props> = ({ permission, ...props }) => {\n  return permission.action === Action.CREATE ? (\n    <ActionCreateButton permission={permission} {...props} />\n  ) : (\n    <ActionPermissionButton permission={permission} {...props} />\n  );\n};\n\nexport default ActionButton;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ActionComponent/ActionButton/ActionCanButton/ActionCanButton.tsx",
    "content": "import React from 'react';\nimport { Button, Props as ButtonProps } from 'components/common/Button/Button';\nimport * as S from 'components/common/ActionComponent/ActionComponent.styled';\nimport {\n  ActionComponentProps,\n  getDefaultActionMessage,\n} from 'components/common/ActionComponent/ActionComponent';\nimport { useActionTooltip } from 'lib/hooks/useActionTooltip';\n\ninterface Props extends Omit<ActionComponentProps, 'permission'>, ButtonProps {\n  canDoAction: boolean;\n}\n\nconst ActionButton: React.FC<Props> = ({\n  placement = 'bottom-end',\n  message = getDefaultActionMessage(),\n  disabled,\n  canDoAction,\n  ...props\n}) => {\n  const isDisabled = !canDoAction;\n\n  const { x, y, reference, floating, strategy, open } = useActionTooltip(\n    isDisabled,\n    placement\n  );\n\n  return (\n    <S.Wrapper ref={reference}>\n      <Button {...props} disabled={disabled || isDisabled} />\n      {open && (\n        <S.MessageTooltipLimited\n          ref={floating}\n          style={{\n            position: strategy,\n            top: y ?? 0,\n            left: x ?? 0,\n            width: 'max-content',\n          }}\n        >\n          {message}\n        </S.MessageTooltipLimited>\n      )}\n    </S.Wrapper>\n  );\n};\n\nexport default ActionButton;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ActionComponent/ActionButton/ActionCanButton/__tests__/ActionCanButton.spec.tsx",
    "content": "import React from 'react';\nimport { render } from 'lib/testHelpers';\nimport ActionCanButton from 'components/common/ActionComponent/ActionButton/ActionCanButton/ActionCanButton';\nimport { screen } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { getDefaultActionMessage } from 'components/common/ActionComponent/ActionComponent';\n\ndescribe('ActionButton', () => {\n  const tooltipText = getDefaultActionMessage();\n\n  it('should render the button with the correct text, for the permission tooltip not to show', async () => {\n    render(\n      <ActionCanButton buttonType=\"primary\" buttonSize=\"M\" canDoAction>\n        test\n      </ActionCanButton>\n    );\n    const button = screen.getByRole('button', { name: 'test' });\n    expect(button).toBeInTheDocument();\n    expect(button).toBeEnabled();\n    await userEvent.hover(button);\n    expect(screen.queryByText(tooltipText)).not.toBeInTheDocument();\n  });\n\n  it('should make the button disable and view the tooltip with the default text', async () => {\n    render(\n      <ActionCanButton buttonType=\"primary\" buttonSize=\"M\" canDoAction={false}>\n        test\n      </ActionCanButton>\n    );\n    const button = screen.getByRole('button');\n    expect(button).toBeDisabled();\n    expect(screen.queryByText(tooltipText)).not.toBeInTheDocument();\n    await userEvent.hover(button);\n    expect(screen.getByText(tooltipText)).toBeInTheDocument();\n  });\n\n  it('should make the button disable and view the tooltip with the given text', async () => {\n    const customTooltipText = 'something here';\n\n    render(\n      <ActionCanButton\n        buttonType=\"primary\"\n        buttonSize=\"M\"\n        canDoAction={false}\n        message={customTooltipText}\n      />\n    );\n    const button = screen.getByRole('button');\n    expect(button).toBeDisabled();\n    expect(screen.queryByText(customTooltipText)).not.toBeInTheDocument();\n    await userEvent.hover(button);\n    expect(screen.getByText(customTooltipText)).toBeInTheDocument();\n    await userEvent.unhover(button);\n    expect(screen.queryByText(customTooltipText)).not.toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ActionComponent/ActionButton/ActionCreateButton/ActionCreateButton.tsx",
    "content": "import React from 'react';\nimport { Props as ButtonProps } from 'components/common/Button/Button';\nimport { ActionComponentProps } from 'components/common/ActionComponent/ActionComponent';\nimport { useCreatePermission } from 'lib/hooks/useCreatePermisson';\nimport ActionCanButton from 'components/common/ActionComponent/ActionButton/ActionCanButton/ActionCanButton';\n\ninterface Props extends ActionComponentProps, ButtonProps {}\n\nconst ActionCreateButton: React.FC<Props> = ({ permission, ...props }) => {\n  const canDoAction = useCreatePermission(permission.resource);\n\n  return <ActionCanButton canDoAction={canDoAction} {...props} />;\n};\n\nexport default ActionCreateButton;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ActionComponent/ActionButton/ActionCreateButton/__tests__/ActionCreateButton.spec.tsx",
    "content": "import React from 'react';\nimport { render } from 'lib/testHelpers';\nimport ActionCreateButton from 'components/common/ActionComponent/ActionButton/ActionCreateButton/ActionCreateButton';\nimport { screen } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { getDefaultActionMessage } from 'components/common/ActionComponent/ActionComponent';\nimport { useParams } from 'react-router-dom';\nimport {\n  clusterName,\n  validPermission,\n  invalidPermission,\n  tooltipIsShowing,\n  userInfoRbacEnabled,\n} from 'components/common/ActionComponent/__tests__/fixtures';\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useParams: jest.fn(),\n}));\n\ndescribe('ActionCreateButton', () => {\n  const tooltipText = getDefaultActionMessage();\n\n  beforeEach(() => {\n    (useParams as jest.Mock).mockImplementation(() => ({\n      clusterName,\n    }));\n  });\n\n  it('should render the button with the correct text, for the permission tooltip not to show', async () => {\n    render(\n      <ActionCreateButton\n        buttonType=\"primary\"\n        buttonSize=\"M\"\n        permission={validPermission}\n      >\n        test\n      </ActionCreateButton>,\n      {\n        userInfo: userInfoRbacEnabled,\n      }\n    );\n    const button = screen.getByRole('button', { name: 'test' });\n    expect(button).toBeInTheDocument();\n    expect(button).toBeEnabled();\n    await userEvent.hover(button);\n    expect(screen.queryByText(tooltipText)).not.toBeInTheDocument();\n  });\n\n  it('should make the button disable and view the tooltip with the default text', async () => {\n    render(\n      <ActionCreateButton\n        buttonType=\"primary\"\n        buttonSize=\"M\"\n        permission={invalidPermission}\n      >\n        test\n      </ActionCreateButton>,\n      { userInfo: userInfoRbacEnabled }\n    );\n    const button = screen.getByRole('button');\n    expect(button).toBeDisabled();\n    await tooltipIsShowing(button, tooltipText);\n  });\n\n  it('should make the button disable and view the tooltip with the given text', async () => {\n    const customTooltipText = 'something here';\n\n    render(\n      <ActionCreateButton\n        buttonType=\"primary\"\n        buttonSize=\"M\"\n        permission={invalidPermission}\n        message={customTooltipText}\n      />,\n      { userInfo: userInfoRbacEnabled }\n    );\n    const button = screen.getByRole('button');\n    expect(button).toBeDisabled();\n    await tooltipIsShowing(button, customTooltipText);\n  });\n\n  it('should render the Button but disabled cause the given role is not correct', async () => {\n    render(\n      <ActionCreateButton\n        buttonType=\"primary\"\n        buttonSize=\"M\"\n        permission={invalidPermission}\n      />,\n      {\n        userInfo: userInfoRbacEnabled,\n      }\n    );\n    const button = screen.getByRole('button');\n    expect(button).toBeDisabled();\n    await tooltipIsShowing(button, tooltipText);\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ActionComponent/ActionButton/ActionPermissionButton/ActionPermissionButton.tsx",
    "content": "import React from 'react';\nimport { Props as ButtonProps } from 'components/common/Button/Button';\nimport { ActionComponentProps } from 'components/common/ActionComponent/ActionComponent';\nimport { usePermission } from 'lib/hooks/usePermission';\nimport ActionCanButton from 'components/common/ActionComponent/ActionButton/ActionCanButton/ActionCanButton';\n\ninterface Props extends ActionComponentProps, ButtonProps {}\n\nconst ActionPermissionButton: React.FC<Props> = ({ permission, ...props }) => {\n  const canDoAction = usePermission(\n    permission.resource,\n    permission.action,\n    permission.value\n  );\n\n  return <ActionCanButton canDoAction={canDoAction} {...props} />;\n};\n\nexport default ActionPermissionButton;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ActionComponent/ActionButton/ActionPermissionButton/__tests__/ActionPermissionButton.spec.tsx",
    "content": "import React from 'react';\nimport { render } from 'lib/testHelpers';\nimport ActionPermissionButton from 'components/common/ActionComponent/ActionButton/ActionPermissionButton/ActionPermissionButton';\nimport { screen } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { getDefaultActionMessage } from 'components/common/ActionComponent/ActionComponent';\nimport { useParams } from 'react-router-dom';\nimport {\n  clusterName,\n  validPermission,\n  invalidPermission,\n  tooltipIsShowing,\n  userInfoRbacEnabled,\n} from 'components/common/ActionComponent/__tests__/fixtures';\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useParams: jest.fn(),\n}));\n\ndescribe('ActionPermissionButton', () => {\n  const tooltipText = getDefaultActionMessage();\n\n  beforeEach(() => {\n    (useParams as jest.Mock).mockImplementation(() => ({\n      clusterName,\n    }));\n  });\n\n  it('should render the button with the correct text, for the permission tooltip not to show', async () => {\n    render(\n      <ActionPermissionButton\n        buttonType=\"primary\"\n        buttonSize=\"M\"\n        permission={validPermission}\n      >\n        test\n      </ActionPermissionButton>,\n      {\n        userInfo: userInfoRbacEnabled,\n      }\n    );\n    const button = screen.getByRole('button', { name: 'test' });\n    expect(button).toBeInTheDocument();\n    expect(button).toBeEnabled();\n    await userEvent.hover(button);\n    expect(screen.queryByText(tooltipText)).not.toBeInTheDocument();\n  });\n\n  it('should make the button disable and view the tooltip with the default text', async () => {\n    render(\n      <ActionPermissionButton\n        buttonType=\"primary\"\n        buttonSize=\"M\"\n        permission={invalidPermission}\n      >\n        test\n      </ActionPermissionButton>,\n      { userInfo: userInfoRbacEnabled }\n    );\n    const button = screen.getByRole('button');\n    expect(button).toBeDisabled();\n    await tooltipIsShowing(button, tooltipText);\n  });\n\n  it('should make the button disable and view the tooltip with the given text', async () => {\n    const customTooltipText = 'something here';\n\n    render(\n      <ActionPermissionButton\n        buttonType=\"primary\"\n        buttonSize=\"M\"\n        permission={invalidPermission}\n        message={customTooltipText}\n      />,\n      { userInfo: userInfoRbacEnabled }\n    );\n    const button = screen.getByRole('button');\n    expect(button).toBeDisabled();\n    await tooltipIsShowing(button, customTooltipText);\n  });\n\n  it('should render the Button but disabled cause the given role is not correct', async () => {\n    render(\n      <ActionPermissionButton\n        buttonType=\"primary\"\n        buttonSize=\"M\"\n        permission={invalidPermission}\n      />,\n      {\n        userInfo: userInfoRbacEnabled,\n      }\n    );\n    const button = screen.getByRole('button');\n    expect(button).toBeDisabled();\n    await tooltipIsShowing(button, tooltipText);\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ActionComponent/ActionButton/__tests__/ActionButton.spec.tsx",
    "content": "import React from 'react';\nimport { screen } from '@testing-library/react';\nimport ActionButton from 'components/common/ActionComponent/ActionButton/ActionButton';\nimport { render } from 'lib/testHelpers';\nimport { Action, ResourceType } from 'generated-sources';\n\nconst createText = 'create';\nconst otherText = 'create';\n\njest.mock(\n  'components/common/ActionComponent/ActionButton/ActionCreateButton/ActionCreateButton',\n  () => () => <div>{createText}</div>\n);\n\njest.mock(\n  'components/common/ActionComponent/ActionButton/ActionPermissionButton/ActionPermissionButton',\n  () => () => <div>{otherText}</div>\n);\n\ndescribe('ActionButton', () => {\n  it('should check when passes action create it renders create component', () => {\n    render(\n      <ActionButton\n        permission={{\n          action: Action.CREATE,\n          resource: ResourceType.CONNECT,\n        }}\n        buttonType=\"secondary\"\n        buttonSize=\"S\"\n      />\n    );\n    expect(screen.getByText(createText)).toBeInTheDocument();\n  });\n\n  it('should check when passes other actions types it renders Others component', () => {\n    render(\n      <ActionButton\n        permission={{\n          action: Action.EDIT,\n          resource: ResourceType.CONNECT,\n        }}\n        buttonType=\"secondary\"\n        buttonSize=\"S\"\n      />\n    );\n    expect(screen.getByText(createText)).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ActionComponent/ActionComponent.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const Wrapper = styled.div`\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: center;\n`;\n\nexport const MessageTooltip = styled.div`\n  background-color: ${({ theme }) => theme.tooltip.bg};\n  color: ${({ theme }) => theme.tooltip.text};\n  border-radius: 6px;\n  padding: 5px;\n  z-index: 1;\n  white-space: pre-wrap;\n`;\n\nexport const MessageTooltipLimited = styled(MessageTooltip)`\n  max-width: 100%;\n  max-height: 100%;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ActionComponent/ActionComponent.ts",
    "content": "import { Placement } from '@floating-ui/react';\nimport { Action, ResourceType } from 'generated-sources';\n\nexport interface ActionComponentProps {\n  permission: {\n    resource: ResourceType;\n    action: Action | Array<Action>;\n    value?: string;\n  };\n  message?: string;\n  placement?: Placement;\n}\n\nexport function getDefaultActionMessage() {\n  return \"You don't have a required permission to perform this action\";\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ActionComponent/ActionDropDownItem/ActionDropdownItem.tsx",
    "content": "import React from 'react';\nimport * as S from 'components/common/ActionComponent/ActionComponent.styled';\nimport {\n  ActionComponentProps,\n  getDefaultActionMessage,\n} from 'components/common/ActionComponent/ActionComponent';\nimport { useActionTooltip } from 'lib/hooks/useActionTooltip';\nimport { usePermission } from 'lib/hooks/usePermission';\nimport { DropdownItemProps } from 'components/common/Dropdown/DropdownItem';\nimport { DropdownItem } from 'components/common/Dropdown';\n\ninterface Props extends ActionComponentProps, DropdownItemProps {}\n\nconst ActionDropdownItem: React.FC<Props> = ({\n  permission,\n  message = getDefaultActionMessage(),\n  placement = 'left',\n  children,\n  disabled,\n  ...props\n}) => {\n  const canDoAction = usePermission(\n    permission.resource,\n    permission.action,\n    permission.value\n  );\n\n  const isDisabled = !canDoAction;\n\n  const { x, y, reference, floating, strategy, open } = useActionTooltip(\n    isDisabled,\n    placement\n  );\n\n  return (\n    <>\n      <DropdownItem\n        {...props}\n        disabled={disabled || isDisabled}\n        ref={reference}\n      >\n        {children}\n      </DropdownItem>\n      {open && (\n        <S.MessageTooltip\n          ref={floating}\n          style={{\n            position: strategy,\n            top: y ?? 0,\n            left: x ?? 0,\n          }}\n        >\n          {message}\n        </S.MessageTooltip>\n      )}\n    </>\n  );\n};\n\nexport default ActionDropdownItem;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ActionComponent/ActionNavLink/ActionNavLink.tsx",
    "content": "import React from 'react';\nimport {\n  ActionComponentProps,\n  getDefaultActionMessage,\n} from 'components/common/ActionComponent/ActionComponent';\nimport { NavLink, NavLinkProps } from 'react-router-dom';\nimport * as S from 'components/common/ActionComponent/ActionComponent.styled';\nimport { useActionTooltip } from 'lib/hooks/useActionTooltip';\nimport { usePermission } from 'lib/hooks/usePermission';\n\ninterface Props extends ActionComponentProps, NavLinkProps {}\n\nconst ActionNavLink: React.FC<Props> = ({\n  message = getDefaultActionMessage(),\n  placement,\n  children,\n  permission,\n  className,\n  ...props\n}) => {\n  const canDoAction = usePermission(\n    permission.resource,\n    permission.action,\n    permission.value\n  );\n\n  const isDisabled = !canDoAction;\n\n  const { x, y, reference, floating, strategy, open } = useActionTooltip(\n    isDisabled,\n    placement\n  );\n\n  return (\n    <>\n      <NavLink\n        {...props}\n        ref={reference}\n        className={isDisabled ? 'is-disabled' : className}\n        aria-disabled={isDisabled}\n        onClick={(event) => (isDisabled ? event.preventDefault() : null)}\n      >\n        {children}\n      </NavLink>\n      {open && (\n        <S.MessageTooltipLimited\n          ref={floating}\n          style={{\n            position: strategy,\n            top: y ?? 0,\n            left: x ?? 0,\n            width: 'max-content',\n          }}\n        >\n          {message}\n        </S.MessageTooltipLimited>\n      )}\n    </>\n  );\n};\n\nexport default ActionNavLink;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ActionComponent/ActionNavLink/__tests__/ActionNavLink.spec.tsx",
    "content": "import React from 'react';\nimport { screen } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { render } from 'lib/testHelpers';\nimport ActionNavLink from 'components/common/ActionComponent/ActionNavLink/ActionNavLink';\nimport { getDefaultActionMessage } from 'components/common/ActionComponent/ActionComponent';\nimport { useParams } from 'react-router-dom';\nimport {\n  clusterName,\n  validPermission,\n  invalidPermission,\n  tooltipIsShowing,\n  userInfoRbacEnabled,\n} from 'components/common/ActionComponent/__tests__/fixtures';\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useParams: jest.fn(),\n}));\n\ndescribe('ActionNavLink', () => {\n  const tooltipText = getDefaultActionMessage();\n\n  beforeEach(() => {\n    (useParams as jest.Mock).mockImplementation(() => ({\n      clusterName,\n    }));\n  });\n\n  it('should render the button with the correct text, for the permission tooltip not to show', async () => {\n    render(\n      <ActionNavLink to=\"/\" permission={validPermission}>\n        test\n      </ActionNavLink>,\n      {\n        userInfo: userInfoRbacEnabled,\n      }\n    );\n    const link = screen.getByRole('link', { name: 'test' });\n    expect(link).toBeInTheDocument();\n    await userEvent.hover(link);\n    expect(screen.queryByText(tooltipText)).not.toBeInTheDocument();\n  });\n\n  it('should make the button disable and view the tooltip with the default text', async () => {\n    render(\n      <ActionNavLink to=\"/\" permission={invalidPermission}>\n        test\n      </ActionNavLink>,\n      {\n        userInfo: userInfoRbacEnabled,\n      }\n    );\n    const link = screen.getByRole('link');\n    expect(link).toHaveAttribute('aria-disabled');\n\n    await tooltipIsShowing(link, tooltipText);\n  });\n\n  it('should make the button disable and view the tooltip with the given text', async () => {\n    const customTooltipText = 'something here';\n\n    render(\n      <ActionNavLink\n        to=\"/\"\n        permission={invalidPermission}\n        message={customTooltipText}\n      />,\n      {\n        userInfo: userInfoRbacEnabled,\n      }\n    );\n    const button = screen.getByRole('link');\n    expect(button).toHaveAttribute('aria-disabled');\n\n    await tooltipIsShowing(button, customTooltipText);\n  });\n\n  it('should render the Link with the correct text, but disabled cause the given role is not correct', async () => {\n    render(\n      <ActionNavLink to=\"/\" permission={invalidPermission}>\n        test\n      </ActionNavLink>,\n      {\n        userInfo: userInfoRbacEnabled,\n      }\n    );\n    const button = screen.getByRole('link');\n    expect(button).toHaveAttribute('aria-disabled');\n\n    await tooltipIsShowing(button, tooltipText);\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ActionComponent/ActionSelect/ActionSelect.tsx",
    "content": "import React from 'react';\nimport Select, { SelectProps } from 'components/common/Select/Select';\nimport {\n  ActionComponentProps,\n  getDefaultActionMessage,\n} from 'components/common/ActionComponent/ActionComponent';\nimport { useActionTooltip } from 'lib/hooks/useActionTooltip';\nimport { usePermission } from 'lib/hooks/usePermission';\nimport * as S from 'components/common/ActionComponent/ActionComponent.styled';\n\ninterface Props extends SelectProps, ActionComponentProps {}\n\nconst ActionSelect: React.FC<Props> = ({\n  message = getDefaultActionMessage(),\n  permission,\n  placement = 'bottom',\n  disabled,\n  ...props\n}) => {\n  const canDoAction = usePermission(\n    permission.resource,\n    permission.action,\n    permission.value\n  );\n\n  const isDisabled = !canDoAction;\n\n  const { x, y, reference, floating, strategy, open } = useActionTooltip(\n    isDisabled,\n    placement\n  );\n\n  return (\n    <>\n      <div ref={reference}>\n        <Select {...props} disabled={disabled || isDisabled} />\n      </div>\n      {open && (\n        <S.MessageTooltipLimited\n          ref={floating}\n          style={{\n            position: strategy,\n            top: y ?? 0,\n            left: x ?? 0,\n            width: 'max-content',\n          }}\n        >\n          {message}\n        </S.MessageTooltipLimited>\n      )}\n    </>\n  );\n};\n\nexport default ActionSelect;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ActionComponent/ActionSelect/__tests__/ActionSelect.spec.tsx",
    "content": "import React from 'react';\nimport { screen } from '@testing-library/react';\nimport { render } from 'lib/testHelpers';\nimport ActionSelect from 'components/common/ActionComponent/ActionSelect/ActionSelect';\nimport { getDefaultActionMessage } from 'components/common/ActionComponent/ActionComponent';\nimport {\n  clusterName,\n  validPermission,\n  invalidPermission,\n  tooltipIsShowing,\n  userInfoRbacEnabled,\n} from 'components/common/ActionComponent/__tests__/fixtures';\nimport { useParams } from 'react-router-dom';\nimport userEvent from '@testing-library/user-event';\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useParams: jest.fn(),\n}));\n\ndescribe('ActionSelect', () => {\n  const tooltipText = getDefaultActionMessage();\n\n  beforeEach(() => {\n    (useParams as jest.Mock).mockImplementation(() => ({\n      clusterName,\n    }));\n  });\n\n  it('should render the button with the correct text, for the permission tooltip not to show', async () => {\n    render(<ActionSelect permission={validPermission} />, {\n      userInfo: userInfoRbacEnabled,\n    });\n    const list = screen.getByRole('listbox');\n    expect(list).toBeInTheDocument();\n    await userEvent.hover(list);\n    expect(screen.queryByText(tooltipText)).not.toBeInTheDocument();\n  });\n\n  it('should make the button disable and view the tooltip with the default text', async () => {\n    render(<ActionSelect permission={invalidPermission} />, {\n      userInfo: userInfoRbacEnabled,\n    });\n    const list = screen.getByRole('listbox');\n    await tooltipIsShowing(list, tooltipText);\n  });\n\n  it('should make the button disable and view the tooltip with the given text', async () => {\n    const customTooltipText = 'something here else';\n\n    render(\n      <ActionSelect\n        permission={invalidPermission}\n        message={customTooltipText}\n      />,\n      {\n        userInfo: userInfoRbacEnabled,\n      }\n    );\n    const list = screen.getByRole('listbox');\n\n    await tooltipIsShowing(list, customTooltipText);\n  });\n\n  it('should render the Select, but disabled cause the given role is not correct', async () => {\n    render(<ActionSelect permission={invalidPermission} />, {\n      userInfo: userInfoRbacEnabled,\n    });\n    const list = screen.getByRole('listbox');\n\n    await tooltipIsShowing(list, tooltipText);\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ActionComponent/__tests__/fixtures.ts",
    "content": "import { screen } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { Action, ResourceType } from 'generated-sources';\n\nexport const clusterName = 'local';\n\nexport const validPermission = {\n  resource: ResourceType.TOPIC,\n  action: Action.CREATE,\n  value: 'topic',\n};\n\nexport const invalidPermission = {\n  resource: ResourceType.SCHEMA,\n  action: Action.DELETE,\n  value: 'test',\n};\n\nconst roles = [\n  {\n    ...validPermission,\n    actions: [validPermission.action],\n    clusters: [clusterName],\n  },\n];\n\nexport const userInfoRbacEnabled = {\n  rbacFlag: true,\n  roles,\n};\n\nexport const userInfoRbacDisabled = {\n  rbacFlag: false,\n  roles,\n};\n\nexport const tooltipIsShowing = async (button: HTMLElement, text: string) => {\n  expect(screen.queryByText(text)).not.toBeInTheDocument();\n  await userEvent.hover(button);\n  expect(screen.getByText(text)).toBeInTheDocument();\n  await userEvent.unhover(button);\n  expect(screen.queryByText(text)).not.toBeInTheDocument();\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ActionComponent/index.ts",
    "content": "import ActionSelect from './ActionSelect/ActionSelect';\nimport ActionButton from './ActionButton/ActionButton';\nimport ActionCanButton from './ActionButton/ActionCanButton/ActionCanButton';\nimport ActionNavLink from './ActionNavLink/ActionNavLink';\nimport ActionDropdownItem from './ActionDropDownItem/ActionDropdownItem';\n\nexport {\n  ActionSelect,\n  ActionNavLink,\n  ActionCanButton,\n  ActionButton,\n  ActionDropdownItem,\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Alert/Alert.styled.ts",
    "content": "import styled from 'styled-components';\nimport { ToastTypes } from 'lib/errorHandling';\n\nexport const Alert = styled.div<{ $type: ToastTypes }>`\n  background-color: ${({ $type, theme }) => theme.alert.color[$type]};\n  width: 500px;\n  min-height: 64px;\n  border-radius: 8px;\n  padding: 12px;\n  display: flex;\n  justify-content: space-between;\n  align-items: baseline;\n  filter: drop-shadow(0px 4px 16px ${({ theme }) => theme.alert.shadow});\n  margin-top: 10px;\n  line-height: 20px;\n`;\n\nexport const Title = styled.div`\n  font-weight: 500;\n  font-size: 14px;\n`;\n\nexport const Message = styled.div`\n  font-weight: normal;\n  font-size: 14px;\n  margin: 3px 0;\n\n  ol,\n  ul {\n    padding-left: 25px;\n    list-style: auto;\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Alert/Alert.tsx",
    "content": "import React from 'react';\nimport CloseCircleIcon from 'components/common/Icons/CloseCircleIcon';\nimport IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';\nimport { ToastTypes } from 'lib/errorHandling';\n\nimport * as S from './Alert.styled';\n\nexport interface AlertProps {\n  title: string;\n  type: ToastTypes;\n  message: React.ReactNode;\n  onDissmiss(): void;\n}\n\nconst Alert: React.FC<AlertProps> = ({ title, type, message, onDissmiss }) => (\n  <S.Alert $type={type} role=\"alert\">\n    <div>\n      <S.Title role=\"heading\">{title}</S.Title>\n      <S.Message role=\"contentinfo\">{message}</S.Message>\n    </div>\n    <IconButtonWrapper role=\"button\" onClick={onDissmiss}>\n      <CloseCircleIcon />\n    </IconButtonWrapper>\n  </S.Alert>\n);\n\nexport default Alert;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Alert/__tests__/Alert.spec.tsx",
    "content": "import React from 'react';\nimport { screen } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { render } from 'lib/testHelpers';\nimport Alert, { AlertProps } from 'components/common/Alert/Alert';\n\nconst title = 'My Alert Title';\nconst message = 'My Alert Message';\nconst dismiss = jest.fn();\n\ndescribe('Alert', () => {\n  const setupComponent = (props: Partial<AlertProps> = {}) =>\n    render(\n      <Alert\n        type=\"error\"\n        title={title}\n        message={message}\n        onDissmiss={dismiss}\n        {...props}\n      />\n    );\n  const getButton = () => screen.getByRole('button');\n  it('renders with initial props', () => {\n    setupComponent();\n    expect(screen.getByRole('heading')).toHaveTextContent(title);\n    expect(screen.getByRole('contentinfo')).toHaveTextContent(message);\n    expect(getButton()).toBeInTheDocument();\n  });\n  it('handles dismiss callback', async () => {\n    setupComponent();\n    await userEvent.click(getButton());\n    expect(dismiss).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Button/Button.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport interface ButtonProps {\n  buttonType: 'primary' | 'secondary' | 'danger';\n  buttonSize: 'S' | 'M' | 'L';\n  isInverted?: boolean;\n}\n\nconst StyledButton = styled.button<ButtonProps>`\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: center;\n  padding: 0 12px;\n  border: none;\n  border-radius: 4px;\n  white-space: nowrap;\n\n  background: ${({ isInverted, buttonType, theme }) =>\n    isInverted\n      ? 'transparent'\n      : theme.button[buttonType].backgroundColor.normal};\n  color: ${({ isInverted, buttonType, theme }) =>\n    isInverted\n      ? theme.button[buttonType].invertedColors.normal\n      : theme.button[buttonType].color.normal};\n  font-size: ${({ theme, buttonSize }) => theme.button.fontSize[buttonSize]};\n  font-weight: 500;\n  height: ${({ theme, buttonSize }) => theme.button.height[buttonSize]};\n\n  &:hover:enabled {\n    background: ${({ isInverted, buttonType, theme }) =>\n      isInverted\n        ? 'transparent'\n        : theme.button[buttonType].backgroundColor.hover};\n    color: ${({ isInverted, buttonType, theme }) =>\n      isInverted\n        ? theme.button[buttonType].invertedColors.hover\n        : theme.button[buttonType].color};\n    cursor: pointer;\n  }\n  &:active:enabled {\n    background: ${({ isInverted, buttonType, theme }) =>\n      isInverted\n        ? 'transparent'\n        : theme.button[buttonType].backgroundColor.active};\n    color: ${({ isInverted, buttonType, theme }) =>\n      isInverted\n        ? theme.button[buttonType].invertedColors.active\n        : theme.button[buttonType].color};\n  }\n  &:disabled {\n    opacity: 0.5;\n    cursor: not-allowed;\n    background: ${({ buttonType, theme }) =>\n      theme.button[buttonType].backgroundColor.disabled};\n    color: ${({ buttonType, theme }) =>\n      theme.button[buttonType].color.disabled};\n  }\n\n  & a {\n    color: ${({ theme }) => theme.button.primary.color};\n  }\n\n  & svg {\n    margin-right: 7px;\n    fill: ${({ theme, disabled, buttonType }) =>\n      disabled\n        ? theme.button[buttonType].color.disabled\n        : theme.button[buttonType].color.normal};\n  }\n`;\n\nexport default StyledButton;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Button/Button.tsx",
    "content": "import StyledButton, {\n  ButtonProps,\n} from 'components/common/Button/Button.styled';\nimport React from 'react';\nimport { Link } from 'react-router-dom';\nimport Spinner from 'components/common/Spinner/Spinner';\n\nexport interface Props\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    ButtonProps {\n  to?: string | object;\n  inProgress?: boolean;\n}\n\nexport const Button: React.FC<Props> = ({ to, ...props }) => {\n  if (to) {\n    return (\n      <Link to={to}>\n        <StyledButton type=\"button\" {...props}>\n          {props.children}\n        </StyledButton>\n      </Link>\n    );\n  }\n  return (\n    <StyledButton\n      type=\"button\"\n      disabled={props.disabled || props.inProgress}\n      {...props}\n    >\n      {props.children}{' '}\n      {props.inProgress ? (\n        <Spinner size={16} borderWidth={2} marginLeft={2} emptyBorderColor />\n      ) : null}\n    </StyledButton>\n  );\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Button/__tests__/Button.spec.tsx",
    "content": "import React from 'react';\nimport { Button } from 'components/common/Button/Button';\nimport { screen } from '@testing-library/react';\nimport { render } from 'lib/testHelpers';\nimport { theme } from 'theme/theme';\n\ndescribe('Button', () => {\n  it('renders small primary Button', () => {\n    render(<Button buttonType=\"primary\" buttonSize=\"S\" />);\n    expect(screen.getByRole('button')).toBeInTheDocument();\n    expect(screen.getByRole('button')).toHaveStyleRule(\n      'color',\n      theme.button.primary.color.normal\n    );\n    expect(screen.getByRole('button')).toHaveStyleRule(\n      'font-size',\n      theme.button.fontSize.S\n    );\n  });\n\n  it('renders medium size secondary Button', () => {\n    render(<Button buttonType=\"secondary\" buttonSize=\"M\" />);\n    expect(screen.getByRole('button')).toBeInTheDocument();\n    expect(screen.getByRole('button')).toHaveStyleRule(\n      'color',\n      theme.button.secondary.color.normal\n    );\n    expect(screen.getByRole('button')).toHaveStyleRule(\n      'font-size',\n      theme.button.fontSize.M\n    );\n  });\n\n  it('renders small Button', () => {\n    render(<Button buttonType=\"secondary\" buttonSize=\"S\" />);\n    expect(screen.getByRole('button')).toBeInTheDocument();\n    expect(screen.getByRole('button')).toHaveStyleRule(\n      'color',\n      theme.button.secondary.color.normal\n    );\n  });\n\n  it('renders link with large primary button inside', () => {\n    render(<Button to=\"/my-link\" buttonType=\"primary\" buttonSize=\"L\" />);\n    expect(screen.getByRole('link')).toBeInTheDocument();\n    expect(screen.getByRole('button')).toBeInTheDocument();\n    expect(screen.getByRole('button')).toHaveStyleRule(\n      'font-size',\n      theme.button.fontSize.L\n    );\n  });\n\n  it('renders inverted color Button', () => {\n    render(<Button buttonType=\"primary\" buttonSize=\"S\" isInverted />);\n    expect(screen.getByRole('button')).toBeInTheDocument();\n    expect(screen.getByRole('button')).toHaveStyleRule(\n      'color',\n      theme.button.primary.invertedColors.normal\n    );\n  });\n  it('renders disabled button and spinner when inProgress truthy', () => {\n    render(<Button buttonType=\"primary\" buttonSize=\"M\" inProgress />);\n    expect(screen.getByRole('button')).toBeInTheDocument();\n    expect(screen.getByRole('progressbar')).toBeInTheDocument();\n    expect(screen.getByRole('button')).toBeDisabled();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/BytesFormatted/BytesFormatted.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const NoWrap = styled.span`\n  white-space: nowrap;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/BytesFormatted/BytesFormatted.tsx",
    "content": "import React from 'react';\n\nimport { NoWrap } from './BytesFormatted.styled';\n\ninterface Props {\n  value: string | number | undefined;\n  precision?: number;\n}\n\nexport const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];\n\nconst BytesFormatted: React.FC<Props> = ({ value, precision = 0 }) => {\n  const formattedValue = React.useMemo((): string => {\n    try {\n      const bytes = typeof value === 'string' ? parseInt(value, 10) : value;\n      if (Number.isNaN(bytes) || (bytes && bytes < 0)) return `-Bytes`;\n      if (!bytes || bytes < 1024) return `${Math.ceil(bytes || 0)} ${sizes[0]}`;\n      const pow = Math.floor(Math.log2(bytes) / 10);\n      const multiplier = 10 ** (precision < 0 ? 0 : precision);\n      return `${Math.round((bytes * multiplier) / 1024 ** pow) / multiplier} ${\n        sizes[pow]\n      }`;\n    } catch (e) {\n      return `-Bytes`;\n    }\n  }, [precision, value]);\n\n  return <NoWrap>{formattedValue}</NoWrap>;\n};\n\nexport default BytesFormatted;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/BytesFormatted/__tests__/BytesFormatted.spec.tsx",
    "content": "import React from 'react';\nimport BytesFormatted, {\n  sizes,\n} from 'components/common/BytesFormatted/BytesFormatted';\nimport { render, screen } from '@testing-library/react';\n\ndescribe('BytesFormatted', () => {\n  it('renders Bytes correctly', () => {\n    render(<BytesFormatted value={666} />);\n    expect(screen.getByText('666 Bytes')).toBeInTheDocument();\n  });\n\n  it('renders correct units', () => {\n    let value = 1;\n    sizes.forEach((unit) => {\n      render(<BytesFormatted value={value} />);\n      expect(screen.getByText(`1 ${unit}`)).toBeInTheDocument();\n      value *= 1024;\n    });\n  });\n\n  it('renders correct precision', () => {\n    render(<BytesFormatted value={2000} precision={100} />);\n    expect(screen.getByText(`1.953125 ${sizes[1]}`)).toBeInTheDocument();\n\n    render(<BytesFormatted value={10000} precision={5} />);\n    expect(screen.getByText(`9.76563 ${sizes[1]}`)).toBeInTheDocument();\n  });\n\n  it('correctly handles invalid props', () => {\n    render(<BytesFormatted value={10000} precision={-1} />);\n    expect(screen.getByText(`10 ${sizes[1]}`)).toBeInTheDocument();\n\n    render(<BytesFormatted value=\"some string\" />);\n    expect(screen.getAllByText(`-${sizes[0]}`).length).toBeTruthy();\n\n    render(<BytesFormatted value={-100000} />);\n    expect(screen.getAllByText(`-${sizes[0]}`).length).toBeTruthy();\n\n    render(<BytesFormatted value={undefined} />);\n    expect(screen.getByText(`0 ${sizes[0]}`)).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Checkbox/Checkbox.tsx",
    "content": "import * as React from 'react';\nimport { useFormContext } from 'react-hook-form';\nimport { InputLabel } from 'components/common/Input/InputLabel.styled';\nimport { FormError, InputHint } from 'components/common/Input/Input.styled';\nimport { ErrorMessage } from '@hookform/error-message';\n\ninterface CheckboxProps {\n  name: string;\n  label: React.ReactNode;\n  hint?: string;\n}\n\nconst Checkbox: React.FC<CheckboxProps> = ({ name, label, hint }) => {\n  const { register } = useFormContext();\n\n  return (\n    <div>\n      <InputLabel>\n        <input {...register(name)} type=\"checkbox\" />\n        {label}\n      </InputLabel>\n      <InputHint>{hint}</InputHint>\n      <FormError>\n        <ErrorMessage name={name} />\n      </FormError>\n    </div>\n  );\n};\n\nexport default Checkbox;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ConfirmationModal/ConfirmationModal.styled.tsx",
    "content": "import styled, { css } from 'styled-components';\n\nexport const Wrapper = styled.div.attrs({ role: 'dialog' })`\n  display: flex;\n  align-items: center;\n  flex-direction: column;\n  justify-content: center;\n  overflow: hidden;\n  position: fixed;\n  z-index: 40;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  top: 0;\n`;\n\nexport const Overlay = styled.div(\n  ({ theme: { modal } }) => css`\n    background-color: ${modal.overlay};\n    bottom: 0;\n    left: 0;\n    position: absolute;\n    right: 0;\n    top: 0;\n  `\n);\n\nexport const Modal = styled.div(\n  ({ theme: { modal, confirmModal } }) => css`\n    position: absolute;\n    display: flex;\n    flex-direction: column;\n    width: 560px;\n    border-radius: 8px;\n\n    background-color: ${confirmModal.backgroundColor};\n    filter: drop-shadow(0px 4px 16px ${modal.shadow});\n  `\n);\n\nexport const Header = styled.div`\n  font-size: 20px;\n  text-align: start;\n  padding: 16px;\n  width: 100%;\n  color: ${({ theme }) => theme.modal.color};\n`;\n\nexport const Content = styled.div(\n  ({ theme: { modal } }) => css`\n    padding: 16px;\n    width: 100%;\n    border-top: 1px solid ${modal.border.top};\n    border-bottom: 1px solid ${modal.border.bottom};\n    color: ${modal.contentColor};\n  `\n);\n\nexport const Footer = styled.div`\n  height: 64px;\n  display: flex;\n  justify-content: flex-end;\n  gap: 10px;\n  padding: 16px;\n  width: 100%;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ConfirmationModal/ConfirmationModal.tsx",
    "content": "import React from 'react';\nimport { Button } from 'components/common/Button/Button';\nimport { ConfirmContext } from 'components/contexts/ConfirmContext';\n\nimport * as S from './ConfirmationModal.styled';\n\nconst ConfirmationModal: React.FC = () => {\n  const context = React.useContext(ConfirmContext);\n  const isOpen = context?.content && context?.confirm;\n\n  if (!isOpen) return null;\n\n  return (\n    <S.Wrapper role=\"dialog\" aria-label=\"Confirmation Dialog\">\n      <S.Overlay onClick={context.cancel} aria-hidden=\"true\" role=\"button\" />\n      <S.Modal>\n        <S.Header>Confirm the action</S.Header>\n        <S.Content>{context.content}</S.Content>\n        <S.Footer>\n          <Button\n            buttonType=\"secondary\"\n            buttonSize=\"M\"\n            onClick={context.cancel}\n            type=\"button\"\n          >\n            Cancel\n          </Button>\n          <Button\n            buttonType={context.dangerButton ? 'danger' : 'primary'}\n            buttonSize=\"M\"\n            onClick={context.confirm}\n            type=\"button\"\n          >\n            Confirm\n          </Button>\n        </S.Footer>\n      </S.Modal>\n    </S.Wrapper>\n  );\n};\n\nexport default ConfirmationModal;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ControlPanel/ControlPanel.styled.ts",
    "content": "import styled from 'styled-components';\n\ninterface Props {\n  hasInput?: boolean;\n}\n\nexport const ControlPanelWrapper = styled.div<Props>`\n  display: flex;\n  align-items: center;\n  padding: 0 16px;\n  margin: 0 0 16px;\n  width: 100%;\n  gap: 16px;\n  color: ${({ theme }) => theme.default.color.normal};\n  & > *:first-child {\n    width: ${(props) => (props.hasInput ? '38%' : 'auto')};\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/DiffViewer/DiffViewer.tsx",
    "content": "import { diff as DiffEditor } from 'react-ace';\nimport 'ace-builds/src-noconflict/ace';\nimport 'ace-builds/src-noconflict/mode-json5';\nimport 'ace-builds/src-noconflict/mode-protobuf';\nimport 'ace-builds/src-noconflict/theme-textmate';\nimport React from 'react';\nimport { IDiffEditorProps } from 'react-ace/lib/diff';\nimport { SchemaType } from 'generated-sources';\n\ninterface DiffViewerProps extends IDiffEditorProps {\n  isFixedHeight?: boolean;\n  schemaType: string;\n}\n\nconst DiffViewer = React.forwardRef<DiffEditor | null, DiffViewerProps>(\n  (props, ref) => {\n    const { isFixedHeight, schemaType, ...rest } = props;\n    const autoHeight =\n      !isFixedHeight && props.value && props.value.length === 2\n        ? Math.max(\n            props.value[0].split(/\\r\\n|\\r|\\n/).length + 1,\n            props.value[1].split(/\\r\\n|\\r|\\n/).length + 1\n          ) * 16\n        : 500;\n    return (\n      <div>\n        <DiffEditor\n          name=\"diff-editor\"\n          ref={ref}\n          mode={\n            schemaType === SchemaType.JSON || schemaType === SchemaType.AVRO\n              ? 'json5'\n              : 'protobuf'\n          }\n          theme=\"textmate\"\n          tabSize={2}\n          width=\"100%\"\n          height={`${autoHeight}px`}\n          showPrintMargin={false}\n          maxLines={Infinity}\n          readOnly\n          wrapEnabled\n          {...rest}\n        />\n      </div>\n    );\n  }\n);\n\nDiffViewer.displayName = 'DiffViewer';\n\nexport default DiffViewer;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/DiffViewer/__tests__/DiffViewer.spec.tsx",
    "content": "import React from 'react';\nimport DiffViewer from 'components/common/DiffViewer/DiffViewer';\nimport { render } from '@testing-library/react';\n\ndescribe('Editor component', () => {\n  const left = '{\\n}';\n  const right = '{\\ntest: true\\n}';\n\n  const renderComponent = (props: {\n    leftVersion?: string;\n    rightVersion?: string;\n    isFixedHeight?: boolean;\n  }) => {\n    const { container } = render(\n      <DiffViewer\n        value={[props.leftVersion ?? '', props.rightVersion ?? '']}\n        name=\"name\"\n        schemaType=\"JSON\"\n        isFixedHeight={props.isFixedHeight}\n      />\n    );\n    return container;\n  };\n\n  it('renders', () => {\n    const component = renderComponent({\n      leftVersion: left,\n      rightVersion: right,\n    });\n    expect(component).toBeInTheDocument();\n  });\n\n  it('renders with fixed height', () => {\n    const component = renderComponent({\n      leftVersion: left,\n      rightVersion: right,\n      isFixedHeight: true,\n    }).children[0].children[0];\n    expect(component).toHaveStyle('height: 500px;');\n  });\n\n  it('renders with fixed height with no value', () => {\n    const component = renderComponent({\n      isFixedHeight: true,\n    }).children[0].children[0];\n    expect(component).toHaveStyle('height: 500px;');\n  });\n\n  it('renders without fixed height with no value', () => {\n    const component = renderComponent({}).children[0].children[0];\n    expect(component).toHaveStyle('height: 32px;');\n  });\n\n  it('renders without fixed height with one value', () => {\n    const component = renderComponent({\n      leftVersion: left,\n    }).children[0].children[0];\n    expect(component).toHaveStyle('height: 48px;');\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Dropdown/Dropdown.styled.ts",
    "content": "import styled, { css, keyframes } from 'styled-components';\nimport { ControlledMenu } from '@szhsin/react-menu';\nimport { menuSelector, menuItemSelector } from '@szhsin/react-menu/style-utils';\n\nimport '@szhsin/react-menu/dist/core.css';\n\nconst menuShow = keyframes`\n  from {\n    opacity: 0;\n  }\n`;\nconst menuHide = keyframes`\n  to {\n    opacity: 0;\n  }\n`;\n\nexport const Dropdown = styled(ControlledMenu)(\n  ({ theme: { dropdown } }) => css`\n    // container for the menu items\n    ${menuSelector.name} {\n      border: 1px solid ${dropdown.borderColor};\n      box-shadow: 0 4px 16px ${dropdown.shadow};\n      padding: 8px 0;\n      border-radius: 4px;\n      font-size: 14px;\n      background-color: ${dropdown.backgroundColor};\n      text-align: left;\n    }\n\n    ${menuSelector.stateOpening} {\n      animation: ${menuShow} 0.15s ease-out;\n    }\n\n    // NOTE: animation-fill-mode: forwards is required to\n    // prevent flickering with React 18 createRoot()\n    ${menuSelector.stateClosing} {\n      animation: ${menuHide} 0.2s ease-out forwards;\n    }\n\n    ${menuItemSelector.name} {\n      padding: 6px 16px;\n      min-width: 150px;\n      background-color: ${dropdown.item.backgroundColor.default};\n      white-space: nowrap;\n    }\n\n    ${menuItemSelector.hover} {\n      background-color: ${dropdown.item.backgroundColor.hover};\n    }\n\n    ${menuItemSelector.disabled} {\n      cursor: not-allowed;\n      opacity: 0.5;\n    }\n  `\n);\n\nexport const DropdownButton = styled.button`\n  background-color: transparent;\n  border: none;\n  display: flex;\n  cursor: pointer;\n  align-self: center;\n\n  &:disabled {\n    opacity: 0.5;\n    cursor: not-allowed;\n  }\n`;\n\nexport const DangerItem = styled.div`\n  color: ${({ theme: { dropdown } }) => dropdown.item.color.danger};\n`;\n\nexport const DropdownItemHint = styled.div`\n  color: ${({ theme }) => theme.topicMetaData.color.label};\n  font-size: 12px;\n  line-height: 1.4;\n  margin-top: 5px;\n`;\n\nexport const Wrapper = styled.div`\n  display: inline-flex;\n  align-items: center;\n  justify-content: end;\n  color: ${({ theme: { dropdown } }) => dropdown.item.color.normal};\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Dropdown/Dropdown.tsx",
    "content": "import { MenuProps } from '@szhsin/react-menu';\nimport React, { PropsWithChildren, useRef } from 'react';\nimport VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';\nimport useBoolean from 'lib/hooks/useBoolean';\n\nimport * as S from './Dropdown.styled';\n\ninterface DropdownProps extends PropsWithChildren<Partial<MenuProps>> {\n  label?: React.ReactNode;\n  disabled?: boolean;\n}\n\nconst Dropdown: React.FC<DropdownProps> = ({ label, disabled, children }) => {\n  const ref = useRef(null);\n  const { value: isOpen, setFalse, setTrue } = useBoolean(false);\n\n  const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setTrue();\n  };\n\n  return (\n    <S.Wrapper>\n      <S.DropdownButton\n        onClick={handleClick}\n        ref={ref}\n        aria-label=\"Dropdown Toggle\"\n        disabled={disabled}\n      >\n        {label || <VerticalElipsisIcon />}\n      </S.DropdownButton>\n      <S.Dropdown\n        anchorRef={ref}\n        state={isOpen ? 'open' : 'closed'}\n        onMouseLeave={setFalse}\n        onClose={setFalse}\n        align=\"end\"\n        direction=\"bottom\"\n        offsetY={10}\n        viewScroll=\"auto\"\n      >\n        {children}\n      </S.Dropdown>\n    </S.Wrapper>\n  );\n};\n\nexport default Dropdown;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Dropdown/DropdownItem.tsx",
    "content": "import React, { PropsWithChildren } from 'react';\nimport { ClickEvent, MenuItem, MenuItemProps } from '@szhsin/react-menu';\nimport { useConfirm } from 'lib/hooks/useConfirm';\n\nimport * as S from './Dropdown.styled';\n\nexport interface DropdownItemProps extends PropsWithChildren<MenuItemProps> {\n  danger?: boolean;\n  onClick?(): void;\n  confirm?: React.ReactNode;\n}\n\nconst DropdownItem = React.forwardRef<unknown, DropdownItemProps>(\n  ({ onClick, danger, children, confirm, ...rest }, ref) => {\n    const confirmation = useConfirm();\n\n    const handleClick = (e: ClickEvent) => {\n      if (!onClick) return;\n\n      // eslint-disable-next-line no-param-reassign\n      e.stopPropagation = true;\n      e.syntheticEvent.stopPropagation();\n\n      if (confirm) {\n        confirmation(confirm, onClick);\n      } else {\n        onClick();\n      }\n    };\n\n    return (\n      <MenuItem onClick={handleClick} {...rest} ref={ref}>\n        {danger ? <S.DangerItem>{children}</S.DangerItem> : children}\n      </MenuItem>\n    );\n  }\n);\n\nexport default DropdownItem;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Dropdown/index.ts",
    "content": "import { DropdownItemHint } from './Dropdown.styled';\nimport Dropdown from './Dropdown';\nimport DropdownItem from './DropdownItem';\n\nexport { Dropdown, DropdownItem, DropdownItemHint };\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Editor/Editor.tsx",
    "content": "import AceEditor, { IAceEditorProps } from 'react-ace';\nimport 'ace-builds/src-noconflict/mode-json5';\nimport 'ace-builds/src-noconflict/mode-protobuf';\nimport 'ace-builds/src-noconflict/theme-tomorrow';\nimport { SchemaType } from 'generated-sources';\nimport React from 'react';\nimport styled from 'styled-components';\n\ninterface EditorProps extends IAceEditorProps {\n  isFixedHeight?: boolean;\n  schemaType?: string;\n}\n\nconst Editor = React.forwardRef<AceEditor | null, EditorProps>((props, ref) => {\n  const { isFixedHeight, schemaType, ...rest } = props;\n  return (\n    <AceEditor\n      ref={ref}\n      mode={\n        schemaType === SchemaType.JSON || schemaType === SchemaType.AVRO\n          ? 'json5'\n          : 'protobuf'\n      }\n      theme=\"tomorrow\"\n      tabSize={2}\n      width=\"100%\"\n      fontSize={14}\n      height={\n        isFixedHeight\n          ? `${(props.value?.split('\\n').length || 32) * 19}px`\n          : '372px'\n      }\n      wrapEnabled\n      {...rest}\n    />\n  );\n});\n\nEditor.displayName = 'Editor';\n\nexport default styled(Editor)`\n  &.ace-tomorrow {\n    background: transparent;\n    .ace_gutter {\n      background-color: ${({ theme }) =>\n        theme.ksqlDb.query.editor.layer.backgroundColor};\n    }\n    .ace_gutter-active-line {\n      background-color: ${({ theme }) =>\n        theme.ksqlDb.query.editor.cell.backgroundColor};\n      color: ${({ theme }) => theme.default.color.normal};\n    }\n    .ace_scroller {\n      background-color: ${({ theme }) => theme.default.backgroundColor};\n    }\n    .ace_line {\n      color: ${({ theme }) => theme.default.color.normal};\n    }\n    .ace_cursor {\n      color: ${({ theme }) => theme.ksqlDb.query.editor.cursor};\n    }\n    .ace_active-line {\n      background-color: ${({ theme }) =>\n        theme.ksqlDb.query.editor.cell.backgroundColor};\n    }\n    .ace_gutter-cell {\n      color: ${({ theme }) => theme.default.color.normal};\n    }\n    .ace_variable {\n      color: ${({ theme }) => theme.ksqlDb.query.editor.variable};\n    }\n    .ace_string {\n      color: ${({ theme }) => theme.ksqlDb.query.editor.aceString};\n    }\n    .ace_print-margin {\n      display: none;\n    }\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/EditorViewer/EditorViewer.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const Wrapper = styled.div`\n  background-color: ${({ theme }) => theme.viewer.wrapper.backgroundColor};\n  padding: 8px 16px;\n  .ace_active-line {\n    background-color: ${({ theme }) =>\n      theme.default.backgroundColor} !important;\n  }\n  .ace_line {\n    color: ${({ theme }) => theme.viewer.wrapper.color} !important;\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/EditorViewer/EditorViewer.tsx",
    "content": "import React from 'react';\nimport Editor from 'components/common/Editor/Editor';\nimport { SchemaType } from 'generated-sources';\nimport { parse, stringify } from 'lossless-json';\n\nimport * as S from './EditorViewer.styled';\n\nexport interface EditorViewerProps {\n  data: string;\n  schemaType?: string;\n  maxLines?: number;\n}\nconst getSchemaValue = (data: string, schemaType?: string) => {\n  if (schemaType === SchemaType.JSON || schemaType === SchemaType.AVRO) {\n    return stringify(parse(data), undefined, '\\t');\n  }\n  return data;\n};\nconst EditorViewer: React.FC<EditorViewerProps> = ({\n  data,\n  schemaType,\n  maxLines,\n}) => {\n  try {\n    return (\n      <S.Wrapper>\n        <Editor\n          isFixedHeight\n          schemaType={schemaType}\n          name=\"schema\"\n          value={getSchemaValue(data, schemaType)}\n          setOptions={{\n            showLineNumbers: false,\n            maxLines,\n            showGutter: false,\n          }}\n          readOnly\n        />\n      </S.Wrapper>\n    );\n  } catch (e) {\n    return (\n      <S.Wrapper>\n        <p>{data}</p>\n      </S.Wrapper>\n    );\n  }\n};\n\nexport default EditorViewer;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/EditorViewer/__test__/EditorViewer.spec.tsx",
    "content": "import React from 'react';\nimport EditorViewer, {\n  EditorViewerProps,\n} from 'components/common/EditorViewer/EditorViewer';\nimport { render } from 'lib/testHelpers';\nimport { screen } from '@testing-library/react';\n\nconst data = { a: 1 };\nconst maxLines = 28;\nconst schemaType = 'JSON';\n\ndescribe('EditorViewer component', () => {\n  const setupComponent = (props: EditorViewerProps) =>\n    render(<EditorViewer {...props} />);\n\n  it('renders JSONTree', () => {\n    setupComponent({\n      data: JSON.stringify(data),\n      maxLines,\n      schemaType,\n    });\n    expect(screen.getByRole('textbox')).toBeInTheDocument();\n  });\n\n  it('to be in the document with fixed height with no value', () => {\n    setupComponent({\n      data: '',\n      maxLines,\n      schemaType,\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Ellipsis/Ellipsis.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const Text = styled.div`\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  max-width: 340px;\n`;\n\nexport const Wrapper = styled.div`\n  display: flex;\n  gap: 8px;\n  align-items: center;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Ellipsis/Ellipsis.tsx",
    "content": "import React, { PropsWithChildren } from 'react';\n\nimport * as S from './Ellipsis.styled';\n\ntype EllipsisProps = {\n  text: React.ReactNode;\n};\n\nconst Ellipsis: React.FC<PropsWithChildren<EllipsisProps>> = ({\n  text,\n  children,\n}) => {\n  return (\n    <S.Wrapper>\n      <S.Text>{text}</S.Text>\n      {children}\n    </S.Wrapper>\n  );\n};\nexport default Ellipsis;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Form/Form.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const StyledForm = styled.form`\n  padding: 16px;\n  max-width: 800px;\n  display: flex;\n  gap: 16px;\n  flex-direction: column;\n\n  h3 {\n    margin-bottom: 0;\n    line-height: 32px;\n  }\n`;\n\nexport const FlexFieldset = styled.fieldset`\n  display: flex;\n  gap: 16px;\n  flex-direction: column;\n\n  &:disabled {\n    ul {\n      opacity: 0.5;\n      background-color: #f5f5f5;\n      pointer-events: none;\n    }\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/ArrowDownIcon.tsx",
    "content": "import React from 'react';\nimport { useTheme } from 'styled-components';\n\nconst ArrowDownIcon: React.FC = () => {\n  const theme = useTheme();\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 384 512\"\n      width=\"10\"\n      height=\"10\"\n      fill={theme.icons.arrowDownIcon}\n    >\n      {/* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}\n      <path d=\"M374.6 310.6l-160 160C208.4 476.9 200.2 480 192 480s-16.38-3.125-22.62-9.375l-160-160c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 370.8V64c0-17.69 14.33-31.1 31.1-31.1S224 46.31 224 64v306.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0S387.1 298.1 374.6 310.6z\" />\n    </svg>\n  );\n};\n\nexport default ArrowDownIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/AutoIcon.tsx",
    "content": "import React from 'react';\nimport { useTheme } from 'styled-components';\n\nconst AutoIcon: React.FC = () => {\n  const theme = useTheme();\n  return (\n    <svg\n      width=\"14\"\n      height=\"15\"\n      viewBox=\"0 0 14 15\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M7.92385 8.49072L7.03019 5.8418H6.97336L6.07796 8.49072H7.92385Z\"\n        fill={theme.icons.autoIcon}\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M7 14.7422C10.866 14.7422 14 11.6082 14 7.74219C14 3.87619 10.866 0.742188 7 0.742188C3.13401 0.742188 0 3.87619 0 7.74219C0 11.6082 3.13401 14.7422 7 14.7422ZM3.5 11.2422H5.14789L5.68745 9.646H8.3136L8.85211 11.2422H10.5L7.99264 4.24219H6.01091L3.5 11.2422Z\"\n        fill={theme.icons.autoIcon}\n      />\n    </svg>\n  );\n};\n\nexport default AutoIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/CancelIcon.tsx",
    "content": "import React from 'react';\nimport { useTheme } from 'styled-components';\n\nconst CancelIcon: React.FC = () => {\n  const theme = useTheme();\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 64 64\"\n      width=\"12\"\n      height=\"12\"\n      aria-labelledby=\"title\"\n      aria-describedby=\"desc\"\n      role=\"img\"\n    >\n      <title>Cancel</title>\n      <desc>A line styled icon from Orion Icon Library.</desc>\n      <path\n        data-name=\"layer1\"\n        d=\"M53.122 48.88L36.243 32l16.878-16.878a3 3 0 0 0-4.242-4.242L32 27.758l-16.878-16.88a3 3 0 0 0-4.243 4.243l16.878 16.88-16.88 16.88a3 3 0 0 0 4.243 4.241L32 36.243l16.878 16.88a3 3 0 0 0 4.244-4.243z\"\n        fill=\"none\"\n        stroke={theme.icons.cancelIcon}\n        strokeMiterlimit=\"10\"\n        strokeWidth=\"2\"\n        strokeLinejoin=\"round\"\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  );\n};\n\nexport default CancelIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/CheckMarkRoundIcon.tsx",
    "content": "import React from 'react';\n\nconst CheckMarkRoundIcon: React.FC = () => {\n  return (\n    <svg\n      width=\"14\"\n      height=\"14\"\n      viewBox=\"0 0 14 14\"\n      fill=\"none\"\n      role=\"tooltip\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14ZM11.2796 5.39575C11.5831 5.1139 11.6007 4.63935 11.3188 4.33582C11.037 4.03228 10.5624 4.01471 10.2589 4.29656L6.13018 8.13037L3.74111 5.91194C3.43757 5.63009 2.96303 5.64767 2.68117 5.9512C2.39932 6.25473 2.4169 6.72928 2.72043 7.01113L5.61984 9.70344C5.9076 9.97065 6.35276 9.97065 6.64052 9.70344L11.2796 5.39575Z\"\n        fill=\"#33CC66\"\n      />\n    </svg>\n  );\n};\n\nexport default CheckMarkRoundIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/CheckmarkIcon.tsx",
    "content": "import React, { FC } from 'react';\n\nconst CheckmarkIcon: FC = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 64 64\"\n      width=\"12\"\n      height=\"12\"\n      aria-labelledby=\"title\"\n      aria-describedby=\"desc\"\n      role=\"img\"\n    >\n      <title>Checkmark</title>\n      <desc>A line styled icon from Orion Icon Library.</desc>\n      <path\n        data-name=\"layer1\"\n        fill=\"none\"\n        stroke=\"#FFFFFF\"\n        strokeMiterlimit=\"10\"\n        strokeWidth=\"2\"\n        d=\"M2 30l21 22 39-40\"\n        strokeLinejoin=\"round\"\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  );\n};\n\nexport default CheckmarkIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/ChevronDownIcon.tsx",
    "content": "import React from 'react';\nimport { useTheme } from 'styled-components';\n\nconst ChevronDownIcon: React.FC = () => {\n  const theme = useTheme();\n  return (\n    <svg\n      width=\"10\"\n      height=\"6\"\n      viewBox=\"0 0 10 6\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M0.646447 0.646447C0.841709 0.451184 1.15829 0.451184 1.35355 0.646447L5 4.29289L8.64645 0.646447C8.84171 0.451184 9.15829 0.451184 9.35355 0.646447C9.54882 0.841709 9.54882 1.15829 9.35355 1.35355L5.35355 5.35355C5.15829 5.54882 4.84171 5.54882 4.64645 5.35355L0.646447 1.35355C0.451184 1.15829 0.451184 0.841709 0.646447 0.646447Z\"\n        fill={theme.icons.chevronDownIcon}\n      />\n    </svg>\n  );\n};\n\nexport default ChevronDownIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/ClockIcon.tsx",
    "content": "import React from 'react';\nimport { useTheme } from 'styled-components';\n\nconst ClockIcon: React.FC = () => {\n  const theme = useTheme();\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 512 512\"\n      width={10}\n      height={10}\n      fill={theme.icons.clockIcon}\n    >\n      {/* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}\n      <path d=\"M232 120C232 106.7 242.7 96 256 96C269.3 96 280 106.7 280 120V243.2L365.3 300C376.3 307.4 379.3 322.3 371.1 333.3C364.6 344.3 349.7 347.3 338.7 339.1L242.7 275.1C236 271.5 232 264 232 255.1L232 120zM256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0zM48 256C48 370.9 141.1 464 256 464C370.9 464 464 370.9 464 256C464 141.1 370.9 48 256 48C141.1 48 48 141.1 48 256z\" />\n    </svg>\n  );\n};\n\nexport default ClockIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/CloseCircleIcon.tsx",
    "content": "import React from 'react';\nimport { useTheme } from 'styled-components';\n\nconst CloseCircleIcon: React.FC = () => {\n  const theme = useTheme();\n  return (\n    <svg\n      width=\"16\"\n      height=\"16\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM11.707 4.29289C12.0976 4.68342 12.0976 5.31658 11.707 5.70711L9.41415 8L11.707 10.2929C12.0976 10.6834 12.0976 11.3166 11.707 11.7071C11.3165 12.0976 10.6834 12.0976 10.2928 11.7071L7.99994 9.41421L5.70711 11.707C5.31658 12.0976 4.68342 12.0976 4.29289 11.707C3.90237 11.3165 3.90237 10.6834 4.29289 10.2928L6.58573 8L4.29289 5.70717C3.90237 5.31664 3.90237 4.68348 4.29289 4.29295C4.68342 3.90243 5.31658 3.90243 5.70711 4.29295L7.99994 6.58579L10.2928 4.29289C10.6834 3.90237 11.3165 3.90237 11.707 4.29289Z\"\n        fill={theme.icons.closeCircleIcon}\n      />\n    </svg>\n  );\n};\n\nexport default CloseCircleIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/CloseIcon.tsx",
    "content": "import React from 'react';\nimport styled, { useTheme } from 'styled-components';\n\nconst CloseIcon: React.FC<{ className?: string }> = ({ className }) => {\n  const theme = useTheme();\n  return (\n    <svg\n      width=\"10\"\n      height=\"10\"\n      viewBox=\"0 0 10 10\"\n      className={className}\n      fill={theme.icons.closeIcon.normal}\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M0.646447 0.646447C0.841709 0.451184 1.15829 0.451184 1.35355 0.646447L5 4.29289L8.64645 0.646447C8.84171 0.451184 9.15829 0.451184 9.35355 0.646447C9.54882 0.841709 9.54882 1.15829 9.35355 1.35355L5.70711 5L9.35355 8.64645C9.54882 8.84171 9.54882 9.15829 9.35355 9.35355C9.15829 9.54882 8.84171 9.54882 8.64645 9.35355L5 5.70711L1.35355 9.35355C1.15829 9.54881 0.841709 9.54881 0.646447 9.35355C0.451185 9.15829 0.451185 8.84171 0.646447 8.64645L4.29289 5L0.646447 1.35355C0.451184 1.15829 0.451184 0.841709 0.646447 0.646447Z\"\n      />\n    </svg>\n  );\n};\n\nexport default styled(CloseIcon)``;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/DeleteIcon.tsx",
    "content": "import React from 'react';\nimport { useTheme } from 'styled-components';\n\nconst DeleteIcon: React.FC<{ fill?: string }> = ({ fill }) => {\n  const theme = useTheme();\n  const curentFill = fill || theme.editFilter.deleteIconColor;\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 448 512\"\n      fill={curentFill}\n      width=\"14\"\n      height=\"14\"\n    >\n      {/* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}\n      <path d=\"M135.2 17.69C140.6 6.848 151.7 0 163.8 0H284.2C296.3 0 307.4 6.848 312.8 17.69L320 32H416C433.7 32 448 46.33 448 64C448 81.67 433.7 96 416 96H32C14.33 96 0 81.67 0 64C0 46.33 14.33 32 32 32H128L135.2 17.69zM31.1 128H416V448C416 483.3 387.3 512 352 512H95.1C60.65 512 31.1 483.3 31.1 448V128zM111.1 208V432C111.1 440.8 119.2 448 127.1 448C136.8 448 143.1 440.8 143.1 432V208C143.1 199.2 136.8 192 127.1 192C119.2 192 111.1 199.2 111.1 208zM207.1 208V432C207.1 440.8 215.2 448 223.1 448C232.8 448 240 440.8 240 432V208C240 199.2 232.8 192 223.1 192C215.2 192 207.1 199.2 207.1 208zM304 208V432C304 440.8 311.2 448 320 448C328.8 448 336 440.8 336 432V208C336 199.2 328.8 192 320 192C311.2 192 304 199.2 304 208z\" />\n    </svg>\n  );\n};\n\nexport default DeleteIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/DiscordIcon.tsx",
    "content": "import React from 'react';\nimport styled from 'styled-components';\n\nconst DiscordIcon: React.FC<{ className?: string }> = ({ className }) => (\n  <svg\n    width=\"22\"\n    height=\"18\"\n    className={className}\n    viewBox=\"0 0 22 18\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      d=\"M18.6239 1.60293C17.2217 0.92338 15.7181 0.422718 14.1459 0.135969C14.1173 0.130434 14.0887 0.144265 14.0739 0.171926C13.8805 0.535202 13.6663 1.00913 13.5163 1.38163C11.8254 1.11425 10.1431 1.11425 8.48679 1.38163C8.33676 1.00085 8.11478 0.535202 7.92053 0.171926C7.90578 0.145187 7.87718 0.131357 7.84855 0.135969C6.27725 0.421802 4.7736 0.922464 3.37052 1.60293C3.35838 1.60846 3.34797 1.61769 3.34106 1.62967C0.488942 6.13013 -0.292371 10.52 0.0909151 14.8554C0.0926494 14.8766 0.103922 14.8969 0.119532 14.9098C2.00127 16.3693 3.82406 17.2554 5.61301 17.8428C5.64164 17.852 5.67197 17.8409 5.69019 17.816C6.11337 17.2057 6.49059 16.5621 6.81402 15.8853C6.83311 15.8456 6.81489 15.7986 6.77588 15.7829C6.17754 15.5432 5.6078 15.2509 5.05975 14.919C5.0164 14.8923 5.01293 14.8268 5.05281 14.7954C5.16814 14.7041 5.2835 14.6092 5.39363 14.5133C5.41355 14.4958 5.44131 14.4921 5.46474 14.5031C9.06518 16.2394 12.9631 16.2394 16.521 14.5031C16.5445 14.4912 16.5722 14.4949 16.593 14.5124C16.7032 14.6083 16.8185 14.7041 16.9347 14.7954C16.9746 14.8268 16.972 14.8923 16.9286 14.919C16.3806 15.2574 15.8108 15.5432 15.2116 15.782C15.1726 15.7977 15.1553 15.8456 15.1744 15.8853C15.5047 16.5611 15.882 17.2047 16.2973 17.8151C16.3147 17.8409 16.3459 17.852 16.3745 17.8428C18.1721 17.2554 19.9949 16.3693 21.8766 14.9098C21.8931 14.8969 21.9035 14.8775 21.9053 14.8563C22.364 9.84408 21.1369 5.49024 18.6525 1.63058C18.6465 1.61769 18.6361 1.60846 18.6239 1.60293ZM7.35169 12.2156C6.26771 12.2156 5.37454 11.1645 5.37454 9.8736C5.37454 8.58274 6.25039 7.53164 7.35169 7.53164C8.46163 7.53164 9.34616 8.59197 9.32881 9.8736C9.32881 11.1645 8.45296 12.2156 7.35169 12.2156ZM14.6619 12.2156C13.5779 12.2156 12.6847 11.1645 12.6847 9.8736C12.6847 8.58274 13.5606 7.53164 14.6619 7.53164C15.7718 7.53164 16.6563 8.59197 16.639 9.8736C16.639 11.1645 15.7718 12.2156 14.6619 12.2156Z\"\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n    />\n  </svg>\n);\n\nexport default styled(DiscordIcon)``;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/DropdownArrowIcon.tsx",
    "content": "import React, { CSSProperties } from 'react';\nimport { useTheme } from 'styled-components';\n\ninterface Props {\n  isOpen: boolean;\n  style?: CSSProperties;\n  color?: string;\n}\n\nconst DropdownArrowIcon: React.FC<Props> = ({ isOpen }) => {\n  const theme = useTheme();\n\n  return (\n    <svg\n      width=\"10\"\n      height=\"5\"\n      viewBox=\"0 0 10 5\"\n      fill=\"currentColor\"\n      stroke=\"currentColor\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      color={theme.icons.dropdownArrowIcon}\n      transform={isOpen ? 'rotate(180)' : ''}\n    >\n      <path d=\"M0.646447 0.146447C0.841709 -0.0488155 1.15829 -0.0488155 1.35355 0.146447L5 3.79289L8.64645 0.146447C8.84171 -0.0488155 9.15829 -0.0488155 9.35355 0.146447C9.54882 0.341709 9.54882 0.658291 9.35355 0.853553L5.35355 4.85355C5.15829 5.04882 4.84171 5.04882 4.64645 4.85355L0.646447 0.853553C0.451184 0.658291 0.451184 0.341709 0.646447 0.146447Z\" />\n    </svg>\n  );\n};\n\nexport default DropdownArrowIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/EditIcon.tsx",
    "content": "import React from 'react';\nimport styled, { useTheme } from 'styled-components';\n\nconst EditIcon: React.FC<{ className?: string }> = ({ className }) => {\n  const theme = useTheme();\n  return (\n    <svg\n      width=\"13\"\n      height=\"14\"\n      viewBox=\"0 0 13 14\"\n      className={className}\n      fill={theme.icons.editIcon.normal}\n      xmlns=\"http://www.w3.org/2000/svg\"\n      aria-labelledby=\"title\"\n    >\n      <title>Edit</title>\n      <path d=\"M9.53697 1.15916C10.0914 0.60473 10.9886 0.602975 11.5408 1.15524L12.5408 2.15518C13.093 2.70745 13.0913 3.60461 12.5368 4.15904L10.3564 6.33944L7.35657 3.33956L9.53697 1.15916Z\" />\n      <path d=\"M6.64946 4.04667L9.53674e-07 10.6961L0 13.696L2.99988 13.696L9.64934 7.04655L6.64946 4.04667Z\" />\n    </svg>\n  );\n};\n\nexport default styled(EditIcon)``;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/FileIcon.tsx",
    "content": "import React from 'react';\nimport { useTheme } from 'styled-components';\n\nconst FileIcon: React.FC = () => {\n  const theme = useTheme();\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 384 512\"\n      width=\"10\"\n      height=\"10\"\n      fill={theme.icons.fileIcon}\n    >\n      {/* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}\n      <path d=\"M365.3 93.38l-74.63-74.64C278.6 6.742 262.3 0 245.4 0L64-.0001c-35.35 0-64 28.65-64 64l.0065 384c0 35.34 28.65 64 64 64H320c35.2 0 64-28.8 64-64V138.6C384 121.7 377.3 105.4 365.3 93.38zM336 448c0 8.836-7.164 16-16 16H64.02c-8.838 0-16-7.164-16-16L48 64.13c0-8.836 7.164-16 16-16h160L224 128c0 17.67 14.33 32 32 32h79.1V448zM96 280C96 293.3 106.8 304 120 304h144C277.3 304 288 293.3 288 280S277.3 256 264 256h-144C106.8 256 96 266.8 96 280zM264 352h-144C106.8 352 96 362.8 96 376s10.75 24 24 24h144c13.25 0 24-10.75 24-24S277.3 352 264 352z\" />\n    </svg>\n  );\n};\n\nexport default FileIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/GitIcon.tsx",
    "content": "import React from 'react';\nimport styled from 'styled-components';\n\nconst GitIcon: React.FC<{ className?: string }> = ({ className }) => (\n  <svg\n    width=\"20\"\n    height=\"20\"\n    className={className}\n    viewBox=\"0 0 1024 1024\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z\"\n      transform=\"scale(64)\"\n    />\n  </svg>\n);\n\nexport default styled(GitIcon)``;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/IconButtonWrapper.ts",
    "content": "import styled from 'styled-components';\n\nconst IconButtonWrapper = styled.span.attrs(() => ({\n  role: 'button',\n  tabIndex: '0',\n}))`\n  height: 16px !important;\n  display: inline-block;\n  &:hover {\n    cursor: pointer;\n  }\n`;\n\nexport default IconButtonWrapper;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/InfoIcon.tsx",
    "content": "import React from 'react';\nimport { useTheme } from 'styled-components';\n\nconst InfoIcon: React.FC = () => {\n  const theme = useTheme();\n  return (\n    <svg\n      width=\"14\"\n      height=\"15\"\n      viewBox=\"0 0 14 15\"\n      fill=\"red\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M7 1.81911C3.72878 1.81911 1.07692 4.47096 1.07692 7.74219C1.07692 11.0134 3.72878 13.6653 7 13.6653C10.2712 13.6653 12.9231 11.0134 12.9231 7.74219C12.9231 4.47096 10.2712 1.81911 7 1.81911ZM0 7.74219C0 3.87619 3.13401 0.742188 7 0.742188C10.866 0.742188 14 3.87619 14 7.74219C14 11.6082 10.866 14.7422 7 14.7422C3.13401 14.7422 0 11.6082 0 7.74219Z\"\n        fill={theme.icons.infoIcon}\n      />\n      <path\n        d=\"M6 4.74219C6 4.1899 6.44772 3.74219 7 3.74219C7.55228 3.74219 8 4.1899 8 4.74219V7.74219C8 8.29447 7.55228 8.74219 7 8.74219C6.44772 8.74219 6 8.29447 6 7.74219V4.74219Z\"\n        fill={theme.icons.infoIcon}\n      />\n      <path\n        d=\"M6 11.2422C6 10.6899 6.44772 10.2422 7 10.2422C7.55228 10.2422 8 10.6899 8 11.2422C8 11.7945 7.55228 12.2422 7 12.2422C6.44772 12.2422 6 11.7945 6 11.2422Z\"\n        fill={theme.icons.infoIcon}\n      />\n    </svg>\n  );\n};\n\nexport default InfoIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/MessageToggleIcon.styled.ts",
    "content": "import styled from 'styled-components';\n\ntype Props = {\n  isOpen?: boolean;\n};\nexport const Svg = styled.svg<Props>`\n  & > path {\n    fill: ${({ theme, isOpen }) =>\n      isOpen\n        ? theme.icons.messageToggleIcon.active\n        : theme.icons.messageToggleIcon.normal};\n    &:hover {\n      fill: ${({ theme }) => theme.icons.messageToggleIcon.hover};\n    }\n    &:active {\n      fill: ${({ theme }) => theme.icons.messageToggleIcon.active};\n    }\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/MessageToggleIcon.tsx",
    "content": "import React from 'react';\nimport * as S from 'components/common/Icons/MessageToggleIcon.styled';\n\ninterface Props {\n  isOpen: boolean;\n}\n\nconst MessageToggleIcon: React.FC<Props> = ({ isOpen }) => {\n  if (isOpen) {\n    return (\n      <S.Svg\n        isOpen={isOpen}\n        width=\"16\"\n        height=\"16\"\n        viewBox=\"0 0 16 16\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <path\n          fillRule=\"evenodd\"\n          clipRule=\"evenodd\"\n          d=\"M14 16C15.1046 16 16 15.1046 16 14L16 2C16 0.895431 15.1046 -7.8281e-08 14 -1.74846e-07L2 -1.22392e-06C0.895432 -1.32048e-06 1.32048e-06 0.895429 1.22392e-06 2L1.74846e-07 14C7.8281e-08 15.1046 0.895431 16 2 16L14 16ZM5 7C4.44772 7 4 7.44771 4 8C4 8.55228 4.44772 9 5 9L11 9C11.5523 9 12 8.55228 12 8C12 7.44772 11.5523 7 11 7L5 7Z\"\n        />\n      </S.Svg>\n    );\n  }\n  return (\n    <S.Svg\n      width=\"16\"\n      height=\"16\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M0 2C0 0.895431 0.895431 0 2 0H14C15.1046 0 16 0.895431 16 2V14C16 15.1046 15.1046 16 14 16H2C0.895431 16 0 15.1046 0 14V2ZM8 4C8.55229 4 9 4.44772 9 5V7H11C11.5523 7 12 7.44772 12 8C12 8.55229 11.5523 9 11 9H9V11C9 11.5523 8.55229 12 8 12C7.44772 12 7 11.5523 7 11V9H5C4.44772 9 4 8.55228 4 8C4 7.44771 4.44772 7 5 7H7V5C7 4.44772 7.44772 4 8 4Z\"\n      />\n    </S.Svg>\n  );\n};\n\nexport default MessageToggleIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/MoonIcon.tsx",
    "content": "import React from 'react';\nimport { useTheme } from 'styled-components';\n\nconst MoonIcon: React.FC = () => {\n  const theme = useTheme();\n  return (\n    <svg\n      width=\"11\"\n      height=\"11.99\"\n      viewBox=\"0 0 12 12\"\n      fill={theme.icons.moonIcon}\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M11.6624 9.31544C11.5535 9.32132 11.4438 9.3243 11.3334 9.3243C8.01971 9.3243 5.33342 6.63801 5.33342 3.3243C5.33342 2.09476 5.70325 0.951604 6.33775 0C3.17705 0.170804 0.666748 2.78781 0.666748 5.99113C0.666748 9.30484 3.35304 11.9911 6.66675 11.9911C8.75092 11.9911 10.5869 10.9285 11.6624 9.31544Z\"\n        fill={theme.icons.moonIcon}\n      />\n    </svg>\n  );\n};\n\nexport default MoonIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/PlusIcon.tsx",
    "content": "import React from 'react';\n\nconst PlusIcon: React.FC = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 448 512\"\n      width=\"14\"\n      height=\"14\"\n    >\n      {/* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}\n      <path d=\"M432 256c0 17.69-14.33 32.01-32 32.01H256v144c0 17.69-14.33 31.99-32 31.99s-32-14.3-32-31.99v-144H48c-17.67 0-32-14.32-32-32.01s14.33-31.99 32-31.99H192v-144c0-17.69 14.33-32.01 32-32.01s32 14.32 32 32.01v144h144C417.7 224 432 238.3 432 256z\" />\n    </svg>\n  );\n};\n\nexport default PlusIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/QuestionIcon.tsx",
    "content": "import React from 'react';\nimport { useTheme } from 'styled-components';\n\nconst QuestionIcon: React.FC = () => {\n  const theme = useTheme();\n  return (\n    <svg\n      width=\"19\"\n      height=\"19\"\n      viewBox=\"0 0 19 19\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <circle cx=\"9.5\" cy=\"9.5\" r=\"8.5\" stroke=\"#5C5CF6\" strokeWidth=\"2\" />\n      <path\n        d=\"M8.31818 11.2727V11.0682C8.31818 10.5994 8.35511 10.2259 8.42898 9.94744C8.50284 9.66903 8.61222 9.44602 8.7571 9.27841C8.90199 9.10795 9.07955 8.95455 9.28977 8.81818C9.47159 8.69886 9.63352 8.58381 9.77557 8.47301C9.92045 8.36222 10.0341 8.24432 10.1165 8.11932C10.2017 7.99432 10.2443 7.85227 10.2443 7.69318C10.2443 7.55114 10.2102 7.42614 10.142 7.31818C10.0739 7.21023 9.98153 7.12642 9.86506 7.06676C9.74858 7.0071 9.61932 6.97727 9.47727 6.97727C9.32386 6.97727 9.18182 7.01278 9.05114 7.08381C8.9233 7.15483 8.8196 7.25284 8.74006 7.37784C8.66335 7.50284 8.625 7.64773 8.625 7.8125H6.44318C6.44886 7.1875 6.59091 6.6804 6.86932 6.29119C7.14773 5.89915 7.51705 5.61222 7.97727 5.4304C8.4375 5.24574 8.94318 5.15341 9.49432 5.15341C10.1023 5.15341 10.6449 5.2429 11.1222 5.42188C11.5994 5.59801 11.9759 5.86506 12.2514 6.22301C12.527 6.57812 12.6648 7.02273 12.6648 7.55682C12.6648 7.90057 12.6051 8.20312 12.4858 8.46449C12.3693 8.72301 12.206 8.9517 11.9957 9.15057C11.7884 9.34659 11.5455 9.52557 11.267 9.6875C11.0625 9.80682 10.8906 9.9304 10.7514 10.0582C10.6122 10.1832 10.5071 10.3267 10.4361 10.4886C10.3651 10.6477 10.3295 10.8409 10.3295 11.0682V11.2727H8.31818ZM9.35795 14.1364C9.02841 14.1364 8.74574 14.0213 8.50994 13.7912C8.27699 13.5582 8.16193 13.2756 8.16477 12.9432C8.16193 12.6193 8.27699 12.3423 8.50994 12.1122C8.74574 11.8821 9.02841 11.767 9.35795 11.767C9.67045 11.767 9.94602 11.8821 10.1847 12.1122C10.4261 12.3423 10.5483 12.6193 10.5511 12.9432C10.5483 13.1648 10.4901 13.3665 10.3764 13.5483C10.2656 13.7273 10.1207 13.8707 9.94176 13.9787C9.76278 14.0838 9.56818 14.1364 9.35795 14.1364Z\"\n        fill={theme.icons.savedIcon}\n      />\n    </svg>\n  );\n};\n\nexport default QuestionIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/SavedIcon.tsx",
    "content": "import React, { FC } from 'react';\nimport { useTheme } from 'styled-components';\n\nconst SavedIcon: FC = () => {\n  const theme = useTheme();\n\n  return (\n    <svg\n      width=\"18\"\n      height=\"20\"\n      viewBox=\"0 0 18 20\"\n      fill={theme.icons.savedIcon}\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M16 2H2L2 17.9873L7.29945 15.4982C8.3767 14.9922 9.6233 14.9922 10.7005 15.4982L16 17.9873V2ZM2 0C0.895431 0 0 0.895431 0 2V17.9873C0 19.4527 1.5239 20.4206 2.85027 19.7976L8.14973 17.3085C8.68835 17.0555 9.31165 17.0555 9.85027 17.3085L15.1497 19.7976C16.4761 20.4206 18 19.4527 18 17.9873V2C18 0.895431 17.1046 0 16 0H2Z\"\n        fill={theme.icons.savedIcon}\n      />\n      <path\n        d=\"M9 4L10.4401 7.01791L13.7553 7.45492L11.3301 9.75709L11.9389 13.0451L9 11.45L6.06107 13.0451L6.66991 9.75709L4.24472 7.45492L7.55993 7.01791L9 4Z\"\n        fill={theme.icons.savedIcon}\n      />\n    </svg>\n  );\n};\n\nexport default SavedIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/SearchIcon.tsx",
    "content": "import React from 'react';\n\nconst SearchIcon: React.FC = () => (\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\n    {/* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}\n    <path d=\"M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z\" />\n  </svg>\n);\n\nexport default SearchIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/SpinnerIcon.tsx",
    "content": "import React from 'react';\nimport { useTheme } from 'styled-components';\n\nconst SpinnerIcon: React.FC = () => {\n  const theme = useTheme();\n  return (\n    <svg\n      width=\"30\"\n      height=\"30\"\n      viewBox=\"0 0 120 30\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill={theme.pageLoader.borderColor}\n    >\n      {/* By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL */}\n      <circle cx=\"15\" cy=\"15\" r=\"15\">\n        <animate\n          attributeName=\"r\"\n          from=\"15\"\n          to=\"15\"\n          begin=\"0s\"\n          dur=\"0.8s\"\n          values=\"15;9;15\"\n          calcMode=\"linear\"\n          repeatCount=\"indefinite\"\n        />\n        <animate\n          attributeName=\"fill-opacity\"\n          from=\"1\"\n          to=\"1\"\n          begin=\"0s\"\n          dur=\"0.8s\"\n          values=\"1;.5;1\"\n          calcMode=\"linear\"\n          repeatCount=\"indefinite\"\n        />\n      </circle>\n      <circle cx=\"60\" cy=\"15\" r=\"9\" fillOpacity=\"0.3\">\n        <animate\n          attributeName=\"r\"\n          from=\"9\"\n          to=\"9\"\n          begin=\"0s\"\n          dur=\"0.8s\"\n          values=\"9;15;9\"\n          calcMode=\"linear\"\n          repeatCount=\"indefinite\"\n        />\n        <animate\n          attributeName=\"fill-opacity\"\n          from=\"0.5\"\n          to=\"0.5\"\n          begin=\"0s\"\n          dur=\"0.8s\"\n          values=\".5;1;.5\"\n          calcMode=\"linear\"\n          repeatCount=\"indefinite\"\n        />\n      </circle>\n      <circle cx=\"105\" cy=\"15\" r=\"15\">\n        <animate\n          attributeName=\"r\"\n          from=\"15\"\n          to=\"15\"\n          begin=\"0s\"\n          dur=\"0.8s\"\n          values=\"15;9;15\"\n          calcMode=\"linear\"\n          repeatCount=\"indefinite\"\n        />\n        <animate\n          attributeName=\"fill-opacity\"\n          from=\"1\"\n          to=\"1\"\n          begin=\"0s\"\n          dur=\"0.8s\"\n          values=\"1;.5;1\"\n          calcMode=\"linear\"\n          repeatCount=\"indefinite\"\n        />\n      </circle>\n    </svg>\n  );\n};\n\nexport default SpinnerIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/StarIcon.tsx",
    "content": "import React from 'react';\n\nconst StarIcon: React.FC = () => (\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 260 245\">\n    <path d=\"m56,237 74-228 74,228L10,96h240\" />\n  </svg>\n);\n\nexport default StarIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/SunIcon.tsx",
    "content": "import React from 'react';\nimport { useTheme } from 'styled-components';\n\nconst SunIcon: React.FC = () => {\n  const theme = useTheme();\n  return (\n    <svg\n      width=\"14\"\n      height=\"14\"\n      viewBox=\"0 0 14 14\"\n      fill={theme.icons.sunIcon}\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M11.4545 7C11.4545 9.46018 9.46018 11.4545 7 11.4545C4.53982 11.4545 2.54545 9.46018 2.54545 7C2.54545 4.53982 4.53982 2.54545 7 2.54545C9.46018 2.54545 11.4545 4.53982 11.4545 7Z\"\n        fill={theme.icons.sunIcon}\n      />\n      <path\n        d=\"M7.63636 0.636364C7.63636 0.987818 7.35145 1.27273 7 1.27273C6.64855 1.27273 6.36364 0.987818 6.36364 0.636364C6.36364 0.28491 6.64855 0 7 0C7.35145 0 7.63636 0.28491 7.63636 0.636364Z\"\n        fill={theme.icons.sunIcon}\n      />\n      <path\n        d=\"M7.63636 13.3636C7.63636 13.7151 7.35145 14 7 14C6.64855 14 6.36364 13.7151 6.36364 13.3636C6.36364 13.0122 6.64855 12.7273 7 12.7273C7.35145 12.7273 7.63636 13.0122 7.63636 13.3636Z\"\n        fill={theme.icons.sunIcon}\n      />\n      <path\n        d=\"M13.3636 7.63636C13.0122 7.63636 12.7273 7.35145 12.7273 7C12.7273 6.64855 13.0122 6.36364 13.3636 6.36364C13.7151 6.36364 14 6.64855 14 7C14 7.35145 13.7151 7.63636 13.3636 7.63636Z\"\n        fill={theme.icons.sunIcon}\n      />\n      <path\n        d=\"M0.636364 7.63636C0.28491 7.63636 -1.53625e-08 7.35145 0 7C1.53625e-08 6.64855 0.28491 6.36364 0.636364 6.36364C0.987818 6.36364 1.27273 6.64855 1.27273 7C1.27273 7.35145 0.987818 7.63636 0.636364 7.63636Z\"\n        fill={theme.icons.sunIcon}\n      />\n      <path\n        d=\"M11.9497 2.95018C11.7012 3.19869 11.2983 3.19869 11.0498 2.95018C10.8013 2.70166 10.8013 2.29874 11.0498 2.05022C11.2983 1.80171 11.7012 1.80171 11.9497 2.05022C12.1983 2.29874 12.1983 2.70166 11.9497 2.95018Z\"\n        fill={theme.icons.sunIcon}\n      />\n      <path\n        d=\"M2.95021 11.9497C2.70169 12.1982 2.29877 12.1982 2.05025 11.9497C1.80174 11.7012 1.80174 11.2983 2.05025 11.0498C2.29877 10.8012 2.70169 10.8012 2.95021 11.0498C3.19872 11.2983 3.19872 11.7012 2.95021 11.9497Z\"\n        fill={theme.icons.sunIcon}\n      />\n      <path\n        d=\"M11.0498 11.9497C10.8013 11.7012 10.8013 11.2983 11.0498 11.0498C11.2983 10.8012 11.7012 10.8012 11.9497 11.0498C12.1983 11.2983 12.1983 11.7012 11.9497 11.9497C11.7012 12.1982 11.2983 12.1982 11.0498 11.9497Z\"\n        fill={theme.icons.sunIcon}\n      />\n      <path\n        d=\"M2.05025 2.95018C1.80174 2.70166 1.80174 2.29874 2.05025 2.05022C2.29877 1.80171 2.70169 1.80171 2.95021 2.05022C3.19872 2.29874 3.19872 2.70166 2.95021 2.95018C2.70169 3.19869 2.29877 3.19869 2.05025 2.95018Z\"\n        fill={theme.icons.sunIcon}\n      />\n    </svg>\n  );\n};\n\nexport default SunIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/UserIcon.tsx",
    "content": "import React from 'react';\n\nconst UserIcon = () => {\n  return (\n    <svg\n      width=\"18\"\n      height=\"20\"\n      viewBox=\"0 0 18 20\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M9 1.75C7.20507 1.75 5.75 3.20507 5.75 5C5.75 6.79493 7.20507 8.25 9 8.25C10.7949 8.25 12.25 6.79493 12.25 5C12.25 3.20507 10.7949 1.75 9 1.75ZM4.25 5C4.25 2.37665 6.37665 0.25 9 0.25C11.6234 0.25 13.75 2.37665 13.75 5C13.75 7.62335 11.6234 9.75 9 9.75C6.37665 9.75 4.25 7.62335 4.25 5ZM1.64124 13.6412C2.53204 12.7504 3.74022 12.25 5 12.25H13C14.2598 12.25 15.468 12.7504 16.3588 13.6412C17.2496 14.532 17.75 15.7402 17.75 17V19C17.75 19.4142 17.4142 19.75 17 19.75C16.5858 19.75 16.25 19.4142 16.25 19V17C16.25 16.138 15.9076 15.3114 15.2981 14.7019C14.6886 14.0924 13.862 13.75 13 13.75H5C4.13805 13.75 3.3114 14.0924 2.7019 14.7019C2.09241 15.3114 1.75 16.138 1.75 17V19C1.75 19.4142 1.41421 19.75 1 19.75C0.585786 19.75 0.25 19.4142 0.25 19V17C0.25 15.7402 0.750445 14.532 1.64124 13.6412Z\"\n        fill=\"#4C4CFF\"\n      />\n    </svg>\n  );\n};\n\nexport default UserIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/VerticalElipsisIcon.tsx",
    "content": "import React from 'react';\nimport { useTheme } from 'styled-components';\n\nconst VerticalElipsisIcon: React.FC = () => {\n  const theme = useTheme();\n  return (\n    <svg\n      width=\"4\"\n      height=\"16\"\n      viewBox=\"0 0 4 16\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M2 4C3.1 4 4 3.1 4 2C4 0.9 3.1 0 2 0C0.9 0 0 0.9 0 2C0 3.1 0.9 4 2 4ZM2 6C0.9 6 0 6.9 0 8C0 9.1 0.9 10 2 10C3.1 10 4 9.1 4 8C4 6.9 3.1 6 2 6ZM2 12C0.9 12 0 12.9 0 14C0 15.1 0.9 16 2 16C3.1 16 4 15.1 4 14C4 12.9 3.1 12 2 12Z\"\n        fill={theme.icons.verticalElipsisIcon}\n      />\n    </svg>\n  );\n};\n\nexport default VerticalElipsisIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/WarningIcon.tsx",
    "content": "import React from 'react';\nimport styled from 'styled-components';\n\nconst WarningIconContainer = styled.span`\n  align-items: center;\n  display: inline-flex;\n  justify-content: center;\n  height: 1.5rem;\n  width: 1.5rem;\n`;\n\nconst WarningIcon: React.FC = () => {\n  return (\n    <WarningIconContainer>\n      <svg\n        role=\"img\"\n        width=\"14\"\n        height=\"13\"\n        viewBox=\"0 0 14 13\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <path\n          fillRule=\"evenodd\"\n          clipRule=\"evenodd\"\n          d=\"M8.09265 1.06679C7.60703 0.250524 6.39297 0.250524 5.90735 1.06679L0.170916 10.7089C-0.314707 11.5252 0.292322 12.5455 1.26357 12.5455H12.7364C13.7077 12.5455 14.3147 11.5252 13.8291 10.7089L8.09265 1.06679ZM6 5.00006C6 4.44778 6.44772 4.00006 7 4.00006C7.55228 4.00006 8 4.44778 8 5.00006V7.00006C8 7.55235 7.55228 8.00006 7 8.00006C6.44772 8.00006 6 7.55235 6 7.00006V5.00006ZM6 10.0001C6 9.44778 6.44772 9.00006 7 9.00006C7.55228 9.00006 8 9.44778 8 10.0001C8 10.5523 7.55228 11.0001 7 11.0001C6.44772 11.0001 6 10.5523 6 10.0001Z\"\n          fill=\"#F2C94C\"\n        />\n      </svg>\n    </WarningIconContainer>\n  );\n};\n\nexport default WarningIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Icons/WarningRedIcon.tsx",
    "content": "import React from 'react';\nimport { useTheme } from 'styled-components';\n\nconst WarningRedIcon: React.FC = () => {\n  const theme = useTheme();\n  return (\n    <svg\n      width=\"20\"\n      height=\"20\"\n      viewBox=\"0 0 20 20\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <rect\n        width=\"20\"\n        height=\"20\"\n        rx=\"10\"\n        fill={theme.icons.warningRedIcon.rectFill}\n      />\n      <path\n        d=\"M9 4.74219H11V12.7422H9V4.74219Z\"\n        fill={theme.icons.warningRedIcon.pathFill}\n      />\n      <path\n        d=\"M9 14.7422C9 14.1899 9.44772 13.7422 10 13.7422C10.5523 13.7422 11 14.1899 11 14.7422C11 15.2945 10.5523 15.7422 10 15.7422C9.44772 15.7422 9 15.2945 9 14.7422Z\"\n        fill={theme.icons.warningRedIcon.pathFill}\n      />\n    </svg>\n  );\n};\n\nexport default WarningRedIcon;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/IndeterminateCheckbox/IndeterminateCheckbox.tsx",
    "content": "import React, { HTMLProps } from 'react';\nimport styled from 'styled-components';\n\ninterface IndeterminateCheckboxProps extends HTMLProps<HTMLInputElement> {\n  indeterminate?: boolean;\n}\n\nconst IndeterminateCheckbox: React.FC<IndeterminateCheckboxProps> = ({\n  indeterminate,\n  ...rest\n}) => {\n  const ref = React.useRef<HTMLInputElement>(null);\n  React.useEffect(() => {\n    if (typeof indeterminate === 'boolean' && ref.current) {\n      ref.current.indeterminate = !rest.checked && indeterminate;\n    }\n  }, [ref, indeterminate]);\n\n  return <input type=\"checkbox\" ref={ref} {...rest} />;\n};\n\nexport default styled(IndeterminateCheckbox)`\n  cursor: pointer;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Input/Input.styled.ts",
    "content": "import styled, { css } from 'styled-components';\n\nexport interface InputProps {\n  inputSize?: 'S' | 'M' | 'L';\n  search: boolean;\n}\n\nconst INPUT_SIZES = {\n  S: '24px',\n  M: '32px',\n  L: '40px',\n};\n\nexport const Wrapper = styled.div`\n  position: relative;\n  &:hover {\n    svg:first-child {\n      fill: ${({ theme }) => theme.input.icon.hover};\n    }\n  }\n  svg:first-child {\n    position: absolute;\n    top: 8px;\n    line-height: 0;\n    z-index: 1;\n    left: 12px;\n    right: unset;\n    height: 16px;\n    width: 16px;\n    fill: ${({ theme }) => theme.input.icon.color};\n  }\n  svg:last-child {\n    position: absolute;\n    top: 8px;\n    line-height: 0;\n    z-index: 1;\n    left: unset;\n    right: 12px;\n    height: 16px;\n    width: 16px;\n  }\n`;\n\nexport const Input = styled.input<InputProps>(\n  ({ theme: { input }, inputSize, search }) => css`\n    background-color: ${input.backgroundColor.normal};\n    border: 1px ${input.borderColor.normal} solid;\n    border-radius: 4px;\n    color: ${input.color.normal};\n    height: ${inputSize && INPUT_SIZES[inputSize]\n      ? INPUT_SIZES[inputSize]\n      : '40px'};\n    width: 100%;\n    padding-left: ${search ? '36px' : '12px'};\n    font-size: 14px;\n\n    &::placeholder {\n      color: ${input.color.placeholder.normal};\n      font-size: 14px;\n    }\n    &:hover {\n      border-color: ${input.borderColor.hover};\n    }\n    &:focus {\n      outline: none;\n      border-color: ${input.borderColor.focus};\n      &::placeholder {\n        color: transparent;\n      }\n    }\n    &:disabled {\n      color: ${input.color.disabled};\n      border-color: ${input.borderColor.disabled};\n      background-color: ${input.backgroundColor.disabled};\n      cursor: not-allowed;\n    }\n    &:read-only {\n      color: ${input.color.readOnly};\n      border: none;\n      background-color: ${input.backgroundColor.readOnly};\n      &:focus {\n        &::placeholder {\n          color: ${input.color.placeholder.readOnly};\n        }\n      }\n      cursor: not-allowed;\n    }\n  `\n);\n\nexport const FormError = styled.p`\n  color: ${({ theme }) => theme.input.error};\n  font-size: 12px;\n`;\n\nexport const InputHint = styled.p`\n  font-size: 0.85rem;\n  margin-top: 0.25rem;\n  color: ${({ theme }) => theme.clusterConfigForm.inputHintText.secondary};\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Input/Input.tsx",
    "content": "import React from 'react';\nimport { RegisterOptions, useFormContext } from 'react-hook-form';\nimport SearchIcon from 'components/common/Icons/SearchIcon';\nimport { ErrorMessage } from '@hookform/error-message';\n\nimport * as S from './Input.styled';\nimport { InputLabel } from './InputLabel.styled';\n\nexport interface InputProps\n  extends React.InputHTMLAttributes<HTMLInputElement>,\n    Omit<S.InputProps, 'search'> {\n  name?: string;\n  hookFormOptions?: RegisterOptions;\n  search?: boolean;\n  positiveOnly?: boolean;\n  withError?: boolean;\n  label?: React.ReactNode;\n  hint?: React.ReactNode;\n  clearIcon?: React.ReactNode;\n\n  // Some may only accept integer, like `Number of Partitions`\n  // some may accept decimal\n  integerOnly?: boolean;\n}\n\nfunction inputNumberCheck(\n  key: string,\n  positiveOnly: boolean,\n  integerOnly: boolean,\n  getValues: (name: string) => string,\n  componentName: string\n) {\n  let isValid = true;\n  if (!((key >= '0' && key <= '9') || key === '-' || key === '.')) {\n    // If not a valid digit char.\n    isValid = false;\n  } else {\n    // If there is any restriction.\n    if (positiveOnly) {\n      isValid = !(key === '-');\n    }\n    if (isValid && integerOnly) {\n      isValid = !(key === '.');\n    }\n\n    // Check invalid format\n    const value = getValues(componentName);\n\n    if (isValid && (key === '-' || key === '.')) {\n      if (!positiveOnly) {\n        if (key === '-') {\n          if (value !== '') {\n            // '-' should not appear anywhere except the start of the string\n            isValid = false;\n          }\n        }\n      }\n      if (!integerOnly) {\n        if (key === '.') {\n          if (value === '' || value.indexOf('.') !== -1) {\n            // '.' should not appear at the start of the string or appear twice\n            isValid = false;\n          }\n        }\n      }\n    }\n  }\n  return isValid;\n}\n\nfunction pasteNumberCheck(\n  text: string,\n  positiveOnly: boolean,\n  integerOnly: boolean\n) {\n  let value: string;\n  value = text;\n  let sign = '';\n  if (!positiveOnly) {\n    if (value.charAt(0) === '-') {\n      sign = '-';\n    }\n  }\n  if (integerOnly) {\n    value = value.replace(/\\D/g, '');\n  } else {\n    value = value.replace(/[^\\d.]/g, '');\n    if (value.indexOf('.') !== value.lastIndexOf('.')) {\n      const strs = value.split('.');\n      value = '';\n      for (let i = 0; i < strs.length; i += 1) {\n        value += strs[i];\n        if (i === 0) {\n          value += '.';\n        }\n      }\n    }\n  }\n  value = sign + value;\n  return value;\n}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {\n  const {\n    name,\n    hookFormOptions,\n    search,\n    inputSize = 'L',\n    type,\n    positiveOnly,\n    integerOnly,\n    withError = false,\n    label,\n    hint,\n    clearIcon,\n    ...rest\n  } = props;\n\n  const methods = useFormContext();\n\n  const fieldId = React.useId();\n\n  const isHookFormField = !!name && !!methods.register;\n\n  const keyPressEventHandler = (\n    event: React.KeyboardEvent<HTMLInputElement>\n  ) => {\n    const { key } = event;\n    if (type === 'number') {\n      // Manually prevent input of non-digit and non-minus for all number inputs\n      // and prevent input of negative numbers for positiveOnly inputs\n      if (\n        !inputNumberCheck(\n          key,\n          typeof positiveOnly === 'boolean' ? positiveOnly : false,\n          typeof integerOnly === 'boolean' ? integerOnly : false,\n          methods.getValues,\n          typeof name === 'string' ? name : ''\n        )\n      ) {\n        event.preventDefault();\n      }\n    }\n  };\n  const pasteEventHandler = (event: React.ClipboardEvent<HTMLInputElement>) => {\n    if (type === 'number') {\n      const { clipboardData } = event;\n      // The 'clipboardData' does not have key 'Text', but has key 'text' instead.\n      const text = clipboardData.getData('text');\n      // Check the format of pasted text.\n      const value = pasteNumberCheck(\n        text,\n        typeof positiveOnly === 'boolean' ? positiveOnly : false,\n        typeof integerOnly === 'boolean' ? integerOnly : false\n      );\n      // if paste value contains non-numeric characters or\n      // negative for positiveOnly fields then prevent paste\n      if (value !== text) {\n        event.preventDefault();\n\n        // for react-hook-form fields only set transformed value\n        if (isHookFormField) {\n          methods.setValue(name, value);\n        }\n      }\n    }\n  };\n\n  let inputOptions = { ...rest };\n  if (isHookFormField) {\n    // extend input options with react-hook-form options\n    // if the field is a part of react-hook-form form\n    inputOptions = { ...rest, ...methods.register(name, hookFormOptions) };\n  }\n  return (\n    <div>\n      {label && <InputLabel htmlFor={rest.id || fieldId}>{label}</InputLabel>}\n      <S.Wrapper>\n        {search && <SearchIcon />}\n        <S.Input\n          id={fieldId}\n          inputSize={inputSize}\n          search={!!search}\n          type={type}\n          onKeyPress={keyPressEventHandler}\n          onPaste={pasteEventHandler}\n          ref={ref}\n          {...inputOptions}\n        />\n        {clearIcon}\n\n        {withError && isHookFormField && (\n          <S.FormError>\n            <ErrorMessage name={name} />\n          </S.FormError>\n        )}\n        {hint && <S.InputHint>{hint}</S.InputHint>}\n      </S.Wrapper>\n    </div>\n  );\n});\n\nexport default Input;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Input/InputLabel.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const InputLabel = styled.label`\n  font-weight: 500;\n  font-size: 12px;\n  line-height: 20px;\n  color: ${({ theme }) => theme.input.label.color};\n  input[type='checkbox'] {\n    display: inline-block;\n    margin-right: 8px;\n    vertical-align: text-top;\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Input/__tests__/Input.spec.tsx",
    "content": "import Input, { InputProps } from 'components/common/Input/Input';\nimport React from 'react';\nimport { screen } from '@testing-library/react';\nimport { render } from 'lib/testHelpers';\nimport userEvent from '@testing-library/user-event';\n\n// Mock useFormContext\nlet component: HTMLInputElement;\n\nconst setupWrapper = (props?: Partial<InputProps>) => (\n  <Input name=\"test\" {...props} />\n);\njest.mock('react-hook-form', () => ({\n  useFormContext: () => ({\n    register: jest.fn(),\n\n    // Mock methods.getValues and methods.setValue\n    getValues: jest.fn(() => {\n      return component.value;\n    }),\n    setValue: jest.fn((key, val) => {\n      component.value = val;\n    }),\n  }),\n}));\n\ndescribe('Custom Input', () => {\n  describe('with no icons', () => {\n    const getInput = () => screen.getByRole('textbox');\n\n    it('to be in the document', () => {\n      render(setupWrapper());\n      expect(getInput()).toBeInTheDocument();\n    });\n  });\n  describe('number', () => {\n    const getInput = () => screen.getByRole<HTMLInputElement>('spinbutton');\n\n    describe('input', () => {\n      it('allows user to type numbers only', async () => {\n        render(setupWrapper({ type: 'number' }));\n        const input = getInput();\n        component = input;\n        await userEvent.type(input, 'abc131');\n        expect(input).toHaveValue(131);\n      });\n\n      it('allows user to type negative values', async () => {\n        render(setupWrapper({ type: 'number' }));\n        const input = getInput();\n        component = input;\n        await userEvent.type(input, '-2');\n        expect(input).toHaveValue(-2);\n      });\n\n      it('allows user to type positive values only', async () => {\n        render(setupWrapper({ type: 'number', positiveOnly: true }));\n        const input = getInput();\n        component = input;\n        await userEvent.type(input, '-2');\n        expect(input).toHaveValue(2);\n      });\n\n      it('allows user to type decimal', async () => {\n        render(setupWrapper({ type: 'number' }));\n        const input = getInput();\n        component = input;\n        await userEvent.type(input, '2.3');\n        expect(input).toHaveValue(2.3);\n      });\n\n      it('allows user to type integer only', async () => {\n        render(setupWrapper({ type: 'number', integerOnly: true }));\n        const input = getInput();\n        component = input;\n        await userEvent.type(input, '2.3');\n        expect(input).toHaveValue(23);\n      });\n\n      it(\"not allow '-' appear at any position of the string except the start\", async () => {\n        render(setupWrapper({ type: 'number' }));\n        const input = getInput();\n        component = input;\n        await userEvent.type(input, '2-3');\n        expect(input).toHaveValue(23);\n      });\n\n      it(\"not allow '.' appear at the start of the string\", async () => {\n        render(setupWrapper({ type: 'number' }));\n        const input = getInput();\n        component = input;\n        await userEvent.type(input, '.33');\n        expect(input).toHaveValue(33);\n      });\n\n      it(\"not allow '.' appear twice in the string\", async () => {\n        render(setupWrapper({ type: 'number' }));\n        const input = getInput();\n        component = input;\n        await userEvent.type(input, '3.3.3');\n        expect(input).toHaveValue(3.33);\n      });\n    });\n\n    describe('paste', () => {\n      it('allows user to paste numbers only', async () => {\n        render(setupWrapper({ type: 'number' }));\n        const input = getInput();\n        component = input;\n        await userEvent.click(input);\n        await userEvent.paste('abc131');\n        expect(input).toHaveValue(131);\n      });\n\n      it('allows user to paste negative values', async () => {\n        render(setupWrapper({ type: 'number' }));\n        const input = getInput();\n        component = input;\n        await userEvent.click(input);\n        await userEvent.paste('-2');\n        expect(input).toHaveValue(-2);\n      });\n\n      it('allows user to paste positive values only', async () => {\n        render(setupWrapper({ type: 'number', positiveOnly: true }));\n        const input = getInput();\n        component = input;\n        await userEvent.click(input);\n        await userEvent.paste('-2');\n        expect(input).toHaveValue(2);\n      });\n\n      it('allows user to paste decimal', async () => {\n        render(setupWrapper({ type: 'number' }));\n        const input = getInput();\n        component = input;\n        await userEvent.click(input);\n        await userEvent.paste('2.3');\n        expect(input).toHaveValue(2.3);\n      });\n\n      it('allows user to paste integer only', async () => {\n        render(setupWrapper({ type: 'number', integerOnly: true }));\n        const input = getInput();\n        component = input;\n        await userEvent.click(input);\n        await userEvent.paste('2.3');\n        expect(input).toHaveValue(23);\n      });\n\n      it(\"not allow '-' appear at any position of the pasted string except the start\", async () => {\n        render(setupWrapper({ type: 'number' }));\n        const input = getInput();\n        component = input;\n        await userEvent.click(input);\n        await userEvent.paste('2-3');\n        expect(input).toHaveValue(23);\n      });\n\n      it(\"not allow '.' appear at the start of the pasted string\", async () => {\n        render(setupWrapper({ type: 'number' }));\n        const input = getInput();\n        component = input;\n        await userEvent.click(input);\n        await userEvent.paste('.33');\n        expect(input).toHaveValue(0.33);\n      });\n\n      it(\"not allow '.' appear twice in the pasted string\", async () => {\n        render(setupWrapper({ type: 'number' }));\n        const input = getInput();\n        component = input;\n        await userEvent.click(input);\n        await userEvent.paste('3.3.3');\n        expect(input).toHaveValue(3.33);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Logo/Logo.tsx",
    "content": "import React from 'react';\n\nconst Logo: React.FC = () => {\n  return (\n    <svg\n      width=\"23\"\n      height=\"30\"\n      viewBox=\"0 0 23 30\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M2.17668 0C2.17668 0 8.45218 9.02115 19.6155 13.3524C19.6155 13.3524 27.0635 7.06532 19.862 16.1041C12.6605 25.1428 1.6961 30.617 2.17668 29.9444C3.60584 27.9442 8.31948 24.1222 5.91024 21.7649C10.6395 17.1375 0 14.0868 0 14.0868C2.75705 8.06572 2.17668 0 2.17668 0Z\"\n        fill=\"#4F4FFF\"\n      />\n    </svg>\n  );\n};\n\nexport default Logo;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Metrics/Indicator.tsx",
    "content": "import React, { PropsWithChildren } from 'react';\nimport SpinnerIcon from 'components/common/Icons/SpinnerIcon';\n\nimport * as S from './Metrics.styled';\n\nexport interface Props {\n  fetching?: boolean;\n  isAlert?: boolean;\n  label: React.ReactNode;\n  title?: string;\n  alertType?: 'success' | 'error' | 'warning' | 'info';\n}\n\nconst Indicator: React.FC<PropsWithChildren<Props>> = ({\n  label,\n  title,\n  fetching,\n  isAlert,\n  alertType = 'error',\n  children,\n}) => (\n  <S.IndicatorWrapper>\n    <div title={title}>\n      <S.IndicatorTitle>\n        {label}{' '}\n        {isAlert && (\n          <S.CircularAlertWrapper>\n            <S.CircularAlert $type={alertType} />\n          </S.CircularAlertWrapper>\n        )}\n      </S.IndicatorTitle>\n      <span>{fetching ? <SpinnerIcon /> : children}</span>\n    </div>\n  </S.IndicatorWrapper>\n);\n\nexport default Indicator;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Metrics/Metrics.styled.tsx",
    "content": "import styled, { css } from 'styled-components';\n\nexport const Wrapper = styled.div`\n  padding: 1.5rem 1rem;\n  background: ${({ theme }) => theme.metrics.backgroundColor};\n  margin-bottom: 0.5rem !important;\n  display: flex;\n  gap: 16px;\n  flex-wrap: wrap;\n`;\n\nexport const IndicatorWrapper = styled.div`\n  background-color: ${({ theme }) => theme.default.backgroundColor};\n  height: 68px;\n  width: fit-content;\n  min-width: 150px;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: flex-start;\n  padding: 12px 16px;\n  box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.08);\n  flex-grow: 1;\n  color: ${({ theme }) => theme.default.color.normal};\n`;\n\nexport const IndicatorTitle = styled.div`\n  font-weight: 500;\n  font-size: 12px;\n  color: ${({ theme }) => theme.metrics.indicator.titleColor};\n  display: flex;\n  align-items: center;\n  gap: 10px;\n`;\n\nexport const IndicatorsWrapper = styled.div`\n  display: flex;\n  gap: 2px;\n  flex-wrap: wrap;\n  border-radius: 8px;\n  overflow: auto;\n  box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.08);\n  color: ${({ theme }) => theme.metrics.wrapper};\n`;\n\nexport const SectionTitle = styled.h5`\n  font-weight: 500;\n  margin: 0 0 0.5rem 16px;\n  font-size: 100%;\n  color: ${({ theme }) => theme.metrics.sectionTitle};\n`;\n\nexport const LightText = styled.span`\n  color: ${({ theme }) => theme.metrics.indicator.lightTextColor};\n  font-size: 14px;\n`;\n\nexport const RedText = styled.span`\n  color: ${({ theme }) => theme.metrics.indicator.warningTextColor};\n  font-size: 14px;\n`;\n\nexport const CircularAlertWrapper = styled.svg.attrs({\n  role: 'svg',\n  viewBox: '0 0 4 4',\n  xmlns: 'http://www.w3.org/2000/svg',\n})`\n  grid-area: status;\n  fill: none;\n  width: 4px;\n  height: 4px;\n`;\n\nexport const CircularAlert = styled.circle.attrs({\n  role: 'circle',\n  cx: 2,\n  cy: 2,\n  r: 2,\n})<{\n  $type: 'error' | 'success' | 'warning' | 'info';\n}>(\n  ({ theme, $type }) => css`\n    fill: ${theme.circularAlert.color[$type]};\n  `\n);\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Metrics/Section.tsx",
    "content": "import React, { PropsWithChildren } from 'react';\n\nimport * as S from './Metrics.styled';\n\ninterface Props {\n  title?: string;\n}\n\nconst Section: React.FC<PropsWithChildren<Props>> = ({ title, children }) => (\n  <div role=\"group\">\n    {title && <S.SectionTitle>{title}</S.SectionTitle>}\n    <S.IndicatorsWrapper>{children}</S.IndicatorsWrapper>\n  </div>\n);\n\nexport default Section;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Metrics/__tests__/Indicator.spec.tsx",
    "content": "import React from 'react';\nimport { Indicator } from 'components/common/Metrics';\nimport { screen } from '@testing-library/react';\nimport { render } from 'lib/testHelpers';\nimport { Props } from 'components/common/Metrics/Indicator';\nimport { theme } from 'theme/theme';\n\nconst title = 'Test Title';\nconst label = 'Test Label';\nconst child = 'Child';\n\ndescribe('Indicator', () => {\n  const setupComponent = (props: Partial<Props> = {}) =>\n    render(\n      <Indicator title={props.title} label={props.label} {...props}>\n        {child}\n      </Indicator>\n    );\n\n  it('renders indicator', () => {\n    setupComponent({ title, label });\n    expect(screen.getByTitle(title)).toBeInTheDocument();\n    expect(screen.getByText(label)).toBeInTheDocument();\n    expect(screen.getByText(child)).toBeInTheDocument();\n  });\n\n  describe('should render circular alert', () => {\n    const getCircle = () => screen.getByRole('circle');\n\n    it('should be in document', () => {\n      setupComponent({ title, label, isAlert: true });\n      expect(screen.getByRole('svg')).toBeInTheDocument();\n      expect(getCircle()).toBeInTheDocument();\n    });\n\n    it('success alert', () => {\n      setupComponent({ title, label, isAlert: true, alertType: 'success' });\n      expect(getCircle()).toHaveStyle(\n        `fill: ${theme.circularAlert.color.success}`\n      );\n    });\n\n    it('error alert', () => {\n      setupComponent({ title, label, isAlert: true, alertType: 'error' });\n      expect(getCircle()).toHaveStyle(\n        `fill: ${theme.circularAlert.color.error}`\n      );\n    });\n\n    it('warning alert', () => {\n      setupComponent({ title, label, isAlert: true, alertType: 'warning' });\n      expect(getCircle()).toHaveStyle(\n        `fill: ${theme.circularAlert.color.warning}`\n      );\n    });\n\n    it('info alert', () => {\n      setupComponent({ title, label, isAlert: true, alertType: 'info' });\n      expect(getCircle()).toHaveStyle(\n        `fill: ${theme.circularAlert.color.info}`\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Metrics/__tests__/Section.spec.tsx",
    "content": "import React from 'react';\nimport { Section } from 'components/common/Metrics';\nimport { screen } from '@testing-library/react';\nimport { render } from 'lib/testHelpers';\n\nconst child = 'Child';\nconst title = 'Test Title';\n\ndescribe('Metrics.Section', () => {\n  it('renders without title', () => {\n    render(<Section>{child}</Section>);\n    expect(screen.queryByRole('heading')).not.toBeInTheDocument();\n    expect(screen.getByText(child)).toBeInTheDocument();\n  });\n\n  it('renders with title', () => {\n    render(<Section title={title}>{child}</Section>);\n    expect(screen.queryByRole('heading')).toBeInTheDocument();\n    expect(screen.getByText(child)).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Metrics/index.tsx",
    "content": "import Section from 'components/common/Metrics/Section';\nimport Indicator from 'components/common/Metrics/Indicator';\n\nexport { Wrapper, LightText, RedText } from './Metrics.styled';\n\nexport { Section, Indicator };\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/MultiSelect/MultiSelect.styled.ts",
    "content": "import styled from 'styled-components';\nimport { MultiSelect as ReactMultiSelect } from 'react-multi-select-component';\n\nconst MultiSelect = styled(ReactMultiSelect)<{\n  minWidth?: string;\n  height?: string;\n}>`\n  min-width: ${({ minWidth }) => minWidth || '200px;'};\n  height: ${({ height }) => height ?? '32px'};\n  font-size: 14px;\n  .search input {\n    color: ${({ theme }) => theme.input.color.normal};\n    background-color: ${(props) =>\n      props.theme.input.backgroundColor.normal} !important;\n  }\n  .select-item {\n    color: ${({ theme }) => theme.select.color.normal};\n    background-color: ${({ theme }) =>\n      theme.select.backgroundColor.normal} !important;\n\n    &:active {\n      background-color: ${({ theme }) =>\n        theme.select.backgroundColor.active} !important;\n    }\n  }\n\n  .select-item.selected {\n    background-color: ${({ theme }) =>\n      theme.select.backgroundColor.active} !important;\n  }\n  .options li {\n    background-color: ${({ theme }) =>\n      theme.select.backgroundColor.normal} !important;\n  }\n  & > .dropdown-container {\n    background-color: ${({ theme }) =>\n      theme.input.backgroundColor.normal} !important;\n    border-color: ${({ theme }) => theme.select.borderColor.normal} !important;\n    &:hover {\n      border-color: ${({ theme }) => theme.select.borderColor.hover} !important;\n    }\n\n    height: ${({ height }) => height ?? '32px'};\n    * {\n      cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};\n    }\n\n    & > .dropdown-heading {\n      height: ${({ height }) => height ?? '32px'};\n      color: ${({ disabled, theme }) =>\n        disabled\n          ? theme.select.color.disabled\n          : theme.select.color.active} !important;\n      & > .clear-selected-button {\n        display: none;\n      }\n      &:hover {\n        & > .clear-selected-button {\n          display: block;\n        }\n      }\n    }\n  }\n`;\n\nexport default MultiSelect;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Navigation/Navbar.styled.ts",
    "content": "import styled from 'styled-components';\n\nconst Navbar = styled.nav`\n  display: flex;\n  border-bottom: 1px ${({ theme }) => theme.primaryTab.borderColor.nav} solid;\n  height: ${({ theme }) => theme.primaryTab.height};\n  & a {\n    height: 40px;\n    min-width: 96px;\n    padding: 0 16px;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    font-weight: 500;\n    font-size: 14px;\n    white-space: nowrap;\n    color: ${({ theme }) => theme.primaryTab.color.normal};\n    border-bottom: 1px ${({ theme }) => theme.default.transparentColor} solid;\n    &.is-active {\n      border-bottom: 1px ${({ theme }) => theme.primaryTab.borderColor.active}\n        solid;\n      color: ${({ theme }) => theme.primaryTab.color.active};\n    }\n    &.is-disabled {\n      color: ${(props) => props.theme.primaryTab.color.disabled};\n      border-bottom: 1px ${({ theme }) => theme.default.transparentColor};\n      cursor: not-allowed;\n    }\n    &:hover:not(.is-active, .is-disabled) {\n      border-bottom: 1px ${({ theme }) => theme.default.transparentColor} solid;\n      color: ${({ theme }) => theme.primaryTab.color.hover};\n    }\n  }\n`;\n\nexport default Navbar;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/NewTable/ColoredCell.tsx",
    "content": "import React from 'react';\nimport styled from 'styled-components';\n\ninterface CellProps {\n  isWarning?: boolean;\n  isAttention?: boolean;\n}\n\ninterface ColoredCellProps {\n  value: number | string;\n  warn?: boolean;\n  attention?: boolean;\n}\n\nconst Cell = styled.div<CellProps>`\n  color: ${(props) => {\n    if (props.isAttention) {\n      return props.theme.table.colored.color.attention;\n    }\n\n    if (props.isWarning) {\n      return props.theme.table.colored.color.warning;\n    }\n\n    return 'inherit';\n  }};\n`;\n\nconst ColoredCell: React.FC<ColoredCellProps> = ({\n  value,\n  warn,\n  attention,\n}) => {\n  return (\n    <Cell isWarning={warn} isAttention={attention}>\n      {value}\n    </Cell>\n  );\n};\n\nexport default ColoredCell;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/NewTable/ExpanderCell.tsx",
    "content": "import { CellContext } from '@tanstack/react-table';\nimport React from 'react';\n\nimport * as S from './Table.styled';\n\nconst ExpanderCell: React.FC<CellContext<unknown, unknown>> = ({ row }) => {\n  return (\n    <S.ExpaderButton\n      width=\"16\"\n      height=\"20\"\n      viewBox=\"0 -2 16 16\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      role=\"button\"\n      aria-label=\"Expand row\"\n      $disabled={!row.getCanExpand()}\n      getIsExpanded={row.getIsExpanded()}\n    >\n      {row.getIsExpanded() ? (\n        <path\n          fillRule=\"evenodd\"\n          clipRule=\"evenodd\"\n          d=\"M14 16C15.1046 16 16 15.1046 16 14L16 2C16 0.895431 15.1046 -7.8281e-08 14 -1.74846e-07L2 -1.22392e-06C0.895432 -1.32048e-06 1.32048e-06 0.895429 1.22392e-06 2L1.74846e-07 14C7.8281e-08 15.1046 0.895431 16 2 16L14 16ZM5 7C4.44772 7 4 7.44771 4 8C4 8.55228 4.44772 9 5 9L11 9C11.5523 9 12 8.55228 12 8C12 7.44772 11.5523 7 11 7L5 7Z\"\n        />\n      ) : (\n        <path\n          fillRule=\"evenodd\"\n          clipRule=\"evenodd\"\n          d=\"M0 2C0 0.895431 0.895431 0 2 0H14C15.1046 0 16 0.895431 16 2V14C16 15.1046 15.1046 16 14 16H2C0.895431 16 0 15.1046 0 14V2ZM8 4C8.55229 4 9 4.44772 9 5V7H11C11.5523 7 12 7.44772 12 8C12 8.55229 11.5523 9 11 9H9V11C9 11.5523 8.55229 12 8 12C7.44772 12 7 11.5523 7 11V9H5C4.44772 9 4 8.55228 4 8C4 7.44771 4.44772 7 5 7H7V5C7 4.44772 7.44772 4 8 4Z\"\n        />\n      )}\n    </S.ExpaderButton>\n  );\n};\n\nexport default ExpanderCell;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/NewTable/LinkCell.tsx",
    "content": "import React from 'react';\nimport { NavLink } from 'react-router-dom';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst LinkCell = ({ value, to = '' }: any) => {\n  const handleClick: React.MouseEventHandler = (e) => e.stopPropagation();\n  return (\n    <NavLink to={to} title={value} onClick={handleClick}>\n      {value}\n    </NavLink>\n  );\n};\n\nexport default LinkCell;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/NewTable/SelectRowCell.tsx",
    "content": "import { CellContext } from '@tanstack/react-table';\nimport React from 'react';\nimport IndeterminateCheckbox from 'components/common/IndeterminateCheckbox/IndeterminateCheckbox';\n\nconst SelectRowCell: React.FC<CellContext<unknown, unknown>> = ({ row }) => (\n  <IndeterminateCheckbox\n    checked={row.getIsSelected()}\n    disabled={!row.getCanSelect()}\n    indeterminate={row.getIsSomeSelected()}\n    onChange={row.getToggleSelectedHandler()}\n  />\n);\n\nexport default SelectRowCell;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/NewTable/SelectRowHeader.tsx",
    "content": "import { HeaderContext } from '@tanstack/react-table';\nimport React from 'react';\nimport IndeterminateCheckbox from 'components/common/IndeterminateCheckbox/IndeterminateCheckbox';\n\nconst SelectRowHeader: React.FC<HeaderContext<unknown, unknown>> = ({\n  table,\n}) => (\n  <IndeterminateCheckbox\n    checked={table.getIsAllRowsSelected()}\n    indeterminate={table.getIsSomeRowsSelected()}\n    onChange={table.getToggleAllRowsSelectedHandler()}\n  />\n);\n\nexport default SelectRowHeader;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/NewTable/SizeCell.tsx",
    "content": "import React from 'react';\nimport { CellContext } from '@tanstack/react-table';\nimport BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AsAny = any;\n\nconst SizeCell: React.FC<\n  CellContext<AsAny, unknown> & { renderSegments?: boolean; precision?: number }\n> = ({ getValue, row, renderSegments = false, precision = 0 }) => (\n  <>\n    <BytesFormatted value={getValue<string | number>()} precision={precision} />\n    {renderSegments ? `, ${row?.original.count} segment(s)` : null}\n  </>\n);\n\nexport default SizeCell;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/NewTable/Table.styled.ts",
    "content": "import styled, { css } from 'styled-components';\n\nexport const ExpaderButton = styled.svg<{\n  $disabled: boolean;\n  getIsExpanded: boolean;\n}>(\n  ({ theme: { table }, $disabled, getIsExpanded }) => css`\n    & > path {\n      fill: ${table.expander[\n        ($disabled && 'disabled') || (getIsExpanded && 'active') || 'normal'\n      ]};\n    }\n    &:hover > path {\n      fill: ${table.expander[$disabled ? 'disabled' : 'hover']};\n    }\n    &:active > path {\n      fill: ${table.expander[$disabled ? 'disabled' : 'active']};\n    }\n  `\n);\n\ninterface ThProps {\n  sortable?: boolean;\n  sortOrder?: 'desc' | 'asc' | false;\n  expander?: boolean;\n}\n\nconst sortableMixin = (normalColor: string, hoverColor: string) => `\n  cursor: pointer;\n  padding-left: 14px;\n  position: relative;\n\n  &::before,\n  &::after {\n    border: 4px solid transparent;\n    content: '';\n    display: block;\n    height: 0;\n    left: 0px;\n    top: 50%;\n    position: absolute;\n  }\n  &::before {\n    border-bottom-color: ${normalColor};\n    margin-top: -9px;\n  }\n  &::after {\n    border-top-color: ${normalColor};\n    margin-top: 1px;\n  }\n  &:hover {\n    color: ${hoverColor};\n  }\n`;\n\nconst ASCMixin = (color: string) => `\n  color: ${color};\n  &:before {\n    border-bottom-color: ${color};\n  }\n  &:after {\n    border-top-color: rgba(0, 0, 0, 0.1);\n  }\n`;\nconst DESCMixin = (color: string) => `\n  color: ${color};\n  &:before {\n    border-bottom-color: rgba(0, 0, 0, 0.1);\n  }\n  &:after {\n    border-top-color: ${color};\n  }\n`;\n\nexport const Th = styled.th<ThProps>(\n  ({\n    theme: {\n      table: { th },\n    },\n    sortable,\n    sortOrder,\n    expander,\n  }) => `\n  padding: 8px 0 8px 24px;\n  border-bottom-width: 1px;\n  vertical-align: middle;\n  text-align: left;\n  font-family: Inter, sans-serif;\n  font-size: 12px;\n  font-style: normal;\n  font-weight: 400;\n  line-height: 16px;\n  letter-spacing: 0em;\n  text-align: left;\n  background: ${th.backgroundColor.normal};\n  width: ${expander ? '5px' : 'auto'};\n  white-space: nowrap;\n\n  & > div {\n    cursor: default;\n    color: ${th.color.normal};\n    ${sortable ? sortableMixin(th.color.sortable, th.color.hover) : ''}\n    ${sortable && sortOrder === 'asc' && ASCMixin(th.color.active)}\n    ${sortable && sortOrder === 'desc' && DESCMixin(th.color.active)}\n  }\n`\n);\n\ninterface RowProps {\n  clickable?: boolean;\n  expanded?: boolean;\n}\n\nexport const Row = styled.tr<RowProps>(\n  ({ theme: { table }, expanded, clickable }) => `\n  cursor: ${clickable ? 'pointer' : 'default'};\n  background-color: ${table.tr.backgroundColor[expanded ? 'hover' : 'normal']};\n  &:hover {\n    background-color: ${table.tr.backgroundColor.hover};\n  }\n`\n);\n\nexport const ExpandedRowInfo = styled.div`\n  background-color: ${({ theme }) => theme.table.tr.backgroundColor.normal};\n  padding: 24px;\n  border-radius: 8px;\n  margin: 0 8px 8px 0;\n`;\n\nexport const Nowrap = styled.div`\n  white-space: nowrap;\n`;\n\nexport const TableActionsBar = styled.div`\n  padding: 8px;\n  background-color: ${({ theme }) => theme.table.actionBar.backgroundColor};\n  margin: 16px 0;\n  display: flex;\n  gap: 8px;\n`;\n\nexport const Table = styled.table(\n  ({ theme: { table } }) => `\n  width: 100%;\n\n  td {\n    border-top: 1px ${table.td.borderTop} solid;\n    font-size: 14px;\n    font-weight: 400;\n    padding: 8px 8px 8px 24px;\n    color: ${table.td.color.normal};\n    vertical-align: middle;\n    word-wrap: break-word;\n\n    & a {\n      color: ${table.td.color.normal};\n      font-weight: 500;\n      max-width: 450px;\n      white-space: nowrap;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      display: block;\n\n      &:hover {\n        color: ${table.link.color.hover};\n      }\n\n      &:active {\n        color: ${table.link.color.active};\n      }\n      &:button {\n      color: ${table.link.color.active};\n      }\n\n    }\n  }\n`\n);\n\nexport const EmptyTableMessageCell = styled.td`\n  padding: 16px;\n  text-align: center;\n`;\n\nexport const Pagination = styled.div`\n  display: flex;\n  justify-content: space-between;\n  padding: 16px;\n  line-height: 32px;\n`;\n\nexport const Pages = styled.div`\n  display: flex;\n  justify-content: left;\n  white-space: nowrap;\n  flex-wrap: nowrap;\n  gap: 8px;\n`;\n\nexport const GoToPage = styled.label`\n  display: flex;\n  flex-wrap: nowrap;\n  gap: 8px;\n  margin-left: 8px;\n  color: ${({ theme }) => theme.table.pagination.info};\n`;\n\nexport const PageInfo = styled.div`\n  display: flex;\n  justify-content: right;\n  gap: 8px;\n  font-size: 14px;\n  flex-wrap: nowrap;\n  white-space: nowrap;\n  margin-left: 16px;\n  color: ${({ theme }) => theme.table.pagination.info};\n`;\n\nexport const Ellipsis = styled.div`\n  max-width: 300px;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  display: block;\n`;\n\nexport const TableWrapper = styled.div<{ $disabled: boolean }>(\n  ({ $disabled }) => css`\n    overflow-x: auto;\n    ${$disabled &&\n    css`\n      pointer-events: none;\n      opacity: 0.5;\n    `}\n  `\n);\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/NewTable/Table.tsx",
    "content": "import React from 'react';\nimport {\n  flexRender,\n  getCoreRowModel,\n  getExpandedRowModel,\n  getSortedRowModel,\n  useReactTable,\n  getPaginationRowModel,\n} from '@tanstack/react-table';\nimport type {\n  Row,\n  SortingState,\n  OnChangeFn,\n  PaginationState,\n  ColumnDef,\n} from '@tanstack/react-table';\nimport { useSearchParams, useLocation } from 'react-router-dom';\nimport { PER_PAGE } from 'lib/constants';\nimport { Button } from 'components/common/Button/Button';\nimport Input from 'components/common/Input/Input';\n\nimport * as S from './Table.styled';\nimport updateSortingState from './utils/updateSortingState';\nimport updatePaginationState from './utils/updatePaginationState';\nimport ExpanderCell from './ExpanderCell';\nimport SelectRowCell from './SelectRowCell';\nimport SelectRowHeader from './SelectRowHeader';\n\nexport interface TableProps<TData> {\n  data: TData[];\n  pageCount?: number;\n  columns: ColumnDef<TData>[];\n\n  // Server-side processing: sorting, pagination\n  serverSideProcessing?: boolean;\n\n  // Expandeble rows\n  getRowCanExpand?: (row: Row<TData>) => boolean; // Enables the ability to expand row. Use `() => true` when want to expand all rows.\n  renderSubComponent?: React.FC<{ row: Row<TData> }>; // Component to render expanded row.\n\n  // Selectable rows\n  enableRowSelection?: boolean | ((row: Row<TData>) => boolean); // Enables the ability to select row.\n  batchActionsBar?: React.FC<{ rows: Row<TData>[]; resetRowSelection(): void }>; // Component to render batch actions bar for slected rows\n\n  // Sorting.\n  enableSorting?: boolean; // Enables sorting for table.\n\n  // Placeholder for empty table\n  emptyMessage?: React.ReactNode;\n\n  disabled?: boolean;\n\n  // Handles row click. Can not be combined with `enableRowSelection` && expandable rows.\n  onRowClick?: (row: Row<TData>) => void;\n\n  onRowHover?: (row: Row<TData>) => void;\n  onMouseLeave?: () => void;\n}\n\ntype UpdaterFn<T> = (previousState: T) => T;\n\nconst getPaginationFromSearchParams = (searchParams: URLSearchParams) => {\n  const page = searchParams.get('page');\n  const perPage = searchParams.get('perPage');\n  const pageIndex = page ? Number(page) - 1 : 0;\n  return {\n    pageIndex,\n    pageSize: Number(perPage || PER_PAGE),\n  };\n};\n\nconst getSortingFromSearchParams = (searchParams: URLSearchParams) => {\n  const sortBy = searchParams.get('sortBy');\n  const sortDirection = searchParams.get('sortDirection');\n  if (!sortBy) return [];\n  return [{ id: sortBy, desc: sortDirection === 'desc' }];\n};\n\n/**\n * Table component that uses the react-table library to render a table.\n * https://tanstack.com/table/v8\n *\n * The most important props are:\n *  - `data`: the data to render in the table\n *  - `columns`: ColumnsDef. You can finde more info about it on https://tanstack.com/table/v8/docs/guide/column-defs\n *  - `emptyMessage`: the message to show when there is no data to render\n *\n * Usecases:\n * 1. Sortable table\n *    - set `enableSorting` property of component to true. It will enable sorting for all columns.\n *      If you want to disable sorting for some particular columns you can pass\n *     `enableSorting = false` to the column def.\n *    - table component stores the sorting state in URLSearchParams. Use `sortBy` and `sortDirection`\n *      search param to set default sortings.\n *    - use `id` property of the column def to set the sortBy for server side sorting.\n *\n * 2. Pagination\n *    - pagination enabled by default.\n *    - use `perPage` search param to manage default page size.\n *    - use `page` search param to manage default page index.\n *    - use `pageCount` prop to set the total number of pages only in case of server side processing.\n *\n * 3. Expandable rows\n *    - use `getRowCanExpand` prop to set a function that returns true if the row can be expanded.\n *    - use `renderSubComponent` prop to provide a sub component for each expanded row.\n *\n * 4. Row selection\n *    - use `enableRowSelection` prop to enable row selection. This prop can be a boolean or\n *      a function that returns true if the particular row can be selected.\n *    - use `batchActionsBar` prop to provide a component that will be rendered at the top of the table\n *      when row selection is enabled.\n *\n * 5. Server side processing:\n *    - set `serverSideProcessing` to true\n *    - set `pageCount` to the total number of pages\n *    - use URLSearchParams to get the pagination and sorting state from the url for your server side processing.\n */\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst Table: React.FC<TableProps<any>> = ({\n  data,\n  pageCount,\n  columns,\n  getRowCanExpand,\n  renderSubComponent: SubComponent,\n  serverSideProcessing = false,\n  enableSorting = false,\n  enableRowSelection = false,\n  batchActionsBar: BatchActionsBar,\n  emptyMessage,\n  disabled,\n  onRowClick,\n  onRowHover,\n  onMouseLeave,\n}) => {\n  const [searchParams, setSearchParams] = useSearchParams();\n  const location = useLocation();\n  const [rowSelection, setRowSelection] = React.useState({});\n  const onSortingChange = React.useCallback(\n    (updater: UpdaterFn<SortingState>) => {\n      const newState = updateSortingState(updater, searchParams);\n      setSearchParams(searchParams);\n      return newState;\n    },\n    [searchParams, location]\n  );\n  const onPaginationChange = React.useCallback(\n    (updater: UpdaterFn<PaginationState>) => {\n      const newState = updatePaginationState(updater, searchParams);\n      setSearchParams(searchParams);\n      setRowSelection({});\n      return newState;\n    },\n    [searchParams, location]\n  );\n\n  const table = useReactTable({\n    data,\n    pageCount,\n    columns,\n    state: {\n      sorting: getSortingFromSearchParams(searchParams),\n      pagination: getPaginationFromSearchParams(searchParams),\n      rowSelection,\n    },\n    getRowId: (originalRow, index) => {\n      return originalRow.name ? originalRow.name : `${index}`;\n    },\n    onSortingChange: onSortingChange as OnChangeFn<SortingState>,\n    onPaginationChange: onPaginationChange as OnChangeFn<PaginationState>,\n    onRowSelectionChange: setRowSelection,\n    getRowCanExpand,\n    getCoreRowModel: getCoreRowModel(),\n    getExpandedRowModel: getExpandedRowModel(),\n    getSortedRowModel: getSortedRowModel(),\n    getPaginationRowModel: getPaginationRowModel(),\n    manualSorting: serverSideProcessing,\n    manualPagination: serverSideProcessing,\n    enableSorting,\n    autoResetPageIndex: false,\n    enableRowSelection,\n  });\n\n  const handleRowClick = (row: Row<typeof data>) => (e: React.MouseEvent) => {\n    // If row selection is enabled do not handle row click.\n    if (enableRowSelection) return undefined;\n\n    // If row can be expanded do not handle row click.\n    if (row.getCanExpand()) {\n      e.stopPropagation();\n      return row.toggleExpanded();\n    }\n\n    if (onRowClick) {\n      e.stopPropagation();\n      return onRowClick(row);\n    }\n\n    return undefined;\n  };\n\n  const handleRowHover = (row: Row<typeof data>) => (e: React.MouseEvent) => {\n    if (onRowHover) {\n      e.stopPropagation();\n      return onRowHover(row);\n    }\n\n    return undefined;\n  };\n\n  const handleMouseLeave = () => {\n    if (onMouseLeave) {\n      onMouseLeave();\n    }\n  };\n\n  return (\n    <>\n      {BatchActionsBar && (\n        <S.TableActionsBar>\n          <BatchActionsBar\n            rows={table.getSelectedRowModel().flatRows}\n            resetRowSelection={table.resetRowSelection}\n          />\n        </S.TableActionsBar>\n      )}\n      <S.TableWrapper $disabled={!!disabled}>\n        <S.Table>\n          <thead>\n            {table.getHeaderGroups().map((headerGroup) => (\n              <tr key={headerGroup.id}>\n                {!!enableRowSelection && (\n                  <S.Th key={`${headerGroup.id}-select`}>\n                    {flexRender(\n                      SelectRowHeader,\n                      headerGroup.headers[0].getContext()\n                    )}\n                  </S.Th>\n                )}\n                {table.getCanSomeRowsExpand() && (\n                  <S.Th expander key={`${headerGroup.id}-expander`} />\n                )}\n                {headerGroup.headers.map((header) => (\n                  <S.Th\n                    key={header.id}\n                    colSpan={header.colSpan}\n                    sortable={header.column.getCanSort()}\n                    sortOrder={header.column.getIsSorted()}\n                    onClick={header.column.getToggleSortingHandler()}\n                    style={{\n                      width:\n                        header.column.getSize() !== 150\n                          ? header.column.getSize()\n                          : undefined,\n                    }}\n                  >\n                    <div>\n                      {flexRender(\n                        header.column.columnDef.header,\n                        header.getContext()\n                      )}\n                    </div>\n                  </S.Th>\n                ))}\n              </tr>\n            ))}\n          </thead>\n          <tbody>\n            {table.getRowModel().rows.map((row) => (\n              <React.Fragment key={row.id}>\n                <S.Row\n                  expanded={row.getIsExpanded()}\n                  onClick={handleRowClick(row)}\n                  onMouseOver={onRowHover ? handleRowHover(row) : undefined}\n                  onMouseLeave={onMouseLeave ? handleMouseLeave : undefined}\n                  clickable={\n                    !enableRowSelection &&\n                    (row.getCanExpand() || onRowClick !== undefined)\n                  }\n                >\n                  {!!enableRowSelection && (\n                    <td key={`${row.id}-select`} style={{ width: '1px' }}>\n                      {flexRender(\n                        SelectRowCell,\n                        row.getVisibleCells()[0].getContext()\n                      )}\n                    </td>\n                  )}\n                  {table.getCanSomeRowsExpand() && (\n                    <td key={`${row.id}-expander`} style={{ width: '1px' }}>\n                      {flexRender(\n                        ExpanderCell,\n                        row.getVisibleCells()[0].getContext()\n                      )}\n                    </td>\n                  )}\n                  {row\n                    .getVisibleCells()\n                    .map(({ id, getContext, column: { columnDef } }) => (\n                      <td\n                        key={id}\n                        style={{\n                          width:\n                            columnDef.size !== 150 ? columnDef.size : undefined,\n                        }}\n                      >\n                        {flexRender(columnDef.cell, getContext())}\n                      </td>\n                    ))}\n                </S.Row>\n                {row.getIsExpanded() && SubComponent && (\n                  <S.Row expanded>\n                    <td colSpan={row.getVisibleCells().length + 2}>\n                      <S.ExpandedRowInfo>\n                        <SubComponent row={row} />\n                      </S.ExpandedRowInfo>\n                    </td>\n                  </S.Row>\n                )}\n              </React.Fragment>\n            ))}\n            {table.getRowModel().rows.length === 0 && (\n              <S.Row>\n                <S.EmptyTableMessageCell colSpan={100}>\n                  {emptyMessage || 'No rows found'}\n                </S.EmptyTableMessageCell>\n              </S.Row>\n            )}\n          </tbody>\n        </S.Table>\n      </S.TableWrapper>\n      {table.getPageCount() > 1 && (\n        <S.Pagination>\n          <S.Pages>\n            <Button\n              buttonType=\"secondary\"\n              buttonSize=\"M\"\n              onClick={() => table.setPageIndex(0)}\n              disabled={!table.getCanPreviousPage()}\n            >\n              ⇤\n            </Button>\n            <Button\n              buttonType=\"secondary\"\n              buttonSize=\"M\"\n              onClick={() => table.previousPage()}\n              disabled={!table.getCanPreviousPage()}\n            >\n              ← Previous\n            </Button>\n            <Button\n              buttonType=\"secondary\"\n              buttonSize=\"M\"\n              onClick={() => table.nextPage()}\n              disabled={!table.getCanNextPage()}\n            >\n              Next →\n            </Button>\n            <Button\n              buttonType=\"secondary\"\n              buttonSize=\"M\"\n              onClick={() => table.setPageIndex(table.getPageCount() - 1)}\n              disabled={!table.getCanNextPage()}\n            >\n              ⇥\n            </Button>\n\n            <S.GoToPage>\n              <span>Go to page:</span>\n              <Input\n                type=\"number\"\n                positiveOnly\n                defaultValue={table.getState().pagination.pageIndex + 1}\n                inputSize=\"M\"\n                max={table.getPageCount()}\n                min={1}\n                onChange={({ target: { value } }) => {\n                  const index = value ? Number(value) - 1 : 0;\n                  table.setPageIndex(index);\n                }}\n              />\n            </S.GoToPage>\n          </S.Pages>\n          <S.PageInfo>\n            <span>\n              Page {table.getState().pagination.pageIndex + 1} of{' '}\n              {table.getPageCount()}{' '}\n            </span>\n          </S.PageInfo>\n        </S.Pagination>\n      )}\n    </>\n  );\n};\n\nexport default Table;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/NewTable/TagCell.tsx",
    "content": "import { CellContext } from '@tanstack/react-table';\nimport React from 'react';\nimport getTagColor from 'components/common/Tag/getTagColor';\nimport { Tag } from 'components/common/Tag/Tag.styled';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst TagCell: React.FC<CellContext<any, unknown>> = ({ getValue }) => {\n  const value = getValue<string>();\n  return <Tag color={getTagColor(value)}>{value}</Tag>;\n};\n\nexport default TagCell;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/NewTable/TimestampCell.tsx",
    "content": "import { CellContext } from '@tanstack/react-table';\nimport { formatTimestamp } from 'lib/dateTimeHelpers';\nimport React from 'react';\n\nimport * as S from './Table.styled';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst TimestampCell: React.FC<CellContext<any, unknown>> = ({ getValue }) => (\n  <S.Nowrap>{formatTimestamp(getValue<string | number>())}</S.Nowrap>\n);\n\nexport default TimestampCell;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/NewTable/__test__/Table.spec.tsx",
    "content": "import React from 'react';\nimport { render, WithRoute } from 'lib/testHelpers';\nimport Table, {\n  TableProps,\n  TimestampCell,\n  SizeCell,\n  LinkCell,\n  TagCell,\n} from 'components/common/NewTable';\nimport { screen, waitFor } from '@testing-library/dom';\nimport { ColumnDef, Row } from '@tanstack/react-table';\nimport userEvent from '@testing-library/user-event';\nimport { formatTimestamp } from 'lib/dateTimeHelpers';\nimport { ConnectorState, ConsumerGroupState } from 'generated-sources';\n\nconst mockedUsedNavigate = jest.fn();\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useNavigate: () => mockedUsedNavigate,\n}));\n\n// This is needed by ESLint.\njest.mock('react-hook-form', () => ({\n  useFormContext: () => ({\n    register: jest.fn(),\n\n    // Mock methods.getValues and methods.setValue\n    getValues: jest.fn(),\n    setValue: jest.fn(),\n  }),\n}));\n\ntype Datum = (typeof data)[0];\n\nconst data = [\n  {\n    timestamp: 1660034383725,\n    text: 'lorem',\n    selectable: false,\n    size: 1234,\n    tag: ConnectorState.RUNNING,\n  },\n  {\n    timestamp: 1660034399999,\n    text: 'ipsum',\n    selectable: true,\n    size: 3,\n    tag: ConnectorState.FAILED,\n  },\n  {\n    timestamp: 1660034399922,\n    text: 'dolor',\n    selectable: true,\n    size: 50000,\n    tag: ConsumerGroupState.EMPTY,\n  },\n  {\n    timestamp: 1660034199922,\n    text: 'sit',\n    selectable: false,\n    size: 1_312_323,\n    tag: 'some_string',\n  },\n];\n\nconst columns: ColumnDef<Datum>[] = [\n  {\n    header: 'DateTime',\n    accessorKey: 'timestamp',\n    cell: TimestampCell,\n  },\n  {\n    header: 'Text',\n    accessorKey: 'text',\n    cell: ({ getValue }) => (\n      <LinkCell\n        value={`${getValue<string | number>()}`}\n        to={encodeURIComponent(`${getValue<string | number>()}`)}\n      />\n    ),\n  },\n  {\n    header: 'Size',\n    accessorKey: 'size',\n    cell: SizeCell,\n  },\n  {\n    header: 'Tag',\n    accessorKey: 'tag',\n    cell: TagCell,\n  },\n];\n\nconst ExpandedRow: React.FC = () => <div>I am expanded row</div>;\n\ninterface Props extends TableProps<Datum> {\n  path?: string;\n}\n\nconst renderComponent = (props: Partial<Props> = {}) => {\n  render(\n    <WithRoute path=\"/*\">\n      <Table\n        columns={columns}\n        data={data}\n        renderSubComponent={ExpandedRow}\n        {...props}\n      />\n    </WithRoute>,\n    { initialEntries: [props.path || ''] }\n  );\n};\n\ndescribe('Table', () => {\n  it('renders table', () => {\n    renderComponent();\n    expect(screen.getByRole('table')).toBeInTheDocument();\n    expect(screen.getAllByRole('row').length).toEqual(data.length + 1);\n  });\n\n  it('renders empty table', () => {\n    renderComponent({ data: [] });\n    expect(screen.getByRole('table')).toBeInTheDocument();\n    expect(screen.getAllByRole('row').length).toEqual(2);\n    expect(screen.getByText('No rows found')).toBeInTheDocument();\n  });\n\n  it('renders empty table with custom message', () => {\n    const emptyMessage = 'Super custom message';\n    renderComponent({ data: [], emptyMessage });\n    expect(screen.getByRole('table')).toBeInTheDocument();\n    expect(screen.getAllByRole('row').length).toEqual(2);\n    expect(screen.getByText(emptyMessage)).toBeInTheDocument();\n  });\n\n  it('renders SizeCell', () => {\n    renderComponent();\n    expect(screen.getByText('1 KB')).toBeInTheDocument();\n    expect(screen.getByText('3 Bytes')).toBeInTheDocument();\n    expect(screen.getByText('49 KB')).toBeInTheDocument();\n    expect(screen.getByText('1 MB')).toBeInTheDocument();\n  });\n\n  it('renders TimestampCell', () => {\n    renderComponent();\n    expect(\n      screen.getByText(formatTimestamp(data[0].timestamp))\n    ).toBeInTheDocument();\n  });\n\n  describe('LinkCell', () => {\n    it('renders link', () => {\n      renderComponent();\n      expect(screen.getByRole('link', { name: 'lorem' })).toBeInTheDocument();\n    });\n\n    it('link click stops propagation', async () => {\n      const onRowClick = jest.fn();\n      renderComponent({ onRowClick });\n      const link = screen.getByRole('link', { name: 'lorem' });\n      await userEvent.click(link);\n      expect(onRowClick).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('ExpanderCell', () => {\n    it('renders button', async () => {\n      renderComponent({ getRowCanExpand: () => true });\n      const btns = screen.getAllByRole('button', { name: 'Expand row' });\n      expect(btns.length).toEqual(data.length);\n\n      expect(screen.queryByText('I am expanded row')).not.toBeInTheDocument();\n      await userEvent.click(btns[2]);\n      expect(screen.getByText('I am expanded row')).toBeInTheDocument();\n      await userEvent.click(btns[0]);\n      expect(screen.getAllByText('I am expanded row').length).toEqual(2);\n    });\n\n    it('does not render button', () => {\n      renderComponent({ getRowCanExpand: () => false });\n      expect(\n        screen.queryByRole('button', { name: 'Expand row' })\n      ).not.toBeInTheDocument();\n      expect(screen.queryByText('I am expanded row')).not.toBeInTheDocument();\n    });\n  });\n\n  it('renders TagCell', () => {\n    renderComponent();\n    expect(screen.getByText(data[0].tag)).toBeInTheDocument();\n    expect(screen.getByText(data[1].tag)).toBeInTheDocument();\n    expect(screen.getByText(data[2].tag)).toBeInTheDocument();\n    expect(screen.getByText(data[3].tag)).toBeInTheDocument();\n  });\n\n  describe('Pagination', () => {\n    it('does not render page buttons', () => {\n      renderComponent();\n      expect(\n        screen.queryByRole('button', { name: 'Next' })\n      ).not.toBeInTheDocument();\n    });\n\n    it('renders page buttons', async () => {\n      renderComponent({ path: '?perPage=1' });\n      // Check it renders header row and only one data row\n      expect(screen.getAllByRole('row').length).toEqual(2);\n      expect(screen.getByText('lorem')).toBeInTheDocument();\n\n      // Check it renders page buttons\n      const firstBtn = screen.getByRole('button', { name: '⇤' });\n      const prevBtn = screen.getByRole('button', { name: '← Previous' });\n      const nextBtn = screen.getByRole('button', { name: 'Next →' });\n      const lastBtn = screen.getByRole('button', { name: '⇥' });\n\n      expect(firstBtn).toBeInTheDocument();\n      expect(firstBtn).toBeDisabled();\n      expect(prevBtn).toBeInTheDocument();\n      expect(prevBtn).toBeDisabled();\n      expect(nextBtn).toBeInTheDocument();\n      expect(nextBtn).toBeEnabled();\n      expect(lastBtn).toBeInTheDocument();\n      expect(lastBtn).toBeEnabled();\n\n      await userEvent.click(nextBtn);\n      expect(screen.getByText('ipsum')).toBeInTheDocument();\n      expect(prevBtn).toBeEnabled();\n      expect(firstBtn).toBeEnabled();\n\n      await userEvent.click(lastBtn);\n      expect(screen.getByText('sit')).toBeInTheDocument();\n      expect(lastBtn).toBeDisabled();\n      expect(nextBtn).toBeDisabled();\n\n      await userEvent.click(prevBtn);\n      expect(screen.getByText('dolor')).toBeInTheDocument();\n\n      await userEvent.click(firstBtn);\n      expect(screen.getByText('lorem')).toBeInTheDocument();\n    });\n\n    describe('Go To page', () => {\n      const getGoToPageInput = () =>\n        screen.getByRole('spinbutton', { name: 'Go to page:' });\n\n      beforeEach(() => {\n        renderComponent({ path: '?perPage=1' });\n      });\n\n      it('renders Go To page', () => {\n        const goToPage = getGoToPageInput();\n        expect(goToPage).toBeInTheDocument();\n        expect(goToPage).toHaveValue(1);\n      });\n      it('updates page on Go To page change', async () => {\n        const goToPage = getGoToPageInput();\n        await userEvent.clear(goToPage);\n        await userEvent.type(goToPage, '2');\n        expect(goToPage).toHaveValue(2);\n        expect(screen.getByText('ipsum')).toBeInTheDocument();\n      });\n      it('does not update page on Go To page change if page is out of range', async () => {\n        const goToPage = getGoToPageInput();\n        await userEvent.type(goToPage, '5');\n        expect(goToPage).toHaveValue(15);\n        expect(screen.getByText('No rows found')).toBeInTheDocument();\n      });\n      it('does not update page on Go To page change if page is not a number', async () => {\n        const goToPage = getGoToPageInput();\n        await userEvent.type(goToPage, 'abc');\n        expect(goToPage).toHaveValue(1);\n      });\n    });\n  });\n\n  describe('Sorting', () => {\n    it('sort rows', async () => {\n      await renderComponent({\n        path: '/?sortBy=text&&sortDirection=desc',\n        enableSorting: true,\n      });\n      expect(screen.getAllByRole('row').length).toEqual(data.length + 1);\n      const th = screen.getByRole('columnheader', { name: 'Text' });\n      expect(th).toBeInTheDocument();\n\n      let rows = screen.getAllByRole('row');\n      // Check initial sort order by text column is descending\n\n      expect(rows[4].textContent?.indexOf('dolor')).toBeGreaterThan(-1);\n      expect(rows[3].textContent?.indexOf('ipsum')).toBeGreaterThan(-1);\n      expect(rows[2].textContent?.indexOf('lorem')).toBeGreaterThan(-1);\n      expect(rows[1].textContent?.indexOf('sit')).toBeGreaterThan(-1);\n\n      // Disable sorting by text column\n      await waitFor(() => userEvent.click(th));\n      rows = screen.getAllByRole('row');\n      expect(rows[1].textContent?.indexOf('lorem')).toBeGreaterThan(-1);\n      expect(rows[2].textContent?.indexOf('ipsum')).toBeGreaterThan(-1);\n      expect(rows[3].textContent?.indexOf('dolor')).toBeGreaterThan(-1);\n      expect(rows[4].textContent?.indexOf('sit')).toBeGreaterThan(-1);\n\n      // Sort by text column ascending\n      await waitFor(() => userEvent.click(th));\n      rows = screen.getAllByRole('row');\n      expect(rows[1].textContent?.indexOf('dolor')).toBeGreaterThan(-1);\n      expect(rows[2].textContent?.indexOf('ipsum')).toBeGreaterThan(-1);\n      expect(rows[3].textContent?.indexOf('lorem')).toBeGreaterThan(-1);\n      expect(rows[4].textContent?.indexOf('sit')).toBeGreaterThan(-1);\n    });\n  });\n\n  describe('Row Selecting', () => {\n    beforeEach(() => {\n      renderComponent({\n        enableRowSelection: (row: Row<Datum>) => row.original.selectable,\n        batchActionsBar: () => <div>I am Action Bar</div>,\n      });\n    });\n    it('renders selectable rows', () => {\n      expect(screen.getAllByRole('row').length).toEqual(data.length + 1);\n      const checkboxes = screen.getAllByRole('checkbox');\n      expect(checkboxes.length).toEqual(data.length + 1);\n      expect(checkboxes[1]).toBeDisabled();\n      expect(checkboxes[2]).toBeEnabled();\n      expect(checkboxes[3]).toBeEnabled();\n      expect(checkboxes[4]).toBeDisabled();\n    });\n\n    it('renders action bar', async () => {\n      expect(screen.getAllByRole('row').length).toEqual(data.length + 1);\n      expect(screen.queryByText('I am Action Bar')).toBeInTheDocument();\n      const checkboxes = screen.getAllByRole('checkbox');\n      expect(checkboxes.length).toEqual(data.length + 1);\n      await userEvent.click(checkboxes[2]);\n      expect(screen.getByText('I am Action Bar')).toBeInTheDocument();\n    });\n  });\n  describe('Clickable Row', () => {\n    const onRowClick = jest.fn();\n    it('handles onRowClick', async () => {\n      renderComponent({ onRowClick });\n      const rows = screen.getAllByRole('row');\n      expect(rows.length).toEqual(data.length + 1);\n      await userEvent.click(rows[1]);\n      expect(onRowClick).toHaveBeenCalledTimes(1);\n    });\n    it('does nothing unless onRowClick is provided', async () => {\n      renderComponent();\n      const rows = screen.getAllByRole('row');\n      expect(rows.length).toEqual(data.length + 1);\n      await userEvent.click(rows[1]);\n    });\n    it('does not handle onRowClick if enableRowSelection', async () => {\n      renderComponent({ onRowClick, enableRowSelection: true });\n      const rows = screen.getAllByRole('row');\n      expect(rows.length).toEqual(data.length + 1);\n      await userEvent.click(rows[1]);\n      expect(onRowClick).not.toHaveBeenCalled();\n    });\n    it('does not handle onRowClick if expandable rows', async () => {\n      renderComponent({ onRowClick, getRowCanExpand: () => true });\n      const rows = screen.getAllByRole('row');\n      expect(rows.length).toEqual(data.length + 1);\n      await userEvent.click(rows[1]);\n      expect(onRowClick).not.toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/NewTable/index.ts",
    "content": "import Table, { TableProps } from './Table';\nimport TimestampCell from './TimestampCell';\nimport SizeCell from './SizeCell';\nimport LinkCell from './LinkCell';\nimport TagCell from './TagCell';\n\nexport type { TableProps };\n\nexport { TimestampCell, SizeCell, LinkCell, TagCell };\n\nexport default Table;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/NewTable/utils/__test__/updateSortingState.spec.ts",
    "content": "import updateSortingState from 'components/common/NewTable/utils/updateSortingState';\nimport { SortingState } from '@tanstack/react-table';\nimport compact from 'lodash/compact';\n\nconst updater = (previousState: SortingState): SortingState => {\n  return compact(\n    previousState.map(({ id, desc }) => {\n      if (!id) return null;\n      return { id, desc: !desc };\n    })\n  );\n};\n\ndescribe('updateSortingState', () => {\n  it('should update the sorting state', () => {\n    const searchParams = new URLSearchParams();\n    searchParams.set('sortBy', 'date');\n    searchParams.set('sortDirection', 'desc');\n    const newState = updateSortingState(updater, searchParams);\n    expect(searchParams.get('sortBy')).toBe('date');\n    expect(searchParams.get('sortDirection')).toBe('asc');\n    expect(newState.length).toBe(1);\n    expect(newState[0].id).toBe('date');\n    expect(newState[0].desc).toBe(false);\n  });\n\n  it('should update the sorting state', () => {\n    const searchParams = new URLSearchParams();\n    const newState = updateSortingState(updater, searchParams);\n    expect(searchParams.get('sortBy')).toBeNull();\n    expect(searchParams.get('sortDirection')).toBeNull();\n    expect(newState.length).toBe(0);\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/NewTable/utils/updatePaginationState.ts",
    "content": "import { PaginationState } from '@tanstack/react-table';\nimport { PER_PAGE } from 'lib/constants';\n\ntype UpdaterFn<T> = (previousState: T) => T;\n\nexport default (\n  updater: UpdaterFn<PaginationState>,\n  searchParams: URLSearchParams\n) => {\n  const page = searchParams.get('page');\n  const previousState: PaginationState = {\n    // Page number starts at 1, but the pageIndex starts at 0\n    pageIndex: page ? Number(page) - 1 : 0,\n    pageSize: Number(searchParams.get('perPage') || PER_PAGE),\n  };\n  const newState = updater(previousState);\n  searchParams.set('page', String(newState.pageIndex + 1));\n  searchParams.set('perPage', newState.pageSize.toString());\n  return previousState;\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/NewTable/utils/updateSortingState.ts",
    "content": "import { SortingState } from '@tanstack/react-table';\n\ntype UpdaterFn<T> = (previousState: T) => T;\n\nexport default (\n  updater: UpdaterFn<SortingState>,\n  searchParams: URLSearchParams\n) => {\n  const previousState: SortingState = [\n    {\n      id: searchParams.get('sortBy') || '',\n      desc: searchParams.get('sortDirection') === 'desc',\n    },\n  ];\n  const newState = updater(previousState);\n\n  if (newState.length > 0) {\n    const { id, desc } = newState[0];\n    searchParams.set('sortBy', id);\n    searchParams.set('sortDirection', desc ? 'desc' : 'asc');\n  } else {\n    searchParams.delete('sortBy');\n    searchParams.delete('sortDirection');\n  }\n  return newState;\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/PageHeading/PageHeading.styled.ts",
    "content": "import styled from 'styled-components';\nimport { NavLink } from 'react-router-dom';\n\nexport const Breadcrumbs = styled.div`\n  display: flex;\n  align-items: baseline;\n`;\n\nexport const BackLink = styled(NavLink)`\n  color: ${({ theme }) => theme.pageHeading.backLink.color.normal};\n  position: relative;\n\n  &:hover {\n    ${({ theme }) => theme.pageHeading.backLink.color.hover};\n  }\n\n  &::after {\n    content: '';\n    position: absolute;\n    right: -11px;\n    bottom: 2px;\n    border-left: 1px solid ${({ theme }) => theme.pageHeading.dividerColor};\n    height: 20px;\n    transform: rotate(14deg);\n  }\n`;\n\nexport const Wrapper = styled.div`\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: 16px;\n\n  & > div {\n    display: flex;\n    gap: 16px;\n  }\n\n  & > ${Breadcrumbs} {\n    gap: 20px;\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/PageHeading/PageHeading.tsx",
    "content": "import React, { PropsWithChildren } from 'react';\nimport Heading from 'components/common/heading/Heading.styled';\n\nimport * as S from './PageHeading.styled';\n\ninterface PageHeadingProps {\n  text: string;\n  backTo?: string;\n  backText?: string;\n}\n\nconst PageHeading: React.FC<PropsWithChildren<PageHeadingProps>> = ({\n  text,\n  backTo,\n  backText,\n  children,\n}) => {\n  const isBackButtonVisible = backTo && backText;\n\n  return (\n    <S.Wrapper>\n      <S.Breadcrumbs>\n        {isBackButtonVisible && <S.BackLink to={backTo}>{backText}</S.BackLink>}\n        <Heading>{text}</Heading>\n      </S.Breadcrumbs>\n      <div>{children}</div>\n    </S.Wrapper>\n  );\n};\n\nexport default PageHeading;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/PageLoader/PageLoader.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const Wrapper = styled.div`\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  padding-top: 15%;\n  height: 100%;\n  width: 100%;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/PageLoader/PageLoader.tsx",
    "content": "import React from 'react';\nimport Spinner from 'components/common/Spinner/Spinner';\n\nimport * as S from './PageLoader.styled';\n\nconst PageLoader: React.FC = () => (\n  <S.Wrapper>\n    <Spinner />\n  </S.Wrapper>\n);\n\nexport default PageLoader;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/PageLoader/__tests__/PageLoader.spec.tsx",
    "content": "import React from 'react';\nimport PageLoader from 'components/common/PageLoader/PageLoader';\nimport { screen } from '@testing-library/react';\nimport { render } from 'lib/testHelpers';\n\ndescribe('PageLoader', () => {\n  it('renders spinner', () => {\n    render(<PageLoader />);\n    expect(screen.getByRole('progressbar')).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ProgressBar/ProgressBar.styled.ts",
    "content": "import styled, { css } from 'styled-components';\n\nexport const Wrapper = styled.div`\n  height: 10px;\n  width: 100%;\n  min-width: 200px;\n  background-color: ${({ theme }) => theme.progressBar.backgroundColor};\n  border-radius: 5px;\n  margin: 16px;\n  border: 1px solid ${({ theme }) => theme.progressBar.borderColor};\n`;\n\nexport const Filler = styled.div<{ completed: number }>(\n  ({ theme: { progressBar }, completed }) => css`\n    height: 100%;\n    width: ${completed}%;\n    background-color: ${progressBar.compleatedColor};\n    border-radius: 5px;\n    text-align: right;\n    transition: width 1.2s linear;\n  `\n);\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ProgressBar/ProgressBar.tsx",
    "content": "import React from 'react';\n\nimport * as S from './ProgressBar.styled';\n\ninterface ProgressBarProps {\n  completed: number;\n}\n\nconst ProgressBar: React.FC<ProgressBarProps> = ({ completed }) => {\n  const p = Math.max(Math.min(completed, 100), 0);\n  return (\n    <S.Wrapper>\n      <S.Filler role=\"progressbar\" completed={p} />\n    </S.Wrapper>\n  );\n};\n\nexport default ProgressBar;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/ProgressBar/__test__/ProgressBar.spec.tsx",
    "content": "import React from 'react';\nimport { render } from 'lib/testHelpers';\nimport ProgressBar from 'components/common/ProgressBar/ProgressBar';\nimport { screen } from '@testing-library/dom';\n\ndescribe('Progressbar', () => {\n  const itRendersCorrectPercentage = (completed: number, expected: number) => {\n    it('renders correct percentage', () => {\n      render(<ProgressBar completed={completed} />);\n      const bar = screen.getByRole('progressbar');\n      expect(bar).toHaveStyleRule('width', `${expected}%`);\n    });\n  };\n\n  [\n    [-143, 0],\n    [0, 0],\n    [67, 67],\n    [143, 100],\n  ].forEach(([completed, expected]) =>\n    itRendersCorrectPercentage(completed, expected)\n  );\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/PropertiesList/PropertiesList.styled.tsx",
    "content": "import styled from 'styled-components';\n\nexport const List = styled.div`\n  display: grid;\n  grid-template-columns: repeat(2, max-content);\n  gap: 8px;\n  column-gap: 24px;\n  margin: 16px 0;\n  text-align: left;\n`;\n\nexport const Label = styled.div`\n  font-size: 14px;\n  font-weight: 500;\n  color: ${({ theme }) => theme.default.color.normal};\n  white-space: nowrap;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/SQLEditor/SQLEditor.tsx",
    "content": "/* eslint-disable react/jsx-props-no-spreading */\nimport AceEditor, { IAceEditorProps } from 'react-ace';\nimport 'ace-builds/src-noconflict/ace';\nimport 'ace-builds/src-noconflict/mode-sql';\nimport 'ace-builds/src-noconflict/theme-textmate';\nimport 'ace-builds/src-noconflict/theme-dracula';\nimport React, { useContext } from 'react';\nimport { ThemeModeContext } from 'components/contexts/ThemeModeContext';\n\ninterface SQLEditorProps extends IAceEditorProps {\n  isFixedHeight?: boolean;\n}\n\nconst SQLEditor = React.forwardRef<AceEditor | null, SQLEditorProps>(\n  (props, ref) => {\n    const { isFixedHeight, ...rest } = props;\n    const { isDarkMode } = useContext(ThemeModeContext);\n\n    return (\n      <AceEditor\n        ref={ref}\n        mode=\"sql\"\n        theme={isDarkMode ? 'dracula' : 'textmate'}\n        tabSize={2}\n        width=\"100%\"\n        height={\n          isFixedHeight\n            ? `${(props.value?.split('\\n').length || 32) * 16}px`\n            : '372px'\n        }\n        wrapEnabled\n        {...rest}\n      />\n    );\n  }\n);\n\nSQLEditor.displayName = 'SQLEditor';\n\nexport default SQLEditor;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/SQLEditor/__tests__/SQLEditor.spec.tsx",
    "content": "import React from 'react';\nimport SQLEditor from 'components/common/SQLEditor/SQLEditor';\nimport { render } from 'lib/testHelpers';\nimport { screen } from '@testing-library/react';\n\ndescribe('SQLEditor component', () => {\n  it('to be in the document with fixed height', () => {\n    render(<SQLEditor value=\"\" name=\"name\" isFixedHeight />);\n    expect(\n      screen.getByRole('textbox').parentElement?.getAttribute('style') !==\n        '16px'\n    );\n  });\n\n  it('to be in the document with fixed height with no value', () => {\n    render(<SQLEditor value=\"\" name=\"name\" />);\n    expect(\n      screen.getByRole('textbox').parentElement?.getAttribute('style') ===\n        '16px'\n    );\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Search/Search.tsx",
    "content": "import React, { useRef } from 'react';\nimport { useDebouncedCallback } from 'use-debounce';\nimport Input from 'components/common/Input/Input';\nimport { useSearchParams } from 'react-router-dom';\nimport CloseCircleIcon from 'components/common/Icons/CloseCircleIcon';\nimport styled from 'styled-components';\n\ninterface SearchProps {\n  placeholder?: string;\n  disabled?: boolean;\n  onChange?: (value: string) => void;\n  value?: string;\n}\n\nconst IconButtonWrapper = styled.span.attrs(() => ({\n  role: 'button',\n  tabIndex: '0',\n}))`\n  height: 16px !important;\n  display: inline-block;\n  &:hover {\n    cursor: pointer;\n  }\n`;\nconst Search: React.FC<SearchProps> = ({\n  placeholder = 'Search',\n  disabled = false,\n  value,\n  onChange,\n}) => {\n  const [searchParams, setSearchParams] = useSearchParams();\n  const ref = useRef<HTMLInputElement>(null);\n  const handleChange = useDebouncedCallback((e) => {\n    if (ref.current != null) {\n      ref.current.value = e.target.value;\n    }\n    if (onChange) {\n      onChange(e.target.value);\n    } else {\n      searchParams.set('q', e.target.value);\n      if (searchParams.get('page')) {\n        searchParams.set('page', '1');\n      }\n      setSearchParams(searchParams);\n    }\n  }, 500);\n  const clearSearchValue = () => {\n    if (searchParams.get('q')) {\n      searchParams.set('q', '');\n      setSearchParams(searchParams);\n    }\n    if (ref.current != null) {\n      ref.current.value = '';\n    }\n  };\n\n  return (\n    <Input\n      type=\"text\"\n      placeholder={placeholder}\n      onChange={handleChange}\n      defaultValue={value || searchParams.get('q') || ''}\n      inputSize=\"M\"\n      disabled={disabled}\n      ref={ref}\n      search\n      clearIcon={\n        <IconButtonWrapper onClick={clearSearchValue}>\n          <CloseCircleIcon />\n        </IconButtonWrapper>\n      }\n    />\n  );\n};\n\nexport default Search;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Search/__tests__/Search.spec.tsx",
    "content": "import Search from 'components/common/Search/Search';\nimport React from 'react';\nimport { render } from 'lib/testHelpers';\nimport userEvent from '@testing-library/user-event';\nimport { screen } from '@testing-library/react';\nimport { useSearchParams } from 'react-router-dom';\n\njest.mock('use-debounce', () => ({\n  useDebouncedCallback: (fn: (e: Event) => void) => fn,\n}));\n\nconst setSearchParamsMock = jest.fn();\njest.mock('react-router-dom', () => ({\n  ...(jest.requireActual('react-router-dom') as object),\n  useSearchParams: jest.fn(),\n}));\n\nconst placeholder = 'I am a search placeholder';\n\ndescribe('Search', () => {\n  beforeEach(() => {\n    (useSearchParams as jest.Mock).mockImplementation(() => [\n      new URLSearchParams(),\n      setSearchParamsMock,\n    ]);\n  });\n  it('calls handleSearch on input', async () => {\n    render(<Search placeholder={placeholder} />);\n    const input = screen.getByPlaceholderText(placeholder);\n    await userEvent.click(input);\n    await userEvent.keyboard('value');\n    expect(setSearchParamsMock).toHaveBeenCalledTimes(5);\n  });\n\n  it('when placeholder is provided', () => {\n    render(<Search placeholder={placeholder} />);\n    expect(screen.getByPlaceholderText(placeholder)).toBeInTheDocument();\n  });\n\n  it('when placeholder is not provided', () => {\n    render(<Search />);\n    expect(screen.queryByPlaceholderText('Search')).toBeInTheDocument();\n  });\n\n  it('Clear button is visible', () => {\n    render(<Search placeholder={placeholder} />);\n\n    const clearButton = screen.getByRole('button');\n    expect(clearButton).toBeInTheDocument();\n  });\n\n  it('Clear button should clear text from input', async () => {\n    render(<Search placeholder={placeholder} />);\n\n    const searchField = screen.getAllByRole('textbox')[0];\n    await userEvent.type(searchField, 'some text');\n    expect(searchField).toHaveValue('some text');\n\n    const clearButton = screen.getByRole('button');\n    await userEvent.click(clearButton);\n\n    expect(searchField).toHaveValue('');\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Select/ControlledSelect.tsx",
    "content": "import * as React from 'react';\nimport { Controller } from 'react-hook-form';\nimport { FormError } from 'components/common/Input/Input.styled';\nimport { InputLabel } from 'components/common/Input/InputLabel.styled';\nimport { ErrorMessage } from '@hookform/error-message';\n\nimport Select, { SelectOption } from './Select';\n\ninterface ControlledSelectProps {\n  name: string;\n  label: React.ReactNode;\n  hint?: string;\n  options: SelectOption[];\n  onChange?: (val: string | number) => void;\n  disabled?: boolean;\n  placeholder?: string;\n}\n\nconst ControlledSelect: React.FC<ControlledSelectProps> = ({\n  name,\n  label,\n  onChange,\n  options,\n  disabled = false,\n  placeholder,\n}) => {\n  const id = React.useId();\n\n  return (\n    <div>\n      <InputLabel htmlFor={id}>{label}</InputLabel>\n      <Controller\n        name={name}\n        render={({ field }) => {\n          return (\n            <Select\n              id={id}\n              name={field.name}\n              minWidth=\"270px\"\n              onChange={(value) => {\n                if (onChange) onChange(value);\n                field.onChange(value);\n              }}\n              value={field.value}\n              options={options}\n              placeholder={placeholder}\n              disabled={disabled}\n              ref={field.ref}\n            />\n          );\n        }}\n      />\n      <FormError>\n        <ErrorMessage name={name} />\n      </FormError>\n    </div>\n  );\n};\n\nexport default ControlledSelect;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Select/LiveIcon.styled.tsx",
    "content": "import styled, { useTheme } from 'styled-components';\nimport React from 'react';\n\ninterface Props {\n  className?: string;\n}\n\nconst SVGWrapper = styled.i`\n  display: flex;\n`;\n\nconst LiveIcon: React.FC<Props> = () => {\n  const theme = useTheme();\n  return (\n    <SVGWrapper>\n      <svg\n        width=\"16\"\n        height=\"16\"\n        viewBox=\"0 0 16 16\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <circle cx=\"8\" cy=\"8\" r=\"7\" fill={theme.icons.liveIcon.circleBig} />\n        <circle cx=\"8\" cy=\"8\" r=\"4\" fill={theme.icons.liveIcon.circleSmall} />\n      </svg>\n    </SVGWrapper>\n  );\n};\n\nexport default styled(LiveIcon)`\n  position: absolute;\n  left: 12px;\n  top: 50%;\n  transform: translateY(-50%);\n  line-height: 0;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Select/Select.styled.ts",
    "content": "import styled from 'styled-components';\n\ninterface Props {\n  selectSize: 'M' | 'L';\n  isLive?: boolean;\n  minWidth?: string;\n  disabled?: boolean;\n  isThemeMode?: boolean;\n}\n\ninterface OptionProps {\n  disabled?: boolean;\n}\n\nexport const Select = styled.ul<Props>`\n  position: relative;\n  list-style: none;\n  display: flex;\n  gap: 6px;\n  align-items: center;\n  justify-content: space-between;\n  height: ${(props) => (props.selectSize === 'M' ? '32px' : '40px')};\n  border: 1px\n    ${({ theme, disabled, isThemeMode }) => {\n      if (isThemeMode) {\n        return 'none';\n      }\n      if (disabled) {\n        return theme.select.borderColor.disabled;\n      }\n\n      return theme.select.borderColor.normal;\n    }}\n    solid;\n  border-radius: 4px;\n  font-size: 14px;\n  width: fit-content;\n  padding-left: 16px;\n  padding-right: 12px;\n  color: ${({ theme, disabled }) =>\n    disabled ? theme.select.color.disabled : theme.select.color.normal};\n  min-width: ${({ minWidth }) => minWidth || 'auto'};\n  cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};\n  &:hover {\n    color: ${({ theme, disabled }) =>\n      disabled ? theme.select.color.disabled : theme.select.color.hover};\n    border-color: ${({ theme, disabled }) =>\n      disabled\n        ? theme.select.borderColor.disabled\n        : theme.select.borderColor.hover};\n  }\n  &:focus {\n    outline: none;\n    color: ${({ theme }) => theme.select.color.active};\n    border-color: ${({ theme }) => theme.select.borderColor.active};\n  }\n  &:disabled {\n    color: ${({ theme }) => theme.select.color.disabled};\n    border-color: ${({ theme }) => theme.select.borderColor.disabled};\n  }\n`;\n\nexport const SelectedOptionWrapper = styled.div`\n  display: flex;\n  align-items: center;\n  gap: 5px;\n  overflow: hidden;\n  text-overflow: ellipsis;\n`;\n\nexport const OptionList = styled.ul`\n  position: absolute;\n  top: 100%;\n  left: 0;\n  max-height: 228px;\n  margin-top: 4px;\n  background-color: ${({ theme }) => theme.select.backgroundColor.normal};\n  border: 1px ${({ theme }) => theme.select.borderColor.normal} solid;\n  border-radius: 4px;\n  font-size: 14px;\n  line-height: 18px;\n  color: ${({ theme }) => theme.select.color.normal};\n  overflow-y: auto;\n  z-index: 10;\n  max-width: 300px;\n  min-width: 100%;\n  align-items: center;\n  & div {\n    white-space: nowrap;\n  }\n  &::-webkit-scrollbar {\n    -webkit-appearance: none;\n    width: 7px;\n  }\n\n  &::-webkit-scrollbar-thumb {\n    border-radius: 4px;\n    background-color: ${({ theme }) =>\n      theme.select.optionList.scrollbar.backgroundColor};\n  }\n\n  &::-webkit-scrollbar:horizontal {\n    height: 7px;\n  }\n`;\n\nexport const Option = styled.li<OptionProps>`\n  display: flex;\n  align-items: center;\n  list-style: none;\n  padding: 10px 12px;\n  transition: all 0.2s ease-in-out;\n  cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};\n  gap: 5px;\n  color: ${({ theme, disabled }) =>\n    theme.select.color[disabled ? 'disabled' : 'normal']};\n\n  &:hover {\n    background-color: ${({ theme, disabled }) =>\n      theme.select.backgroundColor[disabled ? 'normal' : 'hover']};\n  }\n\n  &:active {\n    background-color: ${({ theme }) => theme.select.backgroundColor.active};\n  }\n`;\n\nexport const SelectedOption = styled.li<{ isThemeMode?: boolean }>`\n  display: flex;\n  padding-right: ${({ isThemeMode }) => (isThemeMode ? '' : '16px')};\n  list-style-position: inside;\n  white-space: nowrap;\n  & svg {\n    path {\n      fill: ${({ theme }) => theme.defaultIconColor};\n    }\n  }\n  & div {\n    display: none;\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Select/Select.tsx",
    "content": "import React, { useState, useRef } from 'react';\nimport useClickOutside from 'lib/hooks/useClickOutside';\nimport DropdownArrowIcon from 'components/common/Icons/DropdownArrowIcon';\n\nimport * as S from './Select.styled';\nimport LiveIcon from './LiveIcon.styled';\n\nexport interface SelectProps {\n  options?: Array<SelectOption>;\n  id?: string;\n  name?: string;\n  selectSize?: 'M' | 'L';\n  isLive?: boolean;\n  minWidth?: string;\n  value?: string | number;\n  defaultValue?: string | number;\n  placeholder?: string;\n  disabled?: boolean;\n  onChange?: (option: string | number) => void;\n  isThemeMode?: boolean;\n}\n\nexport interface SelectOption {\n  label: string | number | React.ReactElement;\n  value: string | number;\n  disabled?: boolean;\n  isLive?: boolean;\n}\n\nconst Select = React.forwardRef<HTMLUListElement, SelectProps>(\n  (\n    {\n      options = [],\n      value,\n      defaultValue,\n      selectSize = 'L',\n      placeholder = '',\n      isLive,\n      disabled = false,\n      onChange,\n      isThemeMode,\n      ...props\n    },\n    ref\n  ) => {\n    const [selectedOption, setSelectedOption] = useState(value);\n    const [showOptions, setShowOptions] = useState(false);\n\n    const showOptionsHandler = () => {\n      if (!disabled) setShowOptions(!showOptions);\n    };\n\n    const selectContainerRef = useRef(null);\n    const clickOutsideHandler = () => setShowOptions(false);\n    useClickOutside(selectContainerRef, clickOutsideHandler);\n\n    const updateSelectedOption = (option: SelectOption) => {\n      if (!option.disabled) {\n        setSelectedOption(option.value);\n\n        if (onChange) {\n          onChange(option.value);\n        }\n\n        setShowOptions(false);\n      }\n    };\n\n    React.useEffect(() => {\n      setSelectedOption(value);\n    }, [isLive, value]);\n\n    return (\n      <div ref={selectContainerRef}>\n        <S.Select\n          role=\"listbox\"\n          selectSize={selectSize}\n          isLive={isLive}\n          disabled={disabled}\n          onClick={showOptionsHandler}\n          onKeyDown={showOptionsHandler}\n          isThemeMode={isThemeMode}\n          ref={ref}\n          tabIndex={0}\n          {...props}\n        >\n          <S.SelectedOptionWrapper>\n            {isLive && <LiveIcon />}\n            <S.SelectedOption\n              role=\"option\"\n              tabIndex={0}\n              isThemeMode={isThemeMode}\n            >\n              {options.find(\n                (option) => option.value === (defaultValue || selectedOption)\n              )?.label || placeholder}\n            </S.SelectedOption>\n          </S.SelectedOptionWrapper>\n          {showOptions && (\n            <S.OptionList>\n              {options?.map((option) => (\n                <S.Option\n                  value={option.value}\n                  key={option.value}\n                  disabled={option.disabled}\n                  onClick={() => updateSelectedOption(option)}\n                  tabIndex={0}\n                  role=\"option\"\n                >\n                  {option.isLive && <LiveIcon />}\n                  {option.label}\n                </S.Option>\n              ))}\n            </S.OptionList>\n          )}\n          <DropdownArrowIcon isOpen={showOptions} />\n        </S.Select>\n      </div>\n    );\n  }\n);\n\nSelect.displayName = 'Select';\n\nexport default Select;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Select/__tests__/Select.spec.tsx",
    "content": "import Select, {\n  SelectOption,\n  SelectProps,\n} from 'components/common/Select/Select';\nimport React from 'react';\nimport { render } from 'lib/testHelpers';\nimport { screen } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\n\njest.mock('react-hook-form', () => ({\n  useFormContext: () => ({\n    register: jest.fn(),\n  }),\n}));\n\nconst options: Array<SelectOption> = [\n  { label: 'test-label1', value: 'test-value1', disabled: false },\n  { label: 'test-label2', value: 'test-value2', disabled: true },\n];\n\nconst renderComponent = (props?: Partial<SelectProps>) =>\n  render(<Select name=\"test\" {...props} />);\n\ndescribe('Custom Select', () => {\n  describe('when isLive is not specified', () => {\n    beforeEach(() => {\n      renderComponent({\n        options,\n      });\n    });\n\n    const getListbox = () => screen.getByRole('listbox');\n    const getOption = () => screen.getByRole('option');\n\n    it('renders component', () => {\n      expect(getListbox()).toBeInTheDocument();\n    });\n\n    it('show select options when select is being clicked', async () => {\n      expect(getOption()).toBeInTheDocument();\n      await userEvent.click(getListbox());\n      expect(screen.getAllByRole('option')).toHaveLength(3);\n    });\n\n    it('checking select option change', async () => {\n      const optionLabel = 'test-label1';\n\n      await userEvent.click(getListbox());\n      await userEvent.selectOptions(getListbox(), [optionLabel]);\n\n      expect(getOption()).toHaveTextContent(optionLabel);\n    });\n\n    it('trying to select disabled option does not trigger change', async () => {\n      const normalOptionLabel = 'test-label1';\n      const disabledOptionLabel = 'test-label2';\n\n      await userEvent.click(getListbox());\n      await userEvent.selectOptions(getListbox(), [normalOptionLabel]);\n      await userEvent.click(getListbox());\n      await userEvent.selectOptions(getListbox(), [disabledOptionLabel]);\n\n      expect(getOption()).toHaveTextContent(normalOptionLabel);\n    });\n  });\n\n  describe('when non-live', () => {\n    it('there is not live icon', () => {\n      renderComponent({ isLive: false });\n      expect(screen.queryByTestId('liveIcon')).not.toBeInTheDocument();\n    });\n  });\n\n  describe('when live', () => {\n    it('there is live icon', () => {\n      render(<Select name=\"test\" {...{ isLive: true }} />);\n      expect(screen.getByRole('listbox')).toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/SlidingSidebar/SlidingSidebar.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const Wrapper = styled.div<{ $open?: boolean }>(\n  ({ theme, $open }) => `\n  background-color: ${theme.default.backgroundColor};\n  position: fixed;\n  top: ${theme.layout.navBarHeight};\n  bottom: 0;\n  width: 37vw;\n  right: calc(${$open ? '0px' : theme.layout.rightSidebarWidth} * -1);\n  box-shadow: -1px 0px 10px 0px rgba(0, 0, 0, 0.2);\n  transition: right 0.3s linear;\n  z-index: 200;\n\n  h3 {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    border-bottom: 1px solid ${theme.layout.stuffBorderColor};\n    padding: 16px;\n  }\n`\n);\n\nexport const Content = styled.div<{ $open?: boolean }>(\n  ({ theme }) => `\n  background-color: ${theme.default.backgroundColor};\n  overflow-y: auto;\n  position: absolute;\n  top: 65px;\n  bottom: 16px;\n  left: 0;\n  right: 0;\n  padding: 16px;\n`\n);\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/SlidingSidebar/SlidingSidebar.tsx",
    "content": "import React, { PropsWithChildren } from 'react';\nimport Heading from 'components/common/heading/Heading.styled';\nimport { Button } from 'components/common/Button/Button';\n\nimport * as S from './SlidingSidebar.styled';\n\ninterface SlidingSidebarProps extends PropsWithChildren<unknown> {\n  open?: boolean;\n  title: string;\n  onClose?: () => void;\n}\n\nconst SlidingSidebar: React.FC<SlidingSidebarProps> = ({\n  open,\n  title,\n  children,\n  onClose,\n}) => {\n  return (\n    <S.Wrapper $open={open}>\n      <Heading level={3}>\n        <span>{title}</span>\n        <Button buttonSize=\"M\" buttonType=\"primary\" onClick={onClose}>\n          Close\n        </Button>\n      </Heading>\n      <S.Content>{children}</S.Content>\n    </S.Wrapper>\n  );\n};\n\nexport default SlidingSidebar;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/SlidingSidebar/index.ts",
    "content": "import SlidingSidebar from './SlidingSidebar';\n\nexport default SlidingSidebar;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Spinner/Spinner.styled.ts",
    "content": "import styled from 'styled-components';\nimport { SpinnerProps } from 'components/common/Spinner/types';\n\nexport const Spinner = styled.div<SpinnerProps>`\n  border-width: ${(props) => props.borderWidth}px;\n  border-style: solid;\n  border-color: ${({ theme }) => theme.pageLoader.borderColor};\n  border-bottom-color: ${(props) =>\n    props.emptyBorderColor\n      ? 'transparent'\n      : props.theme.pageLoader.borderBottomColor};\n  border-radius: 50%;\n  width: ${(props) => props.size}px;\n  height: ${(props) => props.size}px;\n  margin-left: ${(props) => props.marginLeft}px;\n  animation: spin 1.3s linear infinite;\n\n  @keyframes spin {\n    0% {\n      transform: rotate(0deg);\n    }\n    100% {\n      transform: rotate(360deg);\n    }\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Spinner/Spinner.tsx",
    "content": "/* eslint-disable react/default-props-match-prop-types */\nimport React from 'react';\nimport { SpinnerProps } from 'components/common/Spinner/types';\n\nimport * as S from './Spinner.styled';\n\nconst defaultProps: SpinnerProps = {\n  size: 80,\n  borderWidth: 10,\n  emptyBorderColor: false,\n  marginLeft: 0,\n};\n\nconst Spinner: React.FC<SpinnerProps> = (props) => (\n  <S.Spinner role=\"progressbar\" {...props} />\n);\n\nSpinner.defaultProps = defaultProps;\n\nexport default Spinner;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Spinner/types.ts",
    "content": "export interface SpinnerProps {\n  size?: number;\n  borderWidth?: number;\n  emptyBorderColor?: boolean;\n  marginLeft?: number;\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/SuspenseQueryComponent/SuspenseQueryComponent.tsx",
    "content": "import React, { PropsWithChildren } from 'react';\nimport { ErrorBoundary } from 'react-error-boundary';\nimport { Navigate } from 'react-router-dom';\n\nconst ErrorComponent: React.FC<{ error: Error }> = ({ error }) => {\n  const errorStatus = (error as unknown as Response)?.status\n    ? (error as unknown as Response).status\n    : '404';\n\n  return <Navigate to={`/${errorStatus}`} />;\n};\n\n/**\n * @description\n * basic idea that you can not choose a wrong url, that is why you are safe, but when\n * the user tries to manipulate some url to get the the desired result and the BE returns 404\n * it will be propagated to this component and redirected\n *\n * !!NOTE!! But only use this Component for GET query Throw error cause maybe in the future inner functionality may change\n * */\nconst SuspenseQueryComponent: React.FC<PropsWithChildren<unknown>> = ({\n  children,\n}) => {\n  return (\n    <ErrorBoundary FallbackComponent={ErrorComponent}>{children}</ErrorBoundary>\n  );\n};\n\nexport default SuspenseQueryComponent;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/SuspenseQueryComponent/__test__/SuspenseQueryComponent.spec.tsx",
    "content": "import React from 'react';\nimport { render } from 'lib/testHelpers';\nimport { screen } from '@testing-library/react';\nimport SuspenseQueryComponent from 'components/common/SuspenseQueryComponent/SuspenseQueryComponent';\n\nconst fallback = 'fallback';\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  Navigate: () => <div>{fallback}</div>,\n}));\n\ndescribe('SuspenseQueryComponent', () => {\n  const text = 'text';\n\n  it('should render the inner component if no error occurs', () => {\n    render(<SuspenseQueryComponent>{text}</SuspenseQueryComponent>);\n    expect(screen.getByText(text)).toBeInTheDocument();\n  });\n\n  it('should not render the inner component and call navigate', () => {\n    // throwing intentional For error boundaries to work\n    jest.spyOn(console, 'error').mockImplementation(() => undefined);\n    const Component = () => {\n      throw new Error('new Error');\n    };\n\n    render(\n      <SuspenseQueryComponent>\n        <Component />\n      </SuspenseQueryComponent>\n    );\n    expect(screen.queryByText(text)).not.toBeInTheDocument();\n    expect(screen.getByText(fallback)).toBeInTheDocument();\n    jest.clearAllMocks();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Switch/Switch.styled.ts",
    "content": "import styled from 'styled-components';\n\ninterface Props {\n  isCheckedIcon?: boolean;\n}\n\nexport const StyledLabel = styled.label<Props>`\n  position: relative;\n  display: inline-block;\n  width: ${({ isCheckedIcon }) => (isCheckedIcon ? '40px' : '34px')};\n  height: 20px;\n  margin-right: 8px;\n`;\nexport const CheckedIcon = styled.span`\n  position: absolute;\n  top: 1px;\n  left: 24px;\n  z-index: 10;\n  cursor: pointer;\n`;\nexport const UnCheckedIcon = styled.span`\n  position: absolute;\n  top: 2px;\n  right: 23px;\n  z-index: 10;\n  cursor: pointer;\n`;\nexport const StyledSlider = styled.span<Props>`\n  position: absolute;\n  cursor: pointer;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  background-color: ${({ isCheckedIcon, theme }) =>\n    isCheckedIcon\n      ? theme.switch.checkedIcon.backgroundColor\n      : theme.switch.unchecked};\n  transition: 0.4s;\n  border-radius: 20px;\n\n  :hover {\n    background-color: ${({ theme }) => theme.switch.hover};\n  }\n\n  &::before {\n    position: absolute;\n    content: '';\n    height: 14px;\n    width: 14px;\n    left: 3px;\n    bottom: 3px;\n    background-color: ${({ theme }) => theme.switch.circle};\n    transition: 0.4s;\n    border-radius: 50%;\n    z-index: 11;\n  }\n`;\n\nexport const StyledInput = styled.input<Props>`\n  opacity: 0;\n  width: 0;\n  height: 0;\n\n  &:checked + ${StyledSlider} {\n    background-color: ${({ isCheckedIcon, theme }) =>\n      isCheckedIcon\n        ? theme.switch.checkedIcon.backgroundColor\n        : theme.switch.checked};\n  }\n\n  &:focus + ${StyledSlider} {\n    box-shadow: 0 0 1px ${({ theme }) => theme.switch.checked};\n  }\n\n  :checked + ${StyledSlider}:before {\n    transform: translateX(\n      ${({ isCheckedIcon }) => (isCheckedIcon ? '20px' : '14px')}\n    );\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Switch/Switch.tsx",
    "content": "import React from 'react';\n\nimport * as S from './Switch.styled';\n\nexport interface SwitchProps {\n  onChange(): void;\n  checked: boolean;\n  name: string;\n  checkedIcon?: React.ReactNode;\n  unCheckedIcon?: React.ReactNode;\n  bgCustomColor?: string;\n}\nconst Switch: React.FC<SwitchProps> = ({\n  name,\n  checked,\n  onChange,\n  checkedIcon,\n  unCheckedIcon,\n}) => {\n  const isCheckedIcon = !!(checkedIcon || unCheckedIcon);\n  return (\n    <S.StyledLabel isCheckedIcon={isCheckedIcon}>\n      <S.StyledInput\n        name={name}\n        type=\"checkbox\"\n        onChange={onChange}\n        checked={checked}\n        isCheckedIcon={isCheckedIcon}\n      />\n      <S.StyledSlider isCheckedIcon={isCheckedIcon} />\n      {checkedIcon && <S.CheckedIcon>{checkedIcon}</S.CheckedIcon>}\n      {unCheckedIcon && <S.UnCheckedIcon>{unCheckedIcon}</S.UnCheckedIcon>}\n    </S.StyledLabel>\n  );\n};\n\nexport default Switch;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Tag/Tag.styled.tsx",
    "content": "import styled from 'styled-components';\n\ninterface Props {\n  color: 'green' | 'gray' | 'yellow' | 'red' | 'white' | 'blue';\n}\n\nexport const Tag = styled.span.attrs({ role: 'widget' })<Props>`\n  border: none;\n  border-radius: 16px;\n  height: 20px;\n  line-height: 20px;\n  background-color: ${({ theme, color }) => theme.tag.backgroundColor[color]};\n  color: ${({ theme }) => theme.tag.color};\n  font-size: 12px;\n  display: inline-block;\n  padding-left: 0.75em;\n  padding-right: 0.75em;\n  text-align: center;\n  width: max-content;\n  margin: 2px 0;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Tag/getTagColor.ts",
    "content": "import { ConnectorState, ConsumerGroupState } from 'generated-sources';\n\nconst getTagColor = (state?: string) => {\n  switch (state) {\n    case ConnectorState.RUNNING:\n    case ConsumerGroupState.STABLE:\n      return 'green';\n    case ConnectorState.FAILED:\n    case ConnectorState.TASK_FAILED:\n    case ConsumerGroupState.DEAD:\n      return 'red';\n    case ConsumerGroupState.EMPTY:\n      return 'white';\n    default:\n      return 'yellow';\n  }\n};\n\nexport default getTagColor;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Textbox/Textarea.styled.ts",
    "content": "import styled, { css } from 'styled-components';\n\nexport const Textarea = styled.textarea(\n  ({ theme: { textArea } }) => css`\n    border: 1px ${textArea.borderColor.normal} solid;\n    border-radius: 4px;\n    width: 100%;\n    padding: 12px;\n    padding-top: 6px;\n    color: ${({ theme }) => theme.default.color.normal};\n    background-color: ${({ theme }) => theme.schema.backgroundColor.textarea};\n    &::placeholder {\n      color: ${textArea.color.placeholder.normal};\n      font-size: 14px;\n    }\n    &:hover {\n      border-color: ${textArea.borderColor.hover};\n    }\n    &:focus {\n      outline: none;\n      border-color: ${textArea.borderColor.focus};\n      &::placeholder {\n        color: ${textArea.color.placeholder.normal};\n      }\n    }\n    &:disabled {\n      color: ${textArea.color.disabled};\n      border-color: ${textArea.borderColor.disabled};\n      cursor: not-allowed;\n    }\n    &:read-only {\n      color: ${textArea.color.readOnly};\n      border: none;\n      background-color: ${textArea.backgroundColor.readOnly};\n      &:focus {\n        &::placeholder {\n          color: ${textArea.color.placeholder.focus.readOnly};\n        }\n      }\n      cursor: not-allowed;\n    }\n  `\n);\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Tooltip/Tooltip.styled.ts",
    "content": "import styled from 'styled-components';\n\nexport const MessageTooltip = styled.div`\n  max-width: 100%;\n  max-height: 100%;\n  background-color: ${({ theme }) => theme.tooltip.bg};\n  color: ${({ theme }) => theme.tooltip.text};\n  border-radius: 6px;\n  padding: 5px;\n  z-index: 1;\n  white-space: pre-wrap;\n`;\n\nexport const Wrapper = styled.div`\n  display: flex;\n  align-items: center;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Tooltip/Tooltip.tsx",
    "content": "import React, { useState } from 'react';\nimport {\n  useFloating,\n  useHover,\n  useInteractions,\n  Placement,\n} from '@floating-ui/react';\n\nimport * as S from './Tooltip.styled';\n\ninterface TooltipProps {\n  value: React.ReactNode;\n  content: string;\n  placement?: Placement;\n}\n\nconst Tooltip: React.FC<TooltipProps> = ({ value, content, placement }) => {\n  const [open, setOpen] = useState(false);\n  const { x, y, refs, strategy, context } = useFloating({\n    open,\n    onOpenChange: setOpen,\n    placement,\n  });\n  const hover = useHover(context);\n  const { getReferenceProps, getFloatingProps } = useInteractions([hover]);\n  return (\n    <>\n      <div ref={refs.setReference} {...getReferenceProps()}>\n        <S.Wrapper>{value}</S.Wrapper>\n      </div>\n      {open && (\n        <S.MessageTooltip\n          ref={refs.setFloating}\n          style={{\n            position: strategy,\n            top: y ?? 0,\n            left: x ?? 0,\n            width: 'max-content',\n          }}\n          {...getFloatingProps()}\n        >\n          {content}\n        </S.MessageTooltip>\n      )}\n    </>\n  );\n};\n\nexport default Tooltip;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/Tooltip/__tests__/Tooltip.spec.tsx",
    "content": "import React from 'react';\nimport { render } from 'lib/testHelpers';\nimport { screen } from '@testing-library/react';\nimport Tooltip from 'components/common/Tooltip/Tooltip';\nimport userEvent from '@testing-library/user-event';\n\ndescribe('Tooltip', () => {\n  const tooltipText = 'tooltip';\n  const tooltipContent = 'tooltip_Content';\n\n  const setUpComponent = () =>\n    render(<Tooltip value={tooltipText} content={tooltipContent} />);\n\n  it('should render the tooltip element with its value text', () => {\n    setUpComponent();\n    expect(screen.getByText(tooltipText)).toBeInTheDocument();\n  });\n\n  it('should render the tooltip with default closed', () => {\n    setUpComponent();\n    expect(screen.queryByText(tooltipContent)).not.toBeInTheDocument();\n  });\n\n  it('should render the tooltip with and open during hover', async () => {\n    setUpComponent();\n    await userEvent.hover(screen.getByText(tooltipText));\n    expect(screen.getByText(tooltipContent)).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/heading/Heading.styled.tsx",
    "content": "import React, { PropsWithChildren } from 'react';\nimport styled from 'styled-components';\n\ntype HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;\ninterface HeadingBaseProps {\n  $level: HeadingLevel;\n}\nconst HeadingBase = styled.h1<HeadingBaseProps>`\n  ${({ theme }) => theme.heading?.base}\n  ${({ theme, $level }) => theme.heading?.variants[$level]}\n`;\n\ninterface HeadingProps {\n  level?: HeadingLevel;\n}\nconst Heading: React.FC<PropsWithChildren<HeadingProps>> = ({\n  level = 1,\n  ...rest\n}) => {\n  return <HeadingBase as={`h${level}`} $level={level} {...rest} />;\n};\n\nexport default Heading;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/table/Table/Table.styled.ts",
    "content": "import styled from 'styled-components';\n\ninterface Props {\n  isFullwidth?: boolean;\n}\n\nexport const Table = styled.table<Props>`\n  width: ${(props) => (props.isFullwidth ? '100%' : 'auto')};\n\n  & td {\n    border-top: 1px ${({ theme }) => theme.table.td.borderTop} solid;\n    font-size: 14px;\n    font-weight: 400;\n    padding: 8px 8px 8px 24px;\n    color: ${({ theme }) => theme.table.td.color.normal};\n    vertical-align: middle;\n    max-width: 350px;\n    word-wrap: break-word;\n  }\n\n  & tbody > tr {\n    &:hover {\n      background-color: ${({ theme }) => theme.table.tr.backgroundColor.hover};\n    }\n  }\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/table/Table/TableKeyLink.styled.ts",
    "content": "import styled, { css } from 'styled-components';\n\nconst tableLinkMixin = css(\n  ({ theme }) => `\n & > a {\n    color: ${theme.table.link.color.normal};\n    font-weight: 500;\n    text-overflow: ellipsis;\n\n    &:hover {\n      color: ${theme.table.link.color.hover};\n    }\n\n    &:active {\n      color: ${theme.table.link.color.active};\n    }\n  }\n   tr {\n  background-color: red;\n  }\n`\n);\n\nexport const TableKeyLink = styled.div`\n  ${tableLinkMixin}\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/table/TableHeaderCell/TableHeaderCell.styled.ts",
    "content": "import styled, { css } from 'styled-components';\nimport { SortOrder } from 'generated-sources';\n\nexport interface TitleProps {\n  isOrderable?: boolean;\n  isOrdered?: boolean;\n  sortOrder?: SortOrder;\n}\n\nconst orderableMixin = css(\n  ({ theme: { table } }) => `\n    cursor: pointer;\n\n    padding-right: 18px;\n    position: relative;\n\n    &::before,\n    &::after {\n      border: 4px solid transparent;\n      content: '';\n      display: block;\n      height: 0;\n      right: 5px;\n      top: 50%;\n      position: absolute;\n    }\n\n    &::before {\n      border-bottom-color: ${table.th.color.normal};\n      margin-top: -9px;\n    }\n\n    &::after {\n      border-top-color: ${table.th.color.normal};\n      margin-top: 1px;\n    }\n\n    &:hover {\n      color: ${table.th.color.hover};\n      &::before {\n        border-bottom-color: ${table.th.color.hover};\n      }\n      &::after {\n        border-top-color: ${table.th.color.hover};\n      }\n    }\n  `\n);\n\nconst ASCMixin = css(\n  ({ theme: { table } }) => `\n    color: ${table.th.color.active};\n\n    &:before {\n        border-bottom-color: ${table.th.color.active};\n    }\n  `\n);\n\nconst DESCMixin = css(\n  ({ theme: { table } }) => `\n    color: ${table.th.color.active};\n\n    &:after {\n        border-top-color: ${table.th.color.active};\n    }\n  `\n);\n\nexport const Title = styled.span<TitleProps>(\n  ({ isOrderable, isOrdered, sortOrder, theme: { table } }) => css`\n    font-family: Inter, sans-serif;\n    font-size: 12px;\n    font-style: normal;\n    font-weight: 400;\n    line-height: 16px;\n    letter-spacing: 0;\n    text-align: left;\n    display: inline-block;\n    justify-content: start;\n    align-items: center;\n    background: ${table.th.backgroundColor.normal};\n    cursor: default;\n    color: ${table.th.color.normal};\n\n    ${isOrderable && orderableMixin}\n\n    ${isOrderable && isOrdered && sortOrder === SortOrder.ASC && ASCMixin}\n\n    ${isOrderable && isOrdered && sortOrder === SortOrder.DESC && DESCMixin}\n  `\n);\n\nexport const Preview = styled.span`\n  margin-left: 8px;\n  font-family: Inter, sans-serif;\n  font-style: normal;\n  font-weight: 400;\n  line-height: 16px;\n  letter-spacing: 0;\n  text-align: left;\n  background: ${({ theme }) => theme.table.th.backgroundColor.normal};\n  font-size: 14px;\n  color: ${({ theme }) => theme.table.th.previewColor.normal};\n  cursor: pointer;\n`;\n\nexport const TableHeaderCell = styled.th`\n  padding: 4px 0 4px 24px;\n  border-bottom-width: 1px;\n  vertical-align: middle;\n  text-align: left;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/table/TableHeaderCell/TableHeaderCell.tsx",
    "content": "import React, { PropsWithChildren } from 'react';\nimport { SortOrder } from 'generated-sources';\nimport * as S from 'components/common/table/TableHeaderCell/TableHeaderCell.styled';\n\nexport interface TableHeaderCellProps {\n  title?: string;\n  previewText?: string;\n  onPreview?: () => void;\n  orderBy?: string | null;\n  sortOrder?: SortOrder;\n  orderValue?: string;\n  handleOrderBy?: (orderBy: string | null) => void;\n}\n\nconst TableHeaderCell: React.FC<PropsWithChildren<TableHeaderCellProps>> = (\n  props\n) => {\n  const {\n    title,\n    previewText,\n    onPreview,\n    orderBy,\n    sortOrder,\n    orderValue,\n    handleOrderBy,\n    ...restProps\n  } = props;\n\n  const isOrdered = !!orderValue && orderValue === orderBy;\n  const isOrderable = !!(orderValue && handleOrderBy);\n\n  const handleOnClick = () => {\n    return orderValue && handleOrderBy && handleOrderBy(orderValue);\n  };\n  const handleOnKeyDown = (event: React.KeyboardEvent) => {\n    return (\n      event.code === 'Space' &&\n      orderValue &&\n      handleOrderBy &&\n      handleOrderBy(orderValue)\n    );\n  };\n  const orderableProps = isOrderable && {\n    isOrderable,\n    sortOrder,\n    onClick: handleOnClick,\n    onKeyDown: handleOnKeyDown,\n    role: 'button',\n    tabIndex: 0,\n  };\n  return (\n    <S.TableHeaderCell {...restProps}>\n      <S.Title isOrdered={isOrdered} {...orderableProps}>\n        {title}\n      </S.Title>\n\n      {previewText && (\n        <S.Preview\n          onClick={onPreview}\n          onKeyDown={onPreview}\n          role=\"button\"\n          tabIndex={0}\n        >\n          {previewText}\n        </S.Preview>\n      )}\n    </S.TableHeaderCell>\n  );\n};\n\nexport default TableHeaderCell;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/table/TableHeaderCell/__test__/TableHeaderCell.styled.spec.tsx",
    "content": "import React from 'react';\nimport { render } from 'lib/testHelpers';\nimport * as S from 'components/common/table/TableHeaderCell/TableHeaderCell.styled';\nimport { SortOrder } from 'generated-sources';\nimport { screen } from '@testing-library/react';\nimport { theme } from 'theme/theme';\n\ndescribe('TableHeaderCell.Styled', () => {\n  describe('Title Component', () => {\n    const DEFAULT_TITLE_TEXT = 'Text';\n    const setUpComponent = (\n      props: Partial<S.TitleProps> = {},\n      text: string = DEFAULT_TITLE_TEXT\n    ) => {\n      render(\n        <S.Title\n          isOrderable={'isOrderable' in props ? props.isOrderable : true}\n          isOrdered={'isOrdered' in props ? props.isOrdered : true}\n          sortOrder={props.sortOrder || SortOrder.ASC}\n        >\n          {text || DEFAULT_TITLE_TEXT}\n        </S.Title>\n      );\n    };\n    describe('test the default Parameters', () => {\n      beforeEach(() => {\n        setUpComponent();\n      });\n      it('should test the props of Title Component', () => {\n        const titleElement = screen.getByText(DEFAULT_TITLE_TEXT);\n        expect(titleElement).toBeInTheDocument();\n        expect(titleElement).toHaveStyle(\n          `color: ${theme.table.th.color.active};`\n        );\n        expect(titleElement).toHaveStyleRule(\n          'border-bottom-color',\n          theme.table.th.color.active,\n          {\n            modifier: '&:before',\n          }\n        );\n      });\n    });\n\n    describe('Custom props', () => {\n      it('should test the sort order styling of Title Component', () => {\n        setUpComponent({\n          sortOrder: SortOrder.DESC,\n        });\n\n        const titleElement = screen.getByText(DEFAULT_TITLE_TEXT);\n        expect(titleElement).toBeInTheDocument();\n        expect(titleElement).toHaveStyleRule(\n          'color',\n          theme.table.th.color.active\n        );\n        expect(titleElement).toHaveStyleRule(\n          'border-top-color',\n          theme.table.th.color.active,\n          {\n            modifier: '&:after',\n          }\n        );\n      });\n\n      it('should test the Title Component styling without the ordering', () => {\n        setUpComponent({\n          isOrderable: false,\n          isOrdered: false,\n        });\n\n        const titleElement = screen.getByText(DEFAULT_TITLE_TEXT);\n        expect(titleElement).toHaveStyleRule('cursor', 'default');\n      });\n    });\n  });\n\n  describe('Preview Component', () => {\n    const DEFAULT_TEXT = 'DEFAULT_TEXT';\n    it('should render the preview and check themes values', () => {\n      render(<S.Preview>{DEFAULT_TEXT}</S.Preview>);\n      const element = screen.getByText(DEFAULT_TEXT);\n      expect(element).toBeInTheDocument();\n      expect(element).toHaveStyleRule(\n        'background',\n        theme.table.th.backgroundColor.normal\n      );\n      expect(element).toHaveStyleRule(\n        'color',\n        theme.table.th.previewColor.normal\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/table/TableTitle/TableTitle.styled.tsx",
    "content": "import React from 'react';\nimport Heading from 'components/common/heading/Heading.styled';\nimport styled from 'styled-components';\n\nexport const TableTitle = styled((props) => <Heading level={3} {...props} />)`\n  padding: 16px 16px 0;\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/common/table/__tests__/TableHeaderCell.spec.tsx",
    "content": "import React from 'react';\nimport { screen, within } from '@testing-library/react';\nimport { render } from 'lib/testHelpers';\nimport TableHeaderCell, {\n  TableHeaderCellProps,\n} from 'components/common/table/TableHeaderCell/TableHeaderCell';\nimport { SortOrder, TopicColumnsToSort } from 'generated-sources';\nimport { theme } from 'theme/theme';\nimport userEvent from '@testing-library/user-event';\n\nconst SPACE_KEY = ' ';\n\nconst testTitle = 'test title';\nconst testPreviewText = 'test preview text';\nconst handleOrderBy = jest.fn();\nconst onPreview = jest.fn();\n\nconst sortIconTitle = 'Sort icon';\n\ndescribe('TableHeaderCell', () => {\n  const setupComponent = (props: Partial<TableHeaderCellProps> = {}) =>\n    render(\n      <table>\n        <thead>\n          <tr>\n            <TableHeaderCell {...props} />\n          </tr>\n        </thead>\n      </table>\n    );\n  const getColumnHeader = () => screen.getByRole('columnheader');\n\n  it('renders without props', () => {\n    setupComponent();\n    expect(getColumnHeader()).toBeInTheDocument();\n  });\n\n  it('renders with title & preview text', () => {\n    setupComponent({\n      title: testTitle,\n      previewText: testPreviewText,\n    });\n\n    expect(within(getColumnHeader()).getByText(testTitle)).toBeInTheDocument();\n    expect(\n      within(getColumnHeader()).getByText(testPreviewText)\n    ).toBeInTheDocument();\n  });\n\n  it('renders with orderable props', () => {\n    setupComponent({\n      title: testTitle,\n      orderBy: TopicColumnsToSort.NAME,\n      orderValue: TopicColumnsToSort.NAME,\n      sortOrder: SortOrder.ASC,\n      handleOrderBy,\n    });\n    const title = within(getColumnHeader()).getByRole('button');\n    expect(title).toBeInTheDocument();\n    expect(title).toHaveTextContent(testTitle);\n    expect(title).toHaveStyle(`color: ${theme.table.th.color.active};`);\n    expect(title).toHaveStyle('cursor: pointer;');\n  });\n  it('renders click on title triggers handler', async () => {\n    setupComponent({\n      title: testTitle,\n      orderBy: TopicColumnsToSort.NAME,\n      orderValue: TopicColumnsToSort.NAME,\n      handleOrderBy,\n    });\n    const title = within(getColumnHeader()).getByRole('button');\n    await userEvent.click(title);\n    expect(handleOrderBy.mock.calls.length).toBe(1);\n  });\n\n  it('renders space on title triggers handler', async () => {\n    setupComponent({\n      title: testTitle,\n      orderBy: TopicColumnsToSort.NAME,\n      orderValue: TopicColumnsToSort.NAME,\n      handleOrderBy,\n    });\n    const title = within(getColumnHeader()).getByRole('button');\n    await userEvent.type(title, SPACE_KEY);\n    // userEvent.type clicks and only then presses space\n    expect(handleOrderBy.mock.calls.length).toBe(2);\n  });\n\n  it('click on preview triggers handler', async () => {\n    setupComponent({\n      title: testTitle,\n      previewText: testPreviewText,\n      onPreview,\n    });\n    const preview = within(getColumnHeader()).getByRole('button');\n    await userEvent.click(preview);\n    expect(onPreview.mock.calls.length).toBe(1);\n  });\n\n  it('click on preview triggers handler', async () => {\n    setupComponent({\n      title: testTitle,\n      previewText: testPreviewText,\n      onPreview,\n    });\n    const preview = within(getColumnHeader()).getByRole('button');\n    await userEvent.type(preview, SPACE_KEY);\n    expect(onPreview.mock.calls.length).toBe(2);\n  });\n\n  it('renders without sort indication', () => {\n    setupComponent({\n      title: testTitle,\n      orderBy: TopicColumnsToSort.NAME,\n    });\n\n    const title = within(getColumnHeader()).getByText(testTitle);\n    expect(within(title).queryByTitle(sortIconTitle)).not.toBeInTheDocument();\n    expect(title).toHaveStyle('cursor: default;');\n  });\n\n  it('renders with hightlighted title when orderBy and orderValue are equal', () => {\n    setupComponent({\n      title: testTitle,\n      orderBy: TopicColumnsToSort.NAME,\n      orderValue: TopicColumnsToSort.NAME,\n      sortOrder: SortOrder.ASC,\n      handleOrderBy: jest.fn(),\n    });\n    const title = within(getColumnHeader()).getByText(testTitle);\n    expect(title).toHaveStyle(`color: ${theme.table.th.color.active};`);\n  });\n\n  it('renders without hightlighted title when orderBy and orderValue are not equal', () => {\n    setupComponent({\n      title: testTitle,\n      orderBy: TopicColumnsToSort.NAME,\n      orderValue: TopicColumnsToSort.OUT_OF_SYNC_REPLICAS,\n      handleOrderBy: jest.fn(),\n    });\n    const title = within(getColumnHeader()).getByText(testTitle);\n    expect(title).toHaveStyle(`color: ${theme.table.th.color.normal}`);\n  });\n\n  it('renders with default (primary) theme', () => {\n    setupComponent({\n      title: testTitle,\n    });\n\n    const title = within(getColumnHeader()).getByText(testTitle);\n    expect(title).toHaveStyle(\n      `background: ${theme.table.th.backgroundColor.normal};`\n    );\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/contexts/ClusterContext.ts",
    "content": "import React from 'react';\n\nexport interface ContextProps {\n  isReadOnly: boolean;\n  hasKafkaConnectConfigured: boolean;\n  hasSchemaRegistryConfigured: boolean;\n  isTopicDeletionAllowed: boolean;\n}\n\nexport const initialValue: ContextProps = {\n  isReadOnly: false,\n  hasKafkaConnectConfigured: false,\n  hasSchemaRegistryConfigured: false,\n  isTopicDeletionAllowed: true,\n};\nconst ClusterContext = React.createContext(initialValue);\n\nexport default ClusterContext;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/contexts/ConfirmContext.tsx",
    "content": "import React, { useState } from 'react';\n\ninterface ConfirmContextType {\n  content: React.ReactNode;\n  confirm?: () => void;\n  setContent: React.Dispatch<React.SetStateAction<React.ReactNode>>;\n  setConfirm: React.Dispatch<React.SetStateAction<(() => void) | undefined>>;\n  cancel: () => void;\n  dangerButton: boolean;\n  setDangerButton: React.Dispatch<React.SetStateAction<boolean>>;\n}\n\nexport const ConfirmContext = React.createContext<ConfirmContextType | null>(\n  null\n);\n\nexport const ConfirmContextProvider: React.FC<\n  React.PropsWithChildren<unknown>\n> = ({ children }) => {\n  const [content, setContent] = useState<React.ReactNode>(null);\n  const [confirm, setConfirm] = useState<(() => void) | undefined>(undefined);\n  const [dangerButton, setDangerButton] = useState(false);\n\n  const cancel = () => {\n    setContent(null);\n    setConfirm(undefined);\n  };\n\n  return (\n    <ConfirmContext.Provider\n      value={{\n        content,\n        setContent,\n        confirm,\n        setConfirm,\n        cancel,\n        dangerButton,\n        setDangerButton,\n      }}\n    >\n      {children}\n    </ConfirmContext.Provider>\n  );\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/contexts/GlobalSettingsContext.tsx",
    "content": "import { useAppInfo } from 'lib/hooks/api/appConfig';\nimport React from 'react';\nimport { ApplicationInfoEnabledFeaturesEnum } from 'generated-sources';\n\ninterface GlobalSettingsContextProps {\n  hasDynamicConfig: boolean;\n}\n\nexport const GlobalSettingsContext =\n  React.createContext<GlobalSettingsContextProps>({\n    hasDynamicConfig: false,\n  });\n\nexport const GlobalSettingsProvider: React.FC<\n  React.PropsWithChildren<unknown>\n> = ({ children }) => {\n  const info = useAppInfo();\n  const value = React.useMemo(() => {\n    const features = info.data?.enabledFeatures || [];\n    return {\n      hasDynamicConfig: features.includes(\n        ApplicationInfoEnabledFeaturesEnum.DYNAMIC_CONFIG\n      ),\n    };\n  }, [info.data]);\n\n  return (\n    <GlobalSettingsContext.Provider value={value}>\n      {children}\n    </GlobalSettingsContext.Provider>\n  );\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/contexts/ThemeModeContext.tsx",
    "content": "import React, { useMemo } from 'react';\nimport type { FC, PropsWithChildren } from 'react';\nimport type { ThemeDropDownValue } from 'components/NavBar/NavBar';\n\ninterface ThemeModeContextProps {\n  isDarkMode: boolean;\n  themeMode: ThemeDropDownValue;\n  setThemeMode: (value: string | number) => void;\n}\n\nexport const ThemeModeContext = React.createContext<ThemeModeContextProps>({\n  isDarkMode: false,\n  themeMode: 'auto_theme',\n  setThemeMode: () => {},\n});\n\nexport const ThemeModeProvider: FC<PropsWithChildren<unknown>> = ({\n  children,\n}) => {\n  const matchDark = window.matchMedia('(prefers-color-scheme: dark)');\n  const [themeMode, setThemeModeState] =\n    React.useState<ThemeDropDownValue>('auto_theme');\n\n  React.useLayoutEffect(() => {\n    const mode = localStorage.getItem('mode');\n    setThemeModeState((mode as ThemeDropDownValue) ?? 'auto_theme');\n  }, [setThemeModeState]);\n\n  const isDarkMode = React.useMemo(() => {\n    if (themeMode === 'auto_theme') {\n      return matchDark.matches;\n    }\n    return themeMode === 'dark_theme';\n  }, [themeMode]);\n\n  const setThemeMode = React.useCallback(\n    (value: string | number) => {\n      setThemeModeState(value as ThemeDropDownValue);\n      localStorage.setItem('mode', value as string);\n    },\n    [setThemeModeState]\n  );\n\n  const contextValue = useMemo(\n    () => ({\n      isDarkMode,\n      themeMode,\n      setThemeMode,\n    }),\n    [isDarkMode, themeMode, setThemeMode]\n  );\n\n  return (\n    <ThemeModeContext.Provider value={contextValue}>\n      {children}\n    </ThemeModeContext.Provider>\n  );\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/contexts/TopicMessagesContext.ts",
    "content": "import React from 'react';\nimport { SeekDirection } from 'generated-sources';\n\nexport interface ContextProps {\n  seekDirection: SeekDirection;\n  changeSeekDirection(val: string): void;\n  isLive: boolean;\n}\n\nconst TopicMessagesContext = React.createContext<ContextProps>(\n  {} as ContextProps\n);\n\nexport default TopicMessagesContext;\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/contexts/UserInfoRolesAccessContext.tsx",
    "content": "import React, { useMemo } from 'react';\nimport { useGetUserInfo } from 'lib/hooks/api/roles';\nimport { modifyRolesData, RolesModifiedTypes } from 'lib/permissions';\n\nexport interface UserInfoType {\n  username: string;\n  roles: RolesModifiedTypes;\n  rbacFlag: boolean;\n}\n\nexport const UserInfoRolesAccessContext = React.createContext({\n  username: '',\n  roles: new Map() as RolesModifiedTypes,\n  rbacFlag: true,\n});\n\nexport const UserInfoRolesAccessProvider: React.FC<\n  React.PropsWithChildren<unknown>\n> = ({ children }) => {\n  const { data } = useGetUserInfo();\n\n  const contextValue = useMemo(() => {\n    const username = data?.userInfo?.username ? data?.userInfo?.username : '';\n\n    const roles = modifyRolesData(data?.userInfo?.permissions);\n\n    return {\n      username,\n      rbacFlag: !!data?.rbacEnabled,\n      roles,\n    };\n  }, [data]);\n\n  return (\n    <UserInfoRolesAccessContext.Provider value={contextValue}>\n      {children}\n    </UserInfoRolesAccessContext.Provider>\n  );\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/components/globalCss.ts",
    "content": "import { createGlobalStyle, css } from 'styled-components';\n\nexport default createGlobalStyle(\n  ({ theme }) => css`\n    html {\n      font-family: 'Inter', sans-serif;\n      font-size: 14px;\n      -webkit-font-smoothing: antialiased;\n      -moz-osx-font-smoothing: grayscale;\n      background-color: ${theme.default.backgroundColor};\n      overflow-x: hidden;\n      overflow-y: scroll;\n      text-rendering: optimizeLegibility;\n      text-size-adjust: 100%;\n      min-width: 300px;\n    }\n\n    #root,\n    body {\n      width: 100%;\n      position: relative;\n      margin: 0;\n      font-family: 'Inter', sans-serif;\n      font-size: 14px;\n      font-weight: 400;\n      line-height: 20px;\n    }\n\n    article,\n    aside,\n    figure,\n    footer,\n    header,\n    hgroup,\n    section {\n      display: block;\n    }\n\n    body,\n    button,\n    input,\n    optgroup,\n    select,\n    textarea {\n      font-family: inherit;\n    }\n\n    code,\n    pre {\n      font-family: 'Roboto Mono', sans-serif;\n      -moz-osx-font-smoothing: auto;\n      -webkit-font-smoothing: auto;\n      background-color: ${theme.code.backgroundColor};\n      color: ${theme.code.color};\n      font-size: 12px;\n      font-weight: 400;\n      padding: 2px 8px;\n      border-radius: 5px;\n      width: fit-content;\n    }\n\n    pre {\n      overflow-x: auto;\n      white-space: pre;\n      word-wrap: normal;\n\n      code {\n        background-color: transparent;\n        color: currentColor;\n        padding: 0;\n      }\n    }\n\n    a {\n      color: ${theme.link.color};\n      cursor: pointer;\n      text-decoration: none;\n      &:hover {\n        color: ${theme.link.hoverColor};\n      }\n    }\n\n    img {\n      height: auto;\n      max-width: 100%;\n    }\n\n    input[type='checkbox'],\n    input[type='radio'] {\n      vertical-align: baseline;\n    }\n\n    hr {\n      background-color: ${theme.hr.backgroundColor};\n      border: none;\n      display: block;\n      height: 1px;\n      margin: 0;\n    }\n\n    fieldset {\n      border: none;\n    }\n\n    @keyframes fadein {\n      from {\n        opacity: 0;\n      }\n      to {\n        opacity: 1;\n      }\n    }\n  `\n);\n"
  },
  {
    "path": "kafka-ui-react-app/src/index.tsx",
    "content": "import React from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { BrowserRouter } from 'react-router-dom';\nimport { Provider } from 'react-redux';\nimport { ThemeModeProvider } from 'components/contexts/ThemeModeContext';\nimport App from 'components/App';\nimport { store } from 'redux/store';\nimport 'lib/constants';\nimport 'theme/index.scss';\n\nconst container =\n  document.getElementById('root') || document.createElement('div');\nconst root = createRoot(container);\n\nroot.render(\n  <Provider store={store}>\n    <BrowserRouter basename={window.basePath || '/'}>\n      <ThemeModeProvider>\n        <App />\n      </ThemeModeProvider>\n    </BrowserRouter>\n  </Provider>\n);\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/__test__/dateTimeHelpers.spec.ts",
    "content": "import {\n  passedTime,\n  calculateTimer,\n  formatMilliseconds,\n} from 'lib/dateTimeHelpers';\n\nconst startedAt = 1664891890889;\n\ndescribe('format Milliseconds', () => {\n  it('hours > 0', () => {\n    const result = formatMilliseconds(10000000);\n\n    expect(result).toEqual('2h 46m');\n  });\n  it('minutes > 0', () => {\n    const result = formatMilliseconds(1000000);\n\n    expect(result).toEqual('16m 40s');\n  });\n\n  it('seconds > 0', () => {\n    const result = formatMilliseconds(10000);\n\n    expect(result).toEqual('10s');\n  });\n\n  it('milliseconds > 0', () => {\n    const result = formatMilliseconds(100);\n\n    expect(result).toEqual('100ms' || '0ms');\n    expect(formatMilliseconds()).toEqual('0ms');\n  });\n});\n\ndescribe('calculate timer', () => {\n  it('time value < 10', () => {\n    expect(passedTime(5)).toBeTruthy();\n  });\n\n  it('time value > 9', () => {\n    expect(passedTime(10)).toBeTruthy();\n  });\n\n  it('run calculate time', () => {\n    expect(calculateTimer(startedAt));\n  });\n\n  it('return when startedAt > new Date()', () => {\n    expect(calculateTimer(1664891890889199)).toBe('00:00');\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/__test__/paths.spec.ts",
    "content": "import { GIT_REPO_LINK } from 'lib/constants';\nimport * as paths from 'lib/paths';\nimport { RouteParams } from 'lib/paths';\n\nconst clusterName = 'test-cluster-name';\nconst groupId = 'test-group-id';\nconst schemaId = 'test-schema-id';\nconst schemaIdWithNonAsciiChars = 'test/test';\nconst schemaIdWithNonAsciiCharsEncoded = 'test%2Ftest';\nconst topicId = 'test-topic-id';\nconst brokerId = 'test-Broker-id';\nconst connectName = 'test-connect-name';\nconst connectorName = 'test-connector-name';\n\ndescribe('Paths', () => {\n  it('gitCommitPath', () => {\n    expect(paths.gitCommitPath('1234567gh')).toEqual(\n      `${GIT_REPO_LINK}/commit/1234567gh`\n    );\n  });\n  it('getNonExactPath', () => {\n    expect(paths.getNonExactPath('')).toEqual('/*');\n    expect(paths.getNonExactPath('/clusters')).toEqual('/clusters/*');\n  });\n  it('clusterPath', () => {\n    expect(paths.clusterPath(clusterName)).toEqual(\n      `/ui/clusters/${clusterName}`\n    );\n    expect(paths.clusterPath()).toEqual(\n      paths.clusterPath(RouteParams.clusterName)\n    );\n  });\n  it('clusterBrokersPath', () => {\n    expect(paths.clusterBrokersPath(clusterName)).toEqual(\n      `${paths.clusterPath(clusterName)}/brokers`\n    );\n    expect(paths.clusterBrokersPath()).toEqual(\n      paths.clusterBrokersPath(RouteParams.clusterName)\n    );\n\n    expect(paths.clusterBrokerPath(clusterName, brokerId)).toEqual(\n      `${paths.clusterPath(clusterName)}/brokers/${brokerId}`\n    );\n    expect(paths.clusterBrokerPath()).toEqual(\n      paths.clusterBrokerPath(RouteParams.clusterName, RouteParams.brokerId)\n    );\n\n    expect(paths.clusterBrokerMetricsPath(clusterName, brokerId)).toEqual(\n      `${paths.clusterPath(clusterName)}/brokers/${brokerId}/metrics`\n    );\n    expect(paths.clusterBrokerMetricsPath()).toEqual(\n      paths.clusterBrokerMetricsPath(\n        RouteParams.clusterName,\n        RouteParams.brokerId\n      )\n    );\n  });\n  it('clusterConsumerGroupsPath', () => {\n    expect(paths.clusterConsumerGroupsPath(clusterName)).toEqual(\n      `${paths.clusterPath(clusterName)}/consumer-groups`\n    );\n    expect(paths.clusterConsumerGroupsPath()).toEqual(\n      paths.clusterConsumerGroupsPath(RouteParams.clusterName)\n    );\n  });\n  it('clusterConsumerGroupDetailsPath', () => {\n    expect(paths.clusterConsumerGroupDetailsPath(clusterName, groupId)).toEqual(\n      `${paths.clusterConsumerGroupsPath(clusterName)}/${groupId}`\n    );\n    expect(paths.clusterConsumerGroupDetailsPath()).toEqual(\n      paths.clusterConsumerGroupDetailsPath(\n        RouteParams.clusterName,\n        RouteParams.consumerGroupID\n      )\n    );\n  });\n  it('clusterConsumerGroupResetOffsetsPath', () => {\n    expect(\n      paths.clusterConsumerGroupResetOffsetsPath(clusterName, groupId)\n    ).toEqual(\n      `${paths.clusterConsumerGroupDetailsPath(\n        clusterName,\n        groupId\n      )}/reset-offsets`\n    );\n    expect(paths.clusterConsumerGroupResetOffsetsPath()).toEqual(\n      paths.clusterConsumerGroupResetOffsetsPath(\n        RouteParams.clusterName,\n        RouteParams.consumerGroupID\n      )\n    );\n  });\n\n  it('clusterSchemasPath', () => {\n    expect(paths.clusterSchemasPath(clusterName)).toEqual(\n      `${paths.clusterPath(clusterName)}/schemas`\n    );\n    expect(paths.clusterSchemasPath()).toEqual(\n      paths.clusterSchemasPath(RouteParams.clusterName)\n    );\n  });\n  it('clusterSchemaNewPath', () => {\n    expect(paths.clusterSchemaNewPath(clusterName)).toEqual(\n      `${paths.clusterSchemasPath(clusterName)}/create-new`\n    );\n    expect(paths.clusterSchemaNewPath()).toEqual(\n      paths.clusterSchemaNewPath(RouteParams.clusterName)\n    );\n  });\n  it('clusterSchemaPath', () => {\n    expect(paths.clusterSchemaPath(clusterName, schemaId)).toEqual(\n      `${paths.clusterSchemasPath(clusterName)}/${schemaId}`\n    );\n    expect(paths.clusterSchemaPath()).toEqual(\n      paths.clusterSchemaPath(RouteParams.clusterName, RouteParams.subject)\n    );\n    expect(\n      paths.clusterSchemaPath(clusterName, schemaIdWithNonAsciiChars)\n    ).toEqual(\n      `${paths.clusterSchemasPath(\n        clusterName\n      )}/${schemaIdWithNonAsciiCharsEncoded}`\n    );\n  });\n  it('clusterSchemaEditPath', () => {\n    expect(paths.clusterSchemaEditPath(clusterName, schemaId)).toEqual(\n      `${paths.clusterSchemaPath(clusterName, schemaId)}/edit`\n    );\n    expect(paths.clusterSchemaEditPath()).toEqual(\n      paths.clusterSchemaEditPath(RouteParams.clusterName, RouteParams.subject)\n    );\n    expect(\n      paths.clusterSchemaEditPath(clusterName, schemaIdWithNonAsciiChars)\n    ).toEqual(\n      `${paths.clusterSchemaPath(clusterName, schemaIdWithNonAsciiChars)}/edit`\n    );\n  });\n  it('clusterSchemaComparePath', () => {\n    expect(paths.clusterSchemaComparePath(clusterName, schemaId)).toEqual(\n      `${paths.clusterSchemaPath(clusterName, schemaId)}/compare`\n    );\n    expect(paths.clusterSchemaComparePath()).toEqual(\n      paths.clusterSchemaComparePath(\n        RouteParams.clusterName,\n        RouteParams.subject\n      )\n    );\n  });\n\n  it('clusterTopicsPath', () => {\n    expect(paths.clusterTopicsPath(clusterName)).toEqual(\n      `${paths.clusterPath(clusterName)}/all-topics`\n    );\n    expect(paths.clusterTopicsPath()).toEqual(\n      paths.clusterTopicsPath(RouteParams.clusterName)\n    );\n  });\n  it('clusterTopicNewPath', () => {\n    expect(paths.clusterTopicNewPath(clusterName)).toEqual(\n      `${paths.clusterTopicsPath(clusterName)}/create-new-topic`\n    );\n    expect(paths.clusterTopicNewPath()).toEqual(\n      paths.clusterTopicNewPath(RouteParams.clusterName)\n    );\n  });\n  it('clusterTopicPath', () => {\n    expect(paths.clusterTopicPath(clusterName, topicId)).toEqual(\n      `${paths.clusterTopicsPath(clusterName)}/${topicId}`\n    );\n    expect(paths.clusterTopicPath()).toEqual(\n      paths.clusterTopicPath(RouteParams.clusterName, RouteParams.topicName)\n    );\n  });\n  it('clusterTopicSettingsPath', () => {\n    expect(paths.clusterTopicSettingsPath(clusterName, topicId)).toEqual(\n      `${paths.clusterTopicPath(clusterName, topicId)}/settings`\n    );\n    expect(paths.clusterTopicSettingsPath()).toEqual(\n      paths.clusterTopicSettingsPath(\n        RouteParams.clusterName,\n        RouteParams.topicName\n      )\n    );\n  });\n  it('clusterTopicConsumerGroupsPath', () => {\n    expect(paths.clusterTopicConsumerGroupsPath(clusterName, topicId)).toEqual(\n      `${paths.clusterTopicPath(clusterName, topicId)}/consumer-groups`\n    );\n    expect(paths.clusterTopicConsumerGroupsPath()).toEqual(\n      paths.clusterTopicConsumerGroupsPath(\n        RouteParams.clusterName,\n        RouteParams.topicName\n      )\n    );\n  });\n  it('clusterTopicMessagesPath', () => {\n    expect(paths.clusterTopicMessagesPath(clusterName, topicId)).toEqual(\n      `${paths.clusterTopicPath(clusterName, topicId)}/messages`\n    );\n    expect(paths.clusterTopicMessagesPath()).toEqual(\n      paths.clusterTopicMessagesPath(\n        RouteParams.clusterName,\n        RouteParams.topicName\n      )\n    );\n  });\n  it('clusterTopicEditPath', () => {\n    expect(paths.clusterTopicEditPath(clusterName, topicId)).toEqual(\n      `${paths.clusterTopicPath(clusterName, topicId)}/edit`\n    );\n    expect(paths.clusterTopicEditPath()).toEqual(\n      paths.clusterTopicEditPath(RouteParams.clusterName, RouteParams.topicName)\n    );\n  });\n  it('clusterTopicCopyPath', () => {\n    expect(paths.clusterTopicCopyPath(clusterName)).toEqual(\n      `${paths.clusterTopicsPath(clusterName)}/copy`\n    );\n    expect(paths.clusterTopicCopyPath()).toEqual(\n      paths.clusterTopicCopyPath(RouteParams.clusterName)\n    );\n  });\n  it('clusterTopicStatisticsPath', () => {\n    expect(paths.clusterTopicStatisticsPath(clusterName, topicId)).toEqual(\n      `${paths.clusterTopicPath(clusterName, topicId)}/statistics`\n    );\n    expect(paths.clusterTopicStatisticsPath()).toEqual(\n      paths.clusterTopicStatisticsPath(\n        RouteParams.clusterName,\n        RouteParams.topicName\n      )\n    );\n  });\n\n  it('clusterConnectsPath', () => {\n    expect(paths.clusterConnectsPath(clusterName)).toEqual(\n      `${paths.clusterPath(clusterName)}/connects`\n    );\n    expect(paths.clusterConnectsPath()).toEqual(\n      paths.clusterConnectsPath(RouteParams.clusterName)\n    );\n  });\n  it('clusterConnectorsPath', () => {\n    expect(paths.clusterConnectorsPath(clusterName)).toEqual(\n      `${paths.clusterPath(clusterName)}/connectors`\n    );\n    expect(paths.clusterConnectorsPath()).toEqual(\n      paths.clusterConnectorsPath(RouteParams.clusterName)\n    );\n  });\n  it('clusterConnectorNewPath', () => {\n    expect(paths.clusterConnectorNewPath(clusterName)).toEqual(\n      `${paths.clusterConnectorsPath(clusterName)}/create-new`\n    );\n    expect(paths.clusterConnectorNewPath()).toEqual(\n      paths.clusterConnectorNewPath(RouteParams.clusterName)\n    );\n  });\n  it('clusterConnectConnectorPath', () => {\n    expect(\n      paths.clusterConnectConnectorPath(clusterName, connectName, connectorName)\n    ).toEqual(\n      `${paths.clusterConnectsPath(\n        clusterName\n      )}/${connectName}/connectors/${connectorName}`\n    );\n    expect(paths.clusterConnectConnectorPath()).toEqual(\n      paths.clusterConnectConnectorPath(\n        RouteParams.clusterName,\n        RouteParams.connectName,\n        RouteParams.connectorName\n      )\n    );\n  });\n  it('clusterConnectConnectorsPath', () => {\n    expect(\n      paths.clusterConnectConnectorsPath(clusterName, connectName)\n    ).toEqual(\n      `${paths.clusterConnectsPath(clusterName)}/${connectName}/connectors`\n    );\n    expect(paths.clusterConnectConnectorsPath()).toEqual(\n      paths.clusterConnectConnectorsPath(\n        RouteParams.clusterName,\n        RouteParams.connectName\n      )\n    );\n  });\n  it('clusterConnectConnectorEditPath', () => {\n    expect(\n      paths.clusterConnectConnectorEditPath(\n        clusterName,\n        connectName,\n        connectorName\n      )\n    ).toEqual(\n      `${paths.clusterConnectConnectorPath(\n        clusterName,\n        connectName,\n        connectorName\n      )}/edit`\n    );\n    expect(paths.clusterConnectConnectorEditPath()).toEqual(\n      paths.clusterConnectConnectorEditPath(\n        RouteParams.clusterName,\n        RouteParams.connectName,\n        RouteParams.connectorName\n      )\n    );\n  });\n  it('clusterConnectConnectorTasksPath', () => {\n    expect(\n      paths.clusterConnectConnectorTasksPath(\n        clusterName,\n        connectName,\n        connectorName\n      )\n    ).toEqual(\n      `${paths.clusterConnectConnectorPath(\n        clusterName,\n        connectName,\n        connectorName\n      )}/tasks`\n    );\n    expect(paths.clusterConnectConnectorTasksPath()).toEqual(\n      paths.clusterConnectConnectorTasksPath(\n        RouteParams.clusterName,\n        RouteParams.connectName,\n        RouteParams.connectorName\n      )\n    );\n  });\n  it('clusterConnectConnectorConfigPath', () => {\n    expect(\n      paths.clusterConnectConnectorConfigPath(\n        clusterName,\n        connectName,\n        connectorName\n      )\n    ).toEqual(\n      `${paths.clusterConnectConnectorPath(\n        clusterName,\n        connectName,\n        connectorName\n      )}/config`\n    );\n    expect(paths.clusterConnectConnectorConfigPath()).toEqual(\n      paths.clusterConnectConnectorConfigPath(\n        RouteParams.clusterName,\n        RouteParams.connectName,\n        RouteParams.connectorName\n      )\n    );\n  });\n\n  it('clusterKsqlDbPath', () => {\n    expect(paths.clusterKsqlDbPath(clusterName)).toEqual(\n      `${paths.clusterPath(clusterName)}/ksqldb`\n    );\n    expect(paths.clusterKsqlDbPath()).toEqual(\n      paths.clusterKsqlDbPath(RouteParams.clusterName)\n    );\n  });\n  it('clusterKsqlDbQueryPath', () => {\n    expect(paths.clusterKsqlDbQueryPath(clusterName)).toEqual(\n      `${paths.clusterKsqlDbPath(clusterName)}/query`\n    );\n    expect(paths.clusterKsqlDbQueryPath()).toEqual(\n      paths.clusterKsqlDbQueryPath(RouteParams.clusterName)\n    );\n  });\n  it('clusterKsqlDbTablesPath', () => {\n    expect(paths.clusterKsqlDbTablesPath(clusterName)).toEqual(\n      `${paths.clusterKsqlDbPath(clusterName)}/tables`\n    );\n    expect(paths.clusterKsqlDbTablesPath()).toEqual(\n      paths.clusterKsqlDbTablesPath(RouteParams.clusterName)\n    );\n  });\n  it('clusterKsqlDbStreamsPath', () => {\n    expect(paths.clusterKsqlDbStreamsPath(clusterName)).toEqual(\n      `${paths.clusterKsqlDbPath(clusterName)}/streams`\n    );\n    expect(paths.clusterKsqlDbStreamsPath()).toEqual(\n      paths.clusterKsqlDbStreamsPath(RouteParams.clusterName)\n    );\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/__test__/permission.spec.ts",
    "content": "import {\n  isPermitted,\n  isPermittedToCreate,\n  modifyRolesData,\n} from 'lib/permissions';\nimport { Action, ResourceType } from 'generated-sources';\n\ndescribe('Permission Helpers', () => {\n  const clusterName1 = 'local';\n  const clusterName2 = 'dev';\n\n  const userPermissionsMock = [\n    {\n      clusters: [clusterName1],\n      resource: ResourceType.TOPIC,\n      actions: [Action.VIEW, Action.CREATE],\n      value: '.*',\n    },\n    {\n      clusters: [clusterName1],\n      resource: ResourceType.KSQL,\n      actions: [Action.EXECUTE],\n    },\n    {\n      clusters: [clusterName1, clusterName2],\n      resource: ResourceType.SCHEMA,\n      actions: [Action.VIEW],\n      value: '.*',\n    },\n    {\n      clusters: [clusterName1, clusterName2],\n      resource: ResourceType.CONNECT,\n      actions: [Action.VIEW],\n      value: '.*',\n    },\n    {\n      clusters: [clusterName1],\n      resource: ResourceType.APPLICATIONCONFIG,\n      actions: [Action.EDIT],\n    },\n    {\n      clusters: [clusterName1],\n      resource: ResourceType.CLUSTERCONFIG,\n      actions: [Action.EDIT],\n    },\n    {\n      clusters: [clusterName1],\n      resource: ResourceType.CONSUMER,\n      actions: [Action.DELETE],\n      value: '.*',\n    },\n    {\n      clusters: [clusterName1],\n      resource: ResourceType.SCHEMA,\n      actions: [Action.EDIT, Action.DELETE, Action.CREATE],\n      value: '123.*',\n    },\n    {\n      clusters: [clusterName1],\n      resource: ResourceType.ACL,\n      actions: [Action.VIEW],\n    },\n    {\n      clusters: [clusterName1],\n      resource: ResourceType.AUDIT,\n      actions: [Action.VIEW],\n    },\n    {\n      clusters: [clusterName1, clusterName2],\n      resource: ResourceType.TOPIC,\n      value: 'test.*',\n      actions: [Action.MESSAGES_DELETE],\n    },\n    {\n      clusters: [clusterName1, clusterName2],\n      resource: ResourceType.TOPIC,\n      value: '.*',\n      actions: [Action.EDIT, Action.DELETE],\n    },\n    {\n      clusters: [clusterName1, clusterName2],\n      resource: ResourceType.TOPIC,\n      value: 'bobross.*',\n      actions: [Action.VIEW, Action.MESSAGES_READ],\n    },\n  ];\n\n  const roles = modifyRolesData(userPermissionsMock);\n\n  describe('modifyRoles', () => {\n    it('should check if it transforms the data in a correct format to normal keys', () => {\n      const result = modifyRolesData(userPermissionsMock);\n      expect(result.keys()).toContain(clusterName1);\n      expect(result.keys()).toContain(clusterName2);\n\n      const cluster1Map = result.get(clusterName1);\n      const cluster2Map = result.get(clusterName2);\n\n      expect(cluster1Map).toBeDefined();\n      expect(cluster2Map).toBeDefined();\n\n      // first cluster\n      expect(cluster1Map?.has(ResourceType.CLUSTERCONFIG)).toBeTruthy();\n      expect(cluster1Map?.has(ResourceType.CLUSTERCONFIG)).toBeTruthy();\n      expect(cluster1Map?.has(ResourceType.CONSUMER)).toBeTruthy();\n      expect(cluster1Map?.has(ResourceType.CONNECT)).toBeTruthy();\n      expect(cluster1Map?.has(ResourceType.KSQL)).toBeTruthy();\n      expect(cluster1Map?.has(ResourceType.TOPIC)).toBeTruthy();\n\n      // second cluster\n      expect(cluster2Map?.has(ResourceType.SCHEMA)).toBeTruthy();\n      expect(cluster2Map?.has(ResourceType.CONNECT)).toBeTruthy();\n      expect(cluster2Map?.has(ResourceType.TOPIC)).toBeTruthy();\n      expect(cluster2Map?.has(ResourceType.CLUSTERCONFIG)).toBeFalsy();\n\n      expect(cluster2Map?.has(ResourceType.CONSUMER)).toBeFalsy();\n      expect(cluster2Map?.has(ResourceType.KSQL)).toBeFalsy();\n    });\n\n    it('should check if it transforms the data length in keys are correct', () => {\n      const result = modifyRolesData(userPermissionsMock);\n\n      const cluster1Map = result.get(clusterName1);\n      const cluster2Map = result.get(clusterName2);\n\n      expect(result.size).toBe(2);\n\n      expect(cluster1Map?.size).toBe(9);\n      expect(cluster2Map?.size).toBe(3);\n\n      // clusterMap1\n      expect(cluster1Map?.get(ResourceType.TOPIC)).toHaveLength(4);\n      expect(cluster1Map?.get(ResourceType.SCHEMA)).toHaveLength(2);\n      expect(cluster1Map?.get(ResourceType.CONSUMER)).toHaveLength(1);\n      expect(cluster1Map?.get(ResourceType.CLUSTERCONFIG)).toHaveLength(1);\n      expect(cluster1Map?.get(ResourceType.CONNECT)).toHaveLength(1);\n      expect(cluster1Map?.get(ResourceType.CLUSTERCONFIG)).toHaveLength(1);\n\n      // clusterMap2\n      expect(cluster2Map?.get(ResourceType.SCHEMA)).toHaveLength(1);\n    });\n  });\n\n  describe('isPermitted', () => {\n    it('should check if the isPermitted returns the correct when there is no roles or clusters', () => {\n      expect(\n        isPermitted({\n          clusterName: clusterName1,\n          resource: ResourceType.TOPIC,\n          action: Action.VIEW,\n          rbacFlag: true,\n        })\n      ).toBeFalsy();\n\n      expect(\n        isPermitted({\n          clusterName: 'unFoundCluster',\n          resource: ResourceType.TOPIC,\n          action: Action.VIEW,\n          rbacFlag: true,\n        })\n      ).toBeFalsy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: 'unFoundCluster',\n          resource: ResourceType.TOPIC,\n          action: Action.VIEW,\n          rbacFlag: true,\n        })\n      ).toBeFalsy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: '',\n          resource: ResourceType.TOPIC,\n          action: Action.VIEW,\n          rbacFlag: true,\n        })\n      ).toBeFalsy();\n\n      expect(\n        isPermitted({\n          roles: new Map(),\n          clusterName: 'unFoundCluster',\n          resource: ResourceType.TOPIC,\n          action: Action.VIEW,\n          rbacFlag: true,\n        })\n      ).toBeFalsy();\n\n      expect(\n        isPermitted({\n          roles: new Map(),\n          clusterName: clusterName1,\n          resource: ResourceType.TOPIC,\n          action: Action.VIEW,\n          rbacFlag: true,\n        })\n      ).toBeFalsy();\n    });\n\n    it('should check if the isPermitted returns the correct value without resource values (exempt list)', () => {\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.KSQL,\n          action: Action.EXECUTE,\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.CLUSTERCONFIG,\n          action: Action.EDIT,\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.APPLICATIONCONFIG,\n          action: Action.EDIT,\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.ACL,\n          action: Action.VIEW,\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.AUDIT,\n          action: Action.VIEW,\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.TOPIC,\n          action: Action.VIEW,\n          rbacFlag: true,\n        })\n      ).toBeFalsy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.SCHEMA,\n          action: Action.VIEW,\n          rbacFlag: true,\n        })\n      ).toBeFalsy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.CONSUMER,\n          action: Action.VIEW,\n          rbacFlag: true,\n        })\n      ).toBeFalsy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.CONNECT,\n          action: Action.VIEW,\n          rbacFlag: true,\n        })\n      ).toBeFalsy();\n    });\n\n    it('should check if the isPermitted returns the correct value with name values', () => {\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.SCHEMA,\n          action: Action.EDIT,\n          value: '123456',\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.SCHEMA,\n          action: Action.EDIT,\n          value: '123',\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.SCHEMA,\n          action: Action.EDIT,\n          value: 'some_wrong_value',\n          rbacFlag: true,\n        })\n      ).toBeFalsy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName2,\n          resource: ResourceType.TOPIC,\n          action: Action.MESSAGES_DELETE,\n          value: 'test_something',\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.TOPIC,\n          action: Action.MESSAGES_DELETE,\n          value: 'test_something',\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName2,\n          resource: ResourceType.TOPIC,\n          action: Action.EDIT,\n          value: 'any_text',\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName2,\n          resource: ResourceType.TOPIC,\n          action: Action.EDIT,\n          value: 'any_text',\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.TOPIC,\n          action: Action.DELETE,\n          value: 'some_other',\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName2,\n          resource: ResourceType.TOPIC,\n          action: Action.DELETE,\n          value: 'some_other',\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n    });\n\n    it('should test the algorithmic worse case when the input is multiple actions', () => {\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.SCHEMA,\n          action: [Action.EDIT, Action.DELETE],\n          value: '123456',\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.SCHEMA,\n          action: [Action.EDIT],\n          value: '123456',\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.SCHEMA,\n          action: [Action.EDIT],\n          value: '123456',\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.SCHEMA,\n          action: [Action.DELETE],\n          value: '123456',\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.SCHEMA,\n          action: [Action.DELETE, Action.EDIT],\n          value: '123456',\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.SCHEMA,\n          action: [Action.EDIT, Action.VIEW],\n          value: '123456',\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.SCHEMA,\n          action: [Action.EDIT, Action.VIEW],\n          value: 'notFound',\n          rbacFlag: true,\n        })\n      ).toBeFalsy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.SCHEMA,\n          action: [],\n          value: '123456',\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.TOPIC,\n          action: [Action.MESSAGES_READ],\n          value: 'bobross-test',\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n    });\n\n    it('should check the rbac flag and works with permissions accordingly', () => {\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.SCHEMA,\n          action: [],\n          value: '123456',\n          rbacFlag: false,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.SCHEMA,\n          action: [Action.EDIT, Action.VIEW],\n          value: '123456',\n          rbacFlag: false,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.SCHEMA,\n          action: [Action.EDIT, Action.VIEW],\n          value: 'notFound',\n          rbacFlag: false,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermitted({\n          roles: new Map(),\n          clusterName: clusterName1,\n          resource: ResourceType.SCHEMA,\n          action: [Action.EDIT, Action.VIEW],\n          value: 'notFound',\n          rbacFlag: false,\n        })\n      ).toBeTruthy();\n    });\n  });\n\n  describe('isPermittedToCreate', () => {\n    it('should check if the isPermitted returns the correct when there is no roles or clusters', () => {\n      expect(\n        isPermittedToCreate({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.TOPIC,\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermittedToCreate({\n          roles,\n          clusterName: clusterName2,\n          resource: ResourceType.TOPIC,\n          rbacFlag: true,\n        })\n      ).toBeFalsy();\n\n      expect(\n        isPermittedToCreate({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.TOPIC,\n          rbacFlag: false,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermittedToCreate({\n          roles,\n          clusterName: clusterName2,\n          resource: ResourceType.TOPIC,\n          rbacFlag: false,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermittedToCreate({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.SCHEMA,\n          rbacFlag: true,\n        })\n      ).toBeTruthy();\n\n      expect(\n        isPermittedToCreate({\n          roles,\n          clusterName: clusterName1,\n          resource: ResourceType.CONNECT,\n          rbacFlag: true,\n        })\n      ).toBeFalsy();\n\n      expect(\n        isPermittedToCreate({\n          roles: new Map(),\n          clusterName: 'unFoundCluster',\n          resource: ResourceType.TOPIC,\n          rbacFlag: true,\n        })\n      ).toBeFalsy();\n\n      expect(\n        isPermittedToCreate({\n          roles,\n          clusterName: 'unFoundCluster',\n          resource: ResourceType.TOPIC,\n          rbacFlag: true,\n        })\n      ).toBeFalsy();\n\n      expect(\n        isPermittedToCreate({\n          roles: new Map(),\n          clusterName: clusterName1,\n          resource: ResourceType.TOPIC,\n          rbacFlag: true,\n        })\n      ).toBeFalsy();\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/__test__/yupExtended.spec.ts",
    "content": "import { isValidJsonObject } from 'lib/yupExtended';\n\ndescribe('yup extended', () => {\n  describe('isValidJsonObject', () => {\n    it('returns false for no value', () => {\n      expect(isValidJsonObject()).toBeFalsy();\n    });\n\n    it('returns false for invalid string', () => {\n      expect(isValidJsonObject('foo: bar')).toBeFalsy();\n    });\n\n    it('returns false on parsing error', () => {\n      JSON.parse = jest.fn().mockImplementationOnce(() => {\n        throw new Error();\n      });\n      expect(isValidJsonObject('{ \"foo\": \"bar\" }')).toBeFalsy();\n    });\n\n    it('returns true for valid JSON object', () => {\n      expect(isValidJsonObject('{ \"foo\": \"bar\" }')).toBeTruthy();\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/api.ts",
    "content": "import {\n  KsqlApi,\n  TopicsApi,\n  SchemasApi,\n  BrokersApi,\n  MessagesApi,\n  ClustersApi,\n  Configuration,\n  KafkaConnectApi,\n  ConsumerGroupsApi,\n  AuthorizationApi,\n  ApplicationConfigApi,\n  AclsApi,\n} from 'generated-sources';\nimport { BASE_PARAMS } from 'lib/constants';\n\nconst apiClientConf = new Configuration(BASE_PARAMS);\n\nexport const ksqlDbApiClient = new KsqlApi(apiClientConf);\nexport const topicsApiClient = new TopicsApi(apiClientConf);\nexport const brokersApiClient = new BrokersApi(apiClientConf);\nexport const schemasApiClient = new SchemasApi(apiClientConf);\nexport const messagesApiClient = new MessagesApi(apiClientConf);\nexport const clustersApiClient = new ClustersApi(apiClientConf);\nexport const kafkaConnectApiClient = new KafkaConnectApi(apiClientConf);\nexport const consumerGroupsApiClient = new ConsumerGroupsApi(apiClientConf);\nexport const authApiClient = new AuthorizationApi(apiClientConf);\nexport const appConfigApiClient = new ApplicationConfigApi(apiClientConf);\nexport const aclApiClient = new AclsApi(apiClientConf);\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/constants.ts",
    "content": "import { SelectOption } from 'components/common/Select/Select';\nimport { ConfigurationParameters, ConsumerGroupState } from 'generated-sources';\n\ndeclare global {\n  interface Window {\n    basePath: string;\n  }\n}\n\nexport const BASE_PARAMS: ConfigurationParameters = {\n  basePath: window.basePath || '',\n  credentials: 'include',\n  headers: {\n    'Content-Type': 'application/json',\n  },\n};\n\nexport const TOPIC_NAME_VALIDATION_PATTERN = /^[a-zA-Z0-9._-]+$/;\nexport const SCHEMA_NAME_VALIDATION_PATTERN = /^[.,A-Za-z0-9_/-]+$/;\n\nexport const TOPIC_CUSTOM_PARAMS_PREFIX = 'customParams';\nexport const TOPIC_CUSTOM_PARAMS: Record<string, string> = {\n  'compression.type': 'producer',\n  'leader.replication.throttled.replicas': '',\n  'message.downconversion.enable': 'true',\n  'segment.jitter.ms': '0',\n  'flush.ms': '9223372036854775807',\n  'follower.replication.throttled.replicas': '',\n  'segment.bytes': '1073741824',\n  'flush.messages': '9223372036854775807',\n  'message.format.version': '2.3-IV1',\n  'file.delete.delay.ms': '60000',\n  'max.compaction.lag.ms': '9223372036854775807',\n  'min.compaction.lag.ms': '0',\n  'message.timestamp.type': 'CreateTime',\n  preallocate: 'false',\n  'min.cleanable.dirty.ratio': '0.5',\n  'index.interval.bytes': '4096',\n  'unclean.leader.election.enable': 'true',\n  'retention.bytes': '-1',\n  'delete.retention.ms': '86400000',\n  'segment.ms': '604800000',\n  'message.timestamp.difference.max.ms': '9223372036854775807',\n  'segment.index.bytes': '10485760',\n};\n\nexport const MILLISECONDS_IN_WEEK = 604_800_000;\nexport const MILLISECONDS_IN_DAY = 86_400_000;\nexport const MILLISECONDS_IN_SECOND = 1_000;\n\nexport const NOT_SET = -1;\nexport const BYTES_IN_GB = 1_073_741_824;\nexport const BUILD_VERSION_PATTERN = /v\\d.\\d.\\d/;\n\nexport const PER_PAGE = 25;\nexport const MESSAGES_PER_PAGE = '100';\n\nexport const GIT_REPO_LINK = 'https://github.com/provectus/kafka-ui';\nexport const GIT_REPO_LATEST_RELEASE_LINK =\n  'https://api.github.com/repos/provectus/kafka-ui/releases/latest';\n\nexport const LOCAL_STORAGE_KEY_PREFIX = 'kafka-ui';\n\nexport enum AsyncRequestStatus {\n  initial = 'initial',\n  pending = 'pending',\n  fulfilled = 'fulfilled',\n  rejected = 'rejected',\n}\n\nexport const QUERY_REFETCH_OFF_OPTIONS = {\n  refetchOnMount: false,\n  refetchOnWindowFocus: false,\n  refetchIntervalInBackground: false,\n};\n\n// Cluster Form Constants\nexport const AUTH_OPTIONS: SelectOption[] = [\n  { value: 'SASL/JAAS', label: 'SASL/JAAS' },\n  { value: 'SASL/GSSAPI', label: 'SASL/GSSAPI' },\n  { value: 'SASL/OAUTHBEARER', label: 'SASL/OAUTHBEARER' },\n  { value: 'SASL/PLAIN', label: 'SASL/PLAIN' },\n  { value: 'SASL/SCRAM-256', label: 'SASL/SCRAM-256' },\n  { value: 'SASL/SCRAM-512', label: 'SASL/SCRAM-512' },\n  { value: 'Delegation tokens', label: 'Delegation tokens' },\n  { value: 'SASL/LDAP', label: 'SASL/LDAP' },\n  { value: 'SASL/AWS IAM', label: 'SASL/AWS IAM' },\n  { value: 'mTLS', label: 'mTLS' },\n];\n\nexport const SECURITY_PROTOCOL_OPTIONS: SelectOption[] = [\n  { value: 'SASL_SSL', label: 'SASL_SSL' },\n  { value: 'SASL_PLAINTEXT', label: 'SASL_PLAINTEXT' },\n];\nexport const METRICS_OPTIONS: SelectOption[] = [\n  { value: 'JMX', label: 'JMX' },\n  { value: 'PROMETHEUS', label: 'PROMETHEUS' },\n];\n\nexport const CONSUMER_GROUP_STATE_TOOLTIPS: Record<ConsumerGroupState, string> =\n  {\n    EMPTY: 'The group exists but has no members.',\n    STABLE: 'Consumers are happily consuming and have assigned partitions.',\n    PREPARING_REBALANCE:\n      'Something has changed, and the reassignment of partitions is required.',\n    COMPLETING_REBALANCE: 'Partition reassignment is in progress.',\n    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.',\n    UNKNOWN: '',\n  } as const;\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/dateTimeHelpers.ts",
    "content": "export const formatTimestamp = (\n  timestamp?: number | string | Date,\n  format: Intl.DateTimeFormatOptions = { hourCycle: 'h23' }\n): string => {\n  if (!timestamp) {\n    return '';\n  }\n\n  // empty array gets the default one from the browser\n  const date = new Date(timestamp);\n  // invalid date\n  if (Number.isNaN(date.getTime())) {\n    return '';\n  }\n\n  // browser support\n  const language = navigator.language || navigator.languages[0];\n  return date.toLocaleString(language || [], format);\n};\n\nexport const formatMilliseconds = (input = 0) => {\n  const milliseconds = Math.max(input || 0, 0);\n\n  const seconds = Math.floor(milliseconds / 1000);\n  const minutes = Math.floor(seconds / 60);\n  const hours = Math.floor(minutes / 60);\n\n  if (hours > 0) {\n    return `${hours}h ${minutes % 60}m`;\n  }\n\n  if (minutes > 0) {\n    return `${minutes}m ${seconds % 60}s`;\n  }\n\n  if (seconds > 0) {\n    return `${seconds}s`;\n  }\n\n  return `${milliseconds}ms`;\n};\n\nexport const passedTime = (value: number) => (value < 10 ? `0${value}` : value);\n\nexport const calculateTimer = (startedAt: number) => {\n  const nowDate = new Date();\n  const now = nowDate.getTime();\n  const newDate = now - startedAt;\n  const minutes = nowDate.getMinutes();\n  const second = nowDate.getSeconds();\n\n  return newDate > 0 ? `${passedTime(minutes)}:${passedTime(second)}` : '00:00';\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/errorHandling.tsx",
    "content": "import React from 'react';\nimport Alert from 'components/common/Alert/Alert';\nimport toast, { ToastType } from 'react-hot-toast';\nimport { ErrorResponse } from 'generated-sources';\n\ninterface ServerResponse {\n  status: number;\n  statusText: string;\n  url?: string;\n  message?: ErrorResponse['message'];\n}\nexport type ToastTypes = ToastType | 'warning';\n\nexport const getResponse = async (\n  response: Response\n): Promise<ServerResponse> => {\n  let body;\n  try {\n    body = await response.json();\n  } catch (e) {\n    // do nothing;\n  }\n  return {\n    status: response.status,\n    statusText: response.statusText,\n    url: response.url,\n    message: body?.message,\n  };\n};\n\ninterface AlertOptions {\n  id?: string;\n  title?: string;\n  message: React.ReactNode;\n}\n\nexport const showAlert = (\n  type: ToastTypes,\n  { title, message, id }: AlertOptions\n) => {\n  toast.custom(\n    (t) => (\n      <Alert\n        title={title || ''}\n        type={type}\n        message={message}\n        onDissmiss={() => toast.remove(t.id)}\n      />\n    ),\n    { id }\n  );\n};\n\nexport const showSuccessAlert = (options: AlertOptions) => {\n  showAlert('success', {\n    ...options,\n    title: options.title || 'Success',\n  });\n};\n\nexport const showServerError = async (\n  response: Response,\n  options?: AlertOptions\n) => {\n  let body: Record<string, string> = {};\n  try {\n    body = await response.json();\n  } catch (e) {\n    // do nothing;\n  }\n  if (response.status) {\n    showAlert('error', {\n      id: response.url,\n      title: `${response.status} ${response.statusText}`,\n      message: body?.message || 'An error occurred',\n      ...options,\n    });\n  } else {\n    showAlert('error', {\n      id: 'server-error',\n      title: `Something went wrong`,\n      message: 'An error occurred',\n      ...options,\n    });\n  }\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/fixtures/acls.ts",
    "content": "import {\n  KafkaAcl,\n  KafkaAclResourceType,\n  KafkaAclNamePatternType,\n  KafkaAclPermissionEnum,\n  KafkaAclOperationEnum,\n} from 'generated-sources';\n\nexport const aclPayload: KafkaAcl[] = [\n  {\n    principal: 'User 1',\n    resourceName: 'Topic',\n    resourceType: KafkaAclResourceType.TOPIC,\n    host: '_host1',\n    namePatternType: KafkaAclNamePatternType.LITERAL,\n    permission: KafkaAclPermissionEnum.ALLOW,\n    operation: KafkaAclOperationEnum.READ,\n  },\n  {\n    principal: 'User 2',\n    resourceName: 'Topic',\n    resourceType: KafkaAclResourceType.TOPIC,\n    host: '_host1',\n    namePatternType: KafkaAclNamePatternType.PREFIXED,\n    permission: KafkaAclPermissionEnum.ALLOW,\n    operation: KafkaAclOperationEnum.READ,\n  },\n  {\n    principal: 'User 3',\n    resourceName: 'Topic',\n    resourceType: KafkaAclResourceType.TOPIC,\n    host: '_host1',\n    namePatternType: KafkaAclNamePatternType.LITERAL,\n    permission: KafkaAclPermissionEnum.DENY,\n    operation: KafkaAclOperationEnum.READ,\n  },\n];\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/fixtures/brokers.ts",
    "content": "import { BrokerConfig, BrokersLogdirs, ConfigSource } from 'generated-sources';\n\nexport const brokersPayload = [\n  { id: 100, host: 'b-1.test.kafka.amazonaws.com', port: 9092 },\n  { id: 200, host: 'b-2.test.kafka.amazonaws.com', port: 9092 },\n];\n\nconst partition = {\n  broker: 2,\n  offsetLag: 0,\n  partition: 2,\n  size: 0,\n};\nconst topics = {\n  name: '_confluent-ksql-devquery_CTAS_NUMBER_OF_TESTS_59-Aggregate-Aggregate-Materialize-changelog',\n  partitions: [partition],\n};\n\nexport const brokerLogDirsPayload: BrokersLogdirs[] = [\n  {\n    error: 'NONE',\n    name: '/opt/kafka/data-0/logs',\n    topics: [\n      {\n        ...topics,\n        partitions: [partition, partition, partition],\n      },\n      topics,\n      {\n        ...topics,\n        partitions: [],\n      },\n    ],\n  },\n  {\n    error: 'NONE',\n    name: '/opt/kafka/data-1/logs',\n  },\n];\n\nexport const brokerConfigPayload: BrokerConfig[] = [\n  {\n    name: 'compression.type',\n    value: 'producer',\n    source: ConfigSource.DYNAMIC_BROKER_CONFIG,\n    isSensitive: false,\n    isReadOnly: false,\n    synonyms: [\n      {\n        name: 'compression.type',\n        value: 'producer',\n        source: ConfigSource.DYNAMIC_BROKER_CONFIG,\n      },\n      {\n        name: 'compression.type',\n        value: 'producer',\n        source: ConfigSource.DEFAULT_CONFIG,\n      },\n    ],\n  },\n  {\n    name: 'confluent.value.schema.validation',\n    value: 'false',\n    source: ConfigSource.DEFAULT_CONFIG,\n    isSensitive: false,\n    isReadOnly: false,\n    synonyms: [],\n  },\n  {\n    name: 'leader.replication.throttled.replicas',\n    value: '',\n    source: ConfigSource.DEFAULT_CONFIG,\n    isSensitive: false,\n    isReadOnly: false,\n    synonyms: [],\n  },\n  {\n    name: 'confluent.key.subject.name.strategy',\n    value: 'io.confluent.kafka.serializers.subject.TopicNameStrategy',\n    source: ConfigSource.DEFAULT_CONFIG,\n    isSensitive: false,\n    isReadOnly: false,\n    synonyms: [],\n  },\n  {\n    name: 'message.downconversion.enable',\n    value: 'true',\n    source: ConfigSource.DEFAULT_CONFIG,\n    isSensitive: false,\n    isReadOnly: false,\n    synonyms: [\n      {\n        name: 'log.message.downconversion.enable',\n        value: 'true',\n        source: ConfigSource.DEFAULT_CONFIG,\n      },\n    ],\n  },\n  {\n    name: 'min.insync.replicas',\n    value: '1',\n    source: ConfigSource.DYNAMIC_BROKER_CONFIG,\n    isSensitive: false,\n    isReadOnly: false,\n    synonyms: [\n      {\n        name: 'min.insync.replicas',\n        value: '1',\n        source: ConfigSource.DYNAMIC_BROKER_CONFIG,\n      },\n      {\n        name: 'min.insync.replicas',\n        value: '1',\n        source: ConfigSource.DEFAULT_CONFIG,\n      },\n    ],\n  },\n];\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/fixtures/clusters.ts",
    "content": "import { Cluster, ServerStatus } from 'generated-sources';\n\nexport const onlineClusterPayload: Cluster = {\n  name: 'secondLocal',\n  defaultCluster: true,\n  status: ServerStatus.ONLINE,\n  brokerCount: 1,\n  onlinePartitionCount: 6,\n  topicCount: 3,\n  bytesInPerSec: 1.55,\n  bytesOutPerSec: 9.314,\n  readOnly: false,\n  features: [],\n};\nexport const offlineClusterPayload: Cluster = {\n  name: 'local',\n  defaultCluster: false,\n  status: ServerStatus.OFFLINE,\n  brokerCount: 1,\n  onlinePartitionCount: 2,\n  topicCount: 2,\n  bytesInPerSec: 3.42,\n  bytesOutPerSec: 4.14,\n  features: [],\n  readOnly: true,\n};\n\nexport const clustersPayload: Cluster[] = [\n  onlineClusterPayload,\n  offlineClusterPayload,\n];\n\nexport const clusterStatsPayload = {\n  brokerCount: 2,\n  activeControllers: 100,\n  onlinePartitionCount: 138,\n  offlinePartitionCount: 0,\n  inSyncReplicasCount: 239,\n  outOfSyncReplicasCount: 0,\n  underReplicatedPartitionCount: 0,\n  diskUsage: [\n    { brokerId: 100, segmentSize: 334567, segmentCount: 245 },\n    { brokerId: 200, segmentSize: 12345678, segmentCount: 121 },\n  ],\n  version: '2.2.1',\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/fixtures/consumerGroups.ts",
    "content": "import { ConsumerGroupState } from 'generated-sources';\n\nexport const consumerGroupPayload = {\n  groupId: 'amazon.msk.canary.group.broker-1',\n  members: 0,\n  topics: 2,\n  simple: false,\n  partitionAssignor: '',\n  state: ConsumerGroupState.EMPTY,\n  coordinator: {\n    id: 2,\n    host: 'b-2.kad-msk.st2jzq.c6.kafka.eu-west-1.amazonaws.com',\n  },\n  consumerLag: 0,\n  partitions: [\n    {\n      topic: '__amazon_msk_canary',\n      partition: 1,\n      currentOffset: 0,\n      endOffset: 0,\n      consumerLag: 0,\n      consumerId: undefined,\n      host: undefined,\n    },\n    {\n      topic: '__amazon_msk_canary',\n      partition: 0,\n      currentOffset: 56932,\n      endOffset: 56932,\n      consumerLag: 0,\n      consumerId: undefined,\n      host: undefined,\n    },\n    {\n      topic: 'other_topic',\n      partition: 3,\n      currentOffset: 56932,\n      endOffset: 56932,\n      consumerLag: 0,\n      consumerId: undefined,\n      host: undefined,\n    },\n    {\n      topic: 'other_topic',\n      partition: 4,\n      currentOffset: 56932,\n      endOffset: 56932,\n      consumerLag: 0,\n      consumerId: undefined,\n      host: undefined,\n    },\n  ],\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/fixtures/kafkaConnect.ts",
    "content": "import {\n  Connect,\n  Connector,\n  ConnectorState,\n  ConnectorTaskStatus,\n  ConnectorType,\n  FullConnectorInfo,\n  Task,\n} from 'generated-sources';\n\nexport const connects: Connect[] = [\n  { name: 'first', address: 'localhost:8083' },\n  { name: 'second', address: 'localhost:8084' },\n];\n\nexport const connectors: FullConnectorInfo[] = [\n  {\n    connect: 'first',\n    name: 'hdfs-source-connector',\n    connectorClass: 'FileStreamSource',\n    type: ConnectorType.SOURCE,\n    topics: ['a', 'b', 'c'],\n    status: {\n      state: ConnectorState.RUNNING,\n    },\n    tasksCount: 2,\n    failedTasksCount: 0,\n  },\n  {\n    connect: 'second',\n    name: 'hdfs2-source-connector',\n    connectorClass: 'FileStreamSource',\n    type: ConnectorType.SINK,\n    topics: ['test-topic'],\n    status: {\n      state: ConnectorState.FAILED,\n    },\n    tasksCount: 3,\n    failedTasksCount: 1,\n  },\n];\n\nexport const connector: Connector = {\n  connect: 'first',\n  name: 'hdfs-source-connector',\n  type: ConnectorType.SOURCE,\n  status: {\n    state: ConnectorState.RUNNING,\n    workerId: 'kafka-connect0:8083',\n  },\n  config: {\n    'connector.class': 'FileStreamSource',\n    'tasks.max': '10',\n    topic: 'test-topic',\n    file: '/some/file',\n  },\n  tasks: [{ connector: 'first', task: 1 }],\n};\n\nexport const tasks: Task[] = [\n  {\n    id: { connector: 'first', task: 1 },\n    status: {\n      id: 1,\n      state: ConnectorTaskStatus.RUNNING,\n      workerId: 'kafka-connect0:8083',\n    },\n    config: {\n      'batch.size': '2000',\n      file: '/some/file',\n      'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask',\n      topic: 'test-topic',\n    },\n  },\n  {\n    id: { connector: 'first', task: 2 },\n    status: {\n      id: 2,\n      state: ConnectorTaskStatus.FAILED,\n      trace: 'Failure 1',\n      workerId: 'kafka-connect0:8083',\n    },\n    config: {\n      'batch.size': '1000',\n      file: '/some/file2',\n      'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask',\n      topic: 'test-topic',\n    },\n  },\n  {\n    id: { connector: 'first', task: 3 },\n    status: {\n      id: 3,\n      state: ConnectorTaskStatus.RUNNING,\n      workerId: 'kafka-connect0:8083',\n      trace:\n        '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.',\n    },\n    config: {\n      'batch.size': '3000',\n      file: '/some/file3',\n      'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask',\n      topic: 'test-topic',\n    },\n  },\n  {\n    id: { connector: 'first', task: 4 },\n    status: {\n      id: 4,\n      state: ConnectorTaskStatus.PAUSED,\n      workerId: 'kafka-connect0:8083',\n    },\n    config: {\n      'batch.size': '3000',\n      file: '/some/file3',\n      'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask',\n      topic: 'test-topic',\n    },\n  },\n];\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/fixtures/latestVersion.ts",
    "content": "export const deprecatedVersionPayload = {\n  build: {\n    buildTime: '2023-04-14T09:47:35.463Z',\n    commitId: '96a577a',\n    isLatestRelease: false,\n    version: '96a577a98c6069376c5d22ed49cffd3739f1bbdc',\n  },\n};\nexport const latestVersionPayload = {\n  build: {\n    buildTime: '2023-04-14T09:47:35.463Z',\n    commitId: '96a577a',\n    isLatestRelease: true,\n    version: '96a577a98c6069376c5d22ed49cffd3739f1bbdc',\n  },\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/fixtures/topicMessages.ts",
    "content": "import { TopicSerdeSuggestion } from 'generated-sources';\n\nexport const serdesPayload: TopicSerdeSuggestion = {\n  key: [\n    {\n      name: 'String',\n      description: undefined,\n      preferred: false,\n      schema: undefined,\n      additionalProperties: undefined,\n    },\n    {\n      name: 'Int32',\n      description: undefined,\n      preferred: true,\n      schema:\n        '{   \"type\" : \"integer\",   \"minimum\" : -2147483648,   \"maximum\" : 2147483647 }',\n      additionalProperties: {},\n    },\n  ],\n  value: [\n    {\n      name: 'String',\n      description: undefined,\n      preferred: false,\n      schema: undefined,\n      additionalProperties: undefined,\n    },\n    {\n      name: 'Int64',\n      description: undefined,\n      preferred: true,\n      schema:\n        '{   \"type\" : \"integer\",   \"minimum\" : -9223372036854775808,   \"maximum\" : 9223372036854775807 }',\n      additionalProperties: {},\n    },\n  ],\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/fixtures/topics.ts",
    "content": "import {\n  ConfigSource,\n  ConsumerGroup,\n  ConsumerGroupState,\n  Topic,\n  TopicConfig,\n  TopicAnalysis,\n} from 'generated-sources';\n\nexport const internalTopicPayload = {\n  name: '__internal.topic',\n  internal: true,\n  partitionCount: 1,\n  replicationFactor: 1,\n  replicas: 1,\n  inSyncReplicas: 1,\n  segmentSize: 0,\n  segmentCount: 1,\n  underReplicatedPartitions: 0,\n  partitions: [\n    {\n      partition: 0,\n      leader: 1,\n      replicas: [{ broker: 1, leader: false, inSync: true }],\n      offsetMax: 0,\n      offsetMin: 0,\n    },\n  ],\n};\n\nexport const externalTopicPayload = {\n  name: 'external.topic',\n  internal: false,\n  partitionCount: 1,\n  replicationFactor: 1,\n  replicas: 1,\n  inSyncReplicas: 1,\n  segmentSize: 1263,\n  segmentCount: 1,\n  underReplicatedPartitions: 0,\n  partitions: [\n    {\n      partition: 0,\n      leader: 1,\n      replicas: [{ broker: 1, leader: false, inSync: true }],\n      offsetMax: 0,\n      offsetMin: 0,\n    },\n  ],\n};\n\nexport const topicsPayload: Topic[] = [\n  internalTopicPayload,\n  externalTopicPayload,\n];\n\nexport const topicConsumerGroups: ConsumerGroup[] = [\n  {\n    groupId: 'amazon.msk.canary.group.broker-7',\n    topics: 0,\n    members: 0,\n    simple: false,\n    partitionAssignor: '',\n    state: ConsumerGroupState.UNKNOWN,\n    coordinator: { id: 1 },\n    consumerLag: 9,\n  },\n  {\n    groupId: 'amazon.msk.canary.group.broker-4',\n    topics: 0,\n    members: 0,\n    simple: false,\n    partitionAssignor: '',\n    state: ConsumerGroupState.COMPLETING_REBALANCE,\n    coordinator: { id: 1 },\n    consumerLag: 9,\n  },\n];\n\nexport const topicConfigPayload: TopicConfig[] = [\n  {\n    name: 'compression.type',\n    value: 'producer',\n    defaultValue: 'producer',\n    source: ConfigSource.DYNAMIC_TOPIC_CONFIG,\n    isSensitive: false,\n    isReadOnly: false,\n    synonyms: [\n      {\n        name: 'compression.type',\n        value: 'producer',\n        source: ConfigSource.DYNAMIC_TOPIC_CONFIG,\n      },\n      {\n        name: 'compression.type',\n        value: 'producer',\n        source: ConfigSource.DEFAULT_CONFIG,\n      },\n    ],\n  },\n  {\n    name: 'confluent.value.schema.validation',\n    value: 'false',\n    source: ConfigSource.DEFAULT_CONFIG,\n    isSensitive: false,\n    isReadOnly: false,\n    synonyms: [],\n  },\n  {\n    name: 'leader.replication.throttled.replicas',\n    value: '',\n    defaultValue: '',\n    source: ConfigSource.DEFAULT_CONFIG,\n    isSensitive: false,\n    isReadOnly: false,\n    synonyms: [],\n  },\n  {\n    name: 'confluent.key.subject.name.strategy',\n    value: 'io.confluent.kafka.serializers.subject.TopicNameStrategy',\n    source: ConfigSource.DEFAULT_CONFIG,\n    isSensitive: false,\n    isReadOnly: false,\n    synonyms: [],\n  },\n  {\n    name: 'message.downconversion.enable',\n    value: 'true',\n    defaultValue: 'true',\n    source: ConfigSource.DEFAULT_CONFIG,\n    isSensitive: false,\n    isReadOnly: false,\n    synonyms: [\n      {\n        name: 'log.message.downconversion.enable',\n        value: 'true',\n        source: ConfigSource.DEFAULT_CONFIG,\n      },\n    ],\n  },\n  {\n    name: 'min.insync.replicas',\n    value: '1',\n    defaultValue: '1',\n    source: ConfigSource.DYNAMIC_TOPIC_CONFIG,\n    isSensitive: false,\n    isReadOnly: false,\n    synonyms: [\n      {\n        name: 'min.insync.replicas',\n        value: '1',\n        source: ConfigSource.DYNAMIC_TOPIC_CONFIG,\n      },\n      {\n        name: 'min.insync.replicas',\n        value: '1',\n        source: ConfigSource.DEFAULT_CONFIG,\n      },\n    ],\n  },\n];\n\nconst topicStatsSize = {\n  sum: 0,\n  avg: 0,\n  prctl50: 0,\n  prctl75: 0,\n  prctl95: 0,\n  prctl99: 0,\n  prctl999: 0,\n};\nexport const topicStatsPayload: TopicAnalysis = {\n  progress: {\n    startedAt: 1659984559167,\n    completenessPercent: 43,\n    msgsScanned: 18077002,\n    bytesScanned: 6750901718,\n  },\n  result: {\n    startedAt: 1659984559095,\n    finishedAt: 1659984617816,\n    totalStats: {\n      totalMsgs: 18194715,\n      minOffset: 98869591,\n      maxOffset: 100576010,\n      minTimestamp: 1659719759485,\n      maxTimestamp: 1659984603419,\n      nullKeys: 18194715,\n      nullValues: 0,\n      approxUniqKeys: 0,\n      approxUniqValues: 17817283,\n      keySize: topicStatsSize,\n      valueSize: topicStatsSize,\n      hourlyMsgCounts: [\n        { hourStart: 1659718800000, count: 16157 },\n        { hourStart: 1659722400000, count: 225790 },\n      ],\n    },\n    partitionStats: [\n      {\n        partition: 0,\n        totalMsgs: 1515285,\n        minOffset: 99060726,\n        maxOffset: 100576010,\n        minTimestamp: 1659722684090,\n        maxTimestamp: 1659984603419,\n        nullKeys: 1515285,\n        nullValues: 0,\n        approxUniqKeys: 0,\n        approxUniqValues: 1515285,\n        keySize: topicStatsSize,\n        valueSize: topicStatsSize,\n        hourlyMsgCounts: [\n          { hourStart: 1659722400000, count: 18040 },\n          { hourStart: 1659726000000, count: 20070 },\n        ],\n      },\n      {\n        partition: 1,\n        totalMsgs: 1534422,\n        minOffset: 98897827,\n        maxOffset: 100432248,\n        minTimestamp: 1659722803993,\n        maxTimestamp: 1659984603416,\n        nullKeys: 1534422,\n        nullValues: 0,\n        approxUniqKeys: 0,\n        approxUniqValues: 1516431,\n        keySize: topicStatsSize,\n        valueSize: topicStatsSize,\n        hourlyMsgCounts: [{ hourStart: 1659722400000, count: 19058 }],\n      },\n    ],\n  },\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/__tests__/dateTimeHelpers.spec.ts",
    "content": "import { formatTimestamp } from 'lib/dateTimeHelpers';\n\ndescribe('dateTimeHelpers', () => {\n  describe('formatTimestamp', () => {\n    it('should check the empty case', () => {\n      expect(formatTimestamp('')).toBe('');\n    });\n\n    it('should check the invalid case', () => {\n      expect(formatTimestamp('invalid')).toBe('');\n    });\n\n    it('should output the correct date', () => {\n      const date = new Date();\n      expect(formatTimestamp(date)).toBe(\n        date.toLocaleString([], { hourCycle: 'h23' })\n      );\n      expect(formatTimestamp(date.getTime())).toBe(\n        date.toLocaleString([], { hourCycle: 'h23' })\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/__tests__/fixtures.ts",
    "content": "import { Action, ResourceType } from 'generated-sources';\nimport { modifyRolesData } from 'lib/permissions';\n\nexport const clusterName1 = 'local';\nexport const clusterName2 = 'dev';\n\nconst userPermissionsMock = [\n  {\n    clusters: [clusterName1],\n    resource: ResourceType.TOPIC,\n    actions: [Action.CREATE],\n  },\n  {\n    clusters: [clusterName1],\n    resource: ResourceType.SCHEMA,\n    actions: [Action.EDIT, Action.DELETE],\n    value: '123.*',\n  },\n  {\n    clusters: [clusterName1, clusterName2],\n    resource: ResourceType.TOPIC,\n    value: 'test.*',\n    actions: [Action.MESSAGES_DELETE],\n  },\n  {\n    clusters: [clusterName1, clusterName2],\n    resource: ResourceType.TOPIC,\n    value: '.*',\n    actions: [Action.EDIT, Action.DELETE],\n  },\n];\n\nexport const modifiedData = modifyRolesData(userPermissionsMock);\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/__tests__/useBoolean.spec.ts",
    "content": "import { renderHook, act } from '@testing-library/react';\nimport useBoolean from 'lib/hooks/useBoolean';\n\ndescribe('useBoolean CustomHook', () => {\n  it('should check true initial values', () => {\n    let initialValue = true;\n    const { result, rerender } = renderHook(() => useBoolean(initialValue));\n    expect(result.current.value).toBe(initialValue);\n    initialValue = false;\n    rerender();\n    // because state is in useState\n    expect(result.current.value).not.toBe(initialValue);\n  });\n\n  it('should check false initial values', () => {\n    let initialValue = false;\n    const { result, rerender } = renderHook(() => useBoolean(initialValue));\n    expect(result.current.value).toBe(initialValue);\n\n    initialValue = true;\n    rerender();\n    // because state is in useState\n    expect(result.current.value).not.toBe(initialValue);\n  });\n\n  it('should check setTrue function', () => {\n    const { result } = renderHook(() => useBoolean());\n    expect(result.current.value).toBeFalsy();\n    act(() => {\n      result.current.setTrue();\n    });\n    expect(result.current.value).toBeTruthy();\n  });\n\n  it('should check setFalse function', () => {\n    const { result } = renderHook(() => useBoolean());\n\n    expect(result.current.value).toBeFalsy();\n    act(() => {\n      result.current.setTrue();\n    });\n\n    expect(result.current.value).toBeTruthy();\n\n    act(() => {\n      result.current.setFalse();\n    });\n    expect(result.current.value).toBeFalsy();\n  });\n\n  it('should check setToggle function', () => {\n    const { result } = renderHook(() => useBoolean());\n\n    expect(result.current.value).toBeFalsy();\n    act(() => {\n      result.current.toggle();\n    });\n\n    expect(result.current.value).toBeTruthy();\n\n    act(() => {\n      result.current.toggle();\n    });\n    expect(result.current.value).toBeFalsy();\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/__tests__/useCreatePermission.spec.tsx",
    "content": "import React from 'react';\nimport { useParams } from 'react-router-dom';\nimport { renderHook } from '@testing-library/react';\nimport { isPermittedToCreate } from 'lib/permissions';\nimport { Action, ResourceType } from 'generated-sources';\nimport {\n  UserInfoRolesAccessContext,\n  UserInfoType,\n} from 'components/contexts/UserInfoRolesAccessContext';\nimport { useCreatePermission } from 'lib/hooks/useCreatePermisson';\n\nimport { modifiedData, clusterName1, clusterName2 } from './fixtures';\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useParams: jest.fn(),\n}));\n\ndescribe('useCreatePermission', () => {\n  const customRenderer = ({\n    resource,\n    userInfo,\n  }: {\n    resource: ResourceType;\n    userInfo: UserInfoType;\n  }) =>\n    renderHook(() => useCreatePermission(resource), {\n      wrapper: ({ children }) => (\n        // eslint-disable-next-line react/react-in-jsx-scope\n\n        // issue in initialProps of wrapper\n        <UserInfoRolesAccessContext.Provider value={userInfo}>\n          {children}\n        </UserInfoRolesAccessContext.Provider>\n      ),\n    });\n\n  it('should check if the hook renders the same value as the isPermittedToCreate Headless logic method', () => {\n    const permissionConfig = {\n      resource: ResourceType.TOPIC,\n      userInfo: {\n        roles: modifiedData,\n        rbacFlag: true,\n        username: '',\n      },\n    };\n\n    (useParams as jest.Mock).mockImplementation(() => ({\n      clusterName: clusterName1,\n    }));\n\n    const { result } = customRenderer(permissionConfig);\n\n    expect(result.current).toEqual(\n      isPermittedToCreate({\n        ...permissionConfig,\n        roles: modifiedData,\n        clusterName: clusterName1,\n        rbacFlag: true,\n      })\n    );\n  });\n\n  it('should check if the hook renders the same value as the isPermittedToCreate Headless logic method for Schema', () => {\n    const permissionConfig = {\n      resource: ResourceType.SCHEMA,\n      action: Action.CREATE,\n      userInfo: {\n        roles: modifiedData,\n        rbacFlag: false,\n        username: '',\n      },\n    };\n\n    (useParams as jest.Mock).mockImplementation(() => ({\n      clusterName: clusterName1,\n    }));\n\n    const { result } = customRenderer(permissionConfig);\n\n    expect(result.current).toEqual(\n      isPermittedToCreate({\n        ...permissionConfig,\n        roles: modifiedData,\n        clusterName: clusterName1,\n        rbacFlag: false,\n      })\n    );\n  });\n\n  it('should check if the hook renders the same value as the isPermittedToCreate Headless logic method for another Cluster', () => {\n    const permissionConfig = {\n      resource: ResourceType.SCHEMA,\n      action: Action.CREATE,\n      userInfo: {\n        roles: modifiedData,\n        rbacFlag: true,\n        username: '',\n      },\n    };\n\n    (useParams as jest.Mock).mockImplementation(() => ({\n      clusterName: clusterName2,\n    }));\n\n    const { result } = customRenderer(permissionConfig);\n\n    expect(result.current).toEqual(\n      isPermittedToCreate({\n        ...permissionConfig,\n        roles: modifiedData,\n        clusterName: clusterName2,\n        rbacFlag: true,\n      })\n    );\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/__tests__/useDataSaver.spec.tsx",
    "content": "import React, { useEffect } from 'react';\nimport useDataSaver from 'lib/hooks/useDataSaver';\nimport { render } from '@testing-library/react';\nimport { showAlert } from 'lib/errorHandling';\n\njest.mock('lib/errorHandling', () => ({\n  ...jest.requireActual('lib/errorHandling'),\n  showAlert: jest.fn(),\n}));\ndescribe('useDataSaver hook', () => {\n  const content = {\n    title: 'title',\n  };\n\n  describe('Save as file', () => {\n    beforeAll(() => {\n      jest.useFakeTimers();\n      jest.setSystemTime(new Date('Wed Mar 24 2021 03:19:56 GMT-0700'));\n    });\n\n    afterAll(() => jest.useRealTimers());\n\n    it('downloads txt file', () => {\n      global.URL.createObjectURL = jest.fn();\n      const link: HTMLAnchorElement = document.createElement('a');\n      link.click = jest.fn();\n\n      const mockCreate = jest\n        .spyOn(document, 'createElement')\n        .mockImplementation(() => link);\n\n      const HookWrapper: React.FC = () => {\n        const { saveFile } = useDataSaver('message', 'content');\n        useEffect(() => saveFile(), [saveFile]);\n        return null;\n      };\n\n      render(<HookWrapper />);\n      expect(mockCreate).toHaveBeenCalledTimes(2);\n      expect(link.download).toEqual('message');\n      expect(link.click).toHaveBeenCalledTimes(1);\n\n      mockCreate.mockRestore();\n    });\n  });\n  describe('copies the data to the clipboard', () => {\n    Object.assign(navigator, {\n      clipboard: {\n        writeText: jest.fn(),\n      },\n    });\n    jest.spyOn(navigator.clipboard, 'writeText');\n\n    it('data with type Object', () => {\n      const HookWrapper: React.FC = () => {\n        const { copyToClipboard } = useDataSaver('topic', content);\n        useEffect(() => copyToClipboard(), [copyToClipboard]);\n        return null;\n      };\n      render(<HookWrapper />);\n      expect(navigator.clipboard.writeText).toHaveBeenCalledWith(\n        JSON.stringify(content)\n      );\n    });\n\n    it('data with type String', () => {\n      const HookWrapper: React.FC = () => {\n        const { copyToClipboard } = useDataSaver(\n          'topic',\n          '{ title: \"title\", }'\n        );\n        useEffect(() => copyToClipboard(), [copyToClipboard]);\n        return null;\n      };\n      render(<HookWrapper />);\n      expect(navigator.clipboard.writeText).toHaveBeenCalledWith(\n        String('{ title: \"title\", }')\n      );\n    });\n  });\n  describe('navigator clipboard is undefined', () => {\n    it('calls showAlert with the correct parameters when clipboard API is unavailable', () => {\n      Object.assign(navigator, {\n        clipboard: undefined,\n      });\n\n      const HookWrapper: React.FC = () => {\n        const { copyToClipboard } = useDataSaver('topic', content);\n        useEffect(() => {\n          copyToClipboard();\n        }, [copyToClipboard]);\n        return null;\n      };\n\n      render(<HookWrapper />);\n\n      expect(showAlert).toHaveBeenCalledTimes(1);\n      expect(showAlert).toHaveBeenCalledWith('warning', {\n        id: 'topic',\n        title: 'Warning',\n        message:\n          'Copying to clipboard is unavailable due to unsecured (non-HTTPS) connection',\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/__tests__/usePermission.spec.tsx",
    "content": "import React from 'react';\nimport { useParams } from 'react-router-dom';\nimport { renderHook } from '@testing-library/react';\nimport { usePermission } from 'lib/hooks/usePermission';\nimport { isPermitted } from 'lib/permissions';\nimport { Action, ResourceType } from 'generated-sources';\nimport {\n  UserInfoRolesAccessContext,\n  UserInfoType,\n} from 'components/contexts/UserInfoRolesAccessContext';\n\nimport { clusterName1, modifiedData, clusterName2 } from './fixtures';\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useParams: jest.fn(),\n}));\n\ndescribe('usePermission', () => {\n  const customRenderer = ({\n    resource,\n    action,\n    value,\n    userInfo,\n  }: {\n    resource: ResourceType;\n    action: Action;\n    value?: string;\n    userInfo: UserInfoType;\n  }) =>\n    renderHook(() => usePermission(resource, action, value), {\n      wrapper: ({ children }) => (\n        // eslint-disable-next-line react/react-in-jsx-scope\n\n        // issue in initialProps of wrapper\n        <UserInfoRolesAccessContext.Provider value={userInfo}>\n          {children}\n        </UserInfoRolesAccessContext.Provider>\n      ),\n    });\n\n  it('should check if the hook renders the same value as the isPermitted Headless logic method', () => {\n    const permissionConfig = {\n      resource: ResourceType.TOPIC,\n      action: Action.CREATE,\n      userInfo: {\n        roles: modifiedData,\n        rbacFlag: true,\n        username: '',\n      },\n    };\n\n    (useParams as jest.Mock).mockImplementation(() => ({\n      clusterName: clusterName1,\n    }));\n\n    const { result } = customRenderer(permissionConfig);\n\n    expect(result.current).toEqual(\n      isPermitted({\n        ...permissionConfig,\n        roles: modifiedData,\n        clusterName: clusterName1,\n        rbacFlag: true,\n      })\n    );\n  });\n\n  it('should check if the hook renders the same value as the isPermitted Headless logic method for Schema', () => {\n    const permissionConfig = {\n      resource: ResourceType.SCHEMA,\n      action: Action.CREATE,\n      userInfo: {\n        roles: modifiedData,\n        rbacFlag: true,\n        username: '',\n      },\n    };\n\n    (useParams as jest.Mock).mockImplementation(() => ({\n      clusterName: clusterName1,\n    }));\n\n    const { result } = customRenderer(permissionConfig);\n\n    expect(result.current).toEqual(\n      isPermitted({\n        ...permissionConfig,\n        roles: modifiedData,\n        clusterName: clusterName1,\n        rbacFlag: true,\n      })\n    );\n  });\n\n  it('should check if the hook renders the same value as the isPermitted Headless logic method for another Cluster', () => {\n    const permissionConfig = {\n      resource: ResourceType.SCHEMA,\n      action: Action.CREATE,\n      userInfo: {\n        roles: modifiedData,\n        rbacFlag: true,\n        username: '',\n      },\n    };\n\n    (useParams as jest.Mock).mockImplementation(() => ({\n      clusterName: clusterName2,\n    }));\n\n    const { result } = customRenderer(permissionConfig);\n\n    expect(result.current).toEqual(\n      isPermitted({\n        ...permissionConfig,\n        roles: modifiedData,\n        clusterName: clusterName2,\n        rbacFlag: true,\n      })\n    );\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/api/__tests__/brokers.spec.ts",
    "content": "import { expectQueryWorks, renderQueryHook } from 'lib/testHelpers';\nimport * as hooks from 'lib/hooks/api/brokers';\nimport fetchMock from 'fetch-mock';\n\nconst clusterName = 'test-cluster';\nconst brokerId = 1;\nconst brokersPath = `/api/clusters/${clusterName}/brokers`;\nconst brokerPath = `${brokersPath}/${brokerId}`;\n\ndescribe('Brokers hooks', () => {\n  beforeEach(() => fetchMock.restore());\n  describe('useBrokers', () => {\n    it('useBrokers', async () => {\n      const mock = fetchMock.getOnce(brokersPath, []);\n      const { result } = renderQueryHook(() => hooks.useBrokers(clusterName));\n      await expectQueryWorks(mock, result);\n    });\n  });\n  describe('useBrokerMetrics', () => {\n    it('useBrokerMetrics', async () => {\n      const mock = fetchMock.getOnce(`${brokerPath}/metrics`, {});\n      const { result } = renderQueryHook(() =>\n        hooks.useBrokerMetrics(clusterName, brokerId)\n      );\n      await expectQueryWorks(mock, result);\n    });\n  });\n  describe('useBrokerLogDirs', () => {\n    it('useBrokerLogDirs', async () => {\n      const mock = fetchMock.getOnce(`${brokersPath}/logdirs?broker=1`, []);\n      const { result } = renderQueryHook(() =>\n        hooks.useBrokerLogDirs(clusterName, brokerId)\n      );\n      await expectQueryWorks(mock, result);\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/api/__tests__/clusters.spec.ts",
    "content": "import { expectQueryWorks, renderQueryHook } from 'lib/testHelpers';\nimport * as hooks from 'lib/hooks/api/clusters';\nimport fetchMock from 'fetch-mock';\nimport { clustersPayload } from 'lib/fixtures/clusters';\n\nconst clusterName = 'test-cluster';\n\ndescribe('Clusters hooks', () => {\n  beforeEach(() => fetchMock.restore());\n  describe('useClusters', () => {\n    it('returns the correct data', async () => {\n      const mock = fetchMock.getOnce('/api/clusters', clustersPayload);\n      const { result } = renderQueryHook(() => hooks.useClusters());\n      await expectQueryWorks(mock, result);\n    });\n  });\n  describe('useClusterStats', () => {\n    it('returns the correct data', async () => {\n      const mock = fetchMock.getOnce(\n        `/api/clusters/${clusterName}/stats`,\n        clustersPayload\n      );\n      const { result } = renderQueryHook(() =>\n        hooks.useClusterStats(clusterName)\n      );\n      await expectQueryWorks(mock, result);\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/api/__tests__/kafkaConnect.spec.ts",
    "content": "import { act, renderHook, waitFor } from '@testing-library/react';\nimport {\n  expectQueryWorks,\n  renderQueryHook,\n  TestQueryClientProvider,\n} from 'lib/testHelpers';\nimport * as hooks from 'lib/hooks/api/kafkaConnect';\nimport fetchMock from 'fetch-mock';\nimport { connectors, connects, tasks } from 'lib/fixtures/kafkaConnect';\nimport { ConnectorAction } from 'generated-sources';\n\nconst clusterName = 'test-cluster';\nconst connectName = 'test-connect';\nconst connectorName = 'test-connector';\n\nconst connectsPath = `/api/clusters/${clusterName}/connects`;\nconst connectorsPath = `/api/clusters/${clusterName}/connectors`;\nconst connectorPath = `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`;\n\nconst connectorProps = {\n  clusterName,\n  connectName,\n  connectorName,\n};\n\ndescribe('kafkaConnect hooks', () => {\n  beforeEach(() => fetchMock.restore());\n  describe('useConnects', () => {\n    it('returns the correct data', async () => {\n      const mock = fetchMock.getOnce(connectsPath, connects);\n      const { result } = renderQueryHook(() => hooks.useConnects(clusterName));\n      await expectQueryWorks(mock, result);\n    });\n  });\n  describe('useConnectors', () => {\n    it('returns the correct data', async () => {\n      const mock = fetchMock.getOnce(connectorsPath, connectors);\n      const { result } = renderQueryHook(() =>\n        hooks.useConnectors(clusterName)\n      );\n      await expectQueryWorks(mock, result);\n    });\n\n    it('returns the correct data for request with search criteria', async () => {\n      const search = 'test-search';\n      const mock = fetchMock.getOnce(\n        `${connectorsPath}?search=${search}`,\n        connectors\n      );\n      const { result } = renderQueryHook(() =>\n        hooks.useConnectors(clusterName, search)\n      );\n      await expectQueryWorks(mock, result);\n    });\n  });\n  describe('useConnector', () => {\n    it('returns the correct data', async () => {\n      const mock = fetchMock.getOnce(connectorPath, connectors[0]);\n      const { result } = renderQueryHook(() =>\n        hooks.useConnector(connectorProps)\n      );\n      await expectQueryWorks(mock, result);\n    });\n  });\n  describe('useConnectorTasks', () => {\n    it('returns the correct data', async () => {\n      const mock = fetchMock.getOnce(`${connectorPath}/tasks`, tasks);\n      const { result } = renderQueryHook(() =>\n        hooks.useConnectorTasks(connectorProps)\n      );\n      await expectQueryWorks(mock, result);\n    });\n  });\n  describe('useConnectorConfig', () => {\n    it('returns the correct data', async () => {\n      const mock = fetchMock.getOnce(`${connectorPath}/config`, {});\n      const { result } = renderQueryHook(() =>\n        hooks.useConnectorConfig(connectorProps)\n      );\n      await expectQueryWorks(mock, result);\n    });\n  });\n\n  describe('mutatations', () => {\n    describe('useUpdateConnectorState', () => {\n      it('returns the correct data', async () => {\n        const action = ConnectorAction.RESTART;\n        const uri = `${connectorPath}/action/${action}`;\n        const mock = fetchMock.postOnce(uri, connectors[0]);\n        const { result } = renderHook(\n          () => hooks.useUpdateConnectorState(connectorProps),\n          { wrapper: TestQueryClientProvider }\n        );\n        await act(() => result.current.mutateAsync(action));\n        await waitFor(() => expect(result.current.isSuccess).toBeTruthy());\n        expect(mock.calls()).toHaveLength(1);\n      });\n    });\n    describe('useRestartConnectorTask', () => {\n      it('returns the correct data', async () => {\n        const taskId = 123456;\n        const uri = `${connectorPath}/tasks/${taskId}/action/restart`;\n        const mock = fetchMock.postOnce(uri, {});\n        const { result } = renderHook(\n          () => hooks.useRestartConnectorTask(connectorProps),\n          { wrapper: TestQueryClientProvider }\n        );\n        await act(() => result.current.mutateAsync(taskId));\n        await waitFor(() => expect(result.current.isSuccess).toBeTruthy());\n        expect(mock.calls()).toHaveLength(1);\n      });\n    });\n    describe('useUpdateConnectorConfig', () => {\n      it('returns the correct data', async () => {\n        const mock = fetchMock.putOnce(`${connectorPath}/config`, {});\n        const { result } = renderHook(\n          () => hooks.useUpdateConnectorConfig(connectorProps),\n          { wrapper: TestQueryClientProvider }\n        );\n        await act(async () => {\n          await result.current.mutateAsync({ config: 1 });\n        });\n        await waitFor(() => expect(result.current.isSuccess).toBeTruthy());\n        expect(mock.calls()).toHaveLength(1);\n      });\n    });\n    describe('useCreateConnector', () => {\n      it('returns the correct data', async () => {\n        const mock = fetchMock.postOnce(\n          `${connectsPath}/${connectName}/connectors`,\n          {}\n        );\n        const { result } = renderHook(\n          () => hooks.useCreateConnector(clusterName),\n          { wrapper: TestQueryClientProvider }\n        );\n        await act(async () => {\n          await result.current.mutateAsync({\n            connectName,\n            newConnector: { name: connectorName, config: { a: 1 } },\n          });\n        });\n        await waitFor(() => expect(result.current.isSuccess).toBeTruthy());\n        expect(mock.calls()).toHaveLength(1);\n      });\n    });\n    describe('useDeleteConnector', () => {\n      it('returns the correct data', async () => {\n        const mock = fetchMock.deleteOnce(connectorPath, {});\n        const { result } = renderHook(\n          () => hooks.useDeleteConnector(connectorProps),\n          { wrapper: TestQueryClientProvider }\n        );\n        await act(async () => {\n          await result.current.mutateAsync();\n        });\n        await waitFor(() => expect(result.current.isSuccess).toBeTruthy());\n        expect(mock.calls()).toHaveLength(1);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/api/__tests__/latestVersion.spec.ts",
    "content": "import fetchMock from 'fetch-mock';\nimport { expectQueryWorks, renderQueryHook } from 'lib/testHelpers';\nimport { latestVersionPayload } from 'lib/fixtures/latestVersion';\nimport { useLatestVersion } from 'lib/hooks/api/latestVersion';\n\nconst latestVersionPath = '/api/info';\n\ndescribe('Latest version hooks', () => {\n  beforeEach(() => fetchMock.restore());\n  describe('useLatestVersion', () => {\n    it('returns the correct data', async () => {\n      const mock = fetchMock.getOnce(latestVersionPath, latestVersionPayload);\n      const { result } = renderQueryHook(() => useLatestVersion());\n      await expectQueryWorks(mock, result);\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/api/__tests__/topicMessages.spec.ts",
    "content": "import { waitFor } from '@testing-library/react';\nimport { renderQueryHook } from 'lib/testHelpers';\nimport * as hooks from 'lib/hooks/api/topicMessages';\nimport fetchMock from 'fetch-mock';\nimport { UseQueryResult } from '@tanstack/react-query';\nimport { SerdeUsage } from 'generated-sources';\n\nconst clusterName = 'test-cluster';\nconst topicName = 'test-topic';\n\nconst expectQueryWorks = async (\n  mock: fetchMock.FetchMockStatic,\n  result: { current: UseQueryResult<unknown, unknown> }\n) => {\n  await waitFor(() => expect(result.current.isFetched).toBeTruthy());\n  expect(mock.calls()).toHaveLength(1);\n  expect(result.current.data).toBeDefined();\n};\n\njest.mock('lib/errorHandling', () => ({\n  ...jest.requireActual('lib/errorHandling'),\n  showServerError: jest.fn(),\n}));\n\ndescribe('Topic Messages hooks', () => {\n  beforeEach(() => fetchMock.restore());\n  it('handles useSerdes', async () => {\n    const path = `/api/clusters/${clusterName}/topic/${topicName}/serdes?use=SERIALIZE`;\n\n    const mock = fetchMock.getOnce(path, {});\n    const { result } = renderQueryHook(() =>\n      hooks.useSerdes({ clusterName, topicName, use: SerdeUsage.SERIALIZE })\n    );\n    await expectQueryWorks(mock, result);\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/api/__tests__/topics.spec.ts",
    "content": "import { act, renderHook, waitFor } from '@testing-library/react';\nimport {\n  expectQueryWorks,\n  renderQueryHook,\n  TestQueryClientProvider,\n} from 'lib/testHelpers';\nimport * as hooks from 'lib/hooks/api/topics';\nimport fetchMock from 'fetch-mock';\nimport { externalTopicPayload, topicConfigPayload } from 'lib/fixtures/topics';\nimport { TopicFormData, TopicFormDataRaw } from 'redux/interfaces';\nimport { CreateTopicMessage } from 'generated-sources';\n\nconst clusterName = 'test-cluster';\nconst topicName = 'test-topic';\n\nconst topicsPath = `/api/clusters/${clusterName}/topics`;\nconst topicPath = `${topicsPath}/${topicName}`;\n\nconst topicParams = { clusterName, topicName };\n\njest.mock('lib/errorHandling', () => ({\n  ...jest.requireActual('lib/errorHandling'),\n  showServerError: jest.fn(),\n}));\n\ndescribe('Topics hooks', () => {\n  beforeEach(() => fetchMock.restore());\n  it('handles useTopics', async () => {\n    const mock = fetchMock.getOnce(topicsPath, []);\n    const { result } = renderQueryHook(() => hooks.useTopics({ clusterName }));\n    await expectQueryWorks(mock, result);\n  });\n  it('handles useTopicDetails', async () => {\n    const mock = fetchMock.getOnce(topicPath, externalTopicPayload);\n    const { result } = renderQueryHook(() =>\n      hooks.useTopicDetails(topicParams)\n    );\n    await expectQueryWorks(mock, result);\n  });\n  it('handles useTopicConfig', async () => {\n    const mock = fetchMock.getOnce(`${topicPath}/config`, topicConfigPayload);\n    const { result } = renderQueryHook(() => hooks.useTopicConfig(topicParams));\n    await expectQueryWorks(mock, result);\n  });\n  it('handles useTopicConsumerGroups', async () => {\n    const mock = fetchMock.getOnce(`${topicPath}/consumer-groups`, []);\n    const { result } = renderQueryHook(() =>\n      hooks.useTopicConsumerGroups(topicParams)\n    );\n    await expectQueryWorks(mock, result);\n  });\n  describe('useTopicAnalysis', () => {\n    it('handles useTopicAnalysis', async () => {\n      const mock = fetchMock.getOnce(`${topicPath}/analysis`, {});\n      const { result } = renderQueryHook(() =>\n        hooks.useTopicAnalysis(topicParams)\n      );\n      await expectQueryWorks(mock, result);\n    });\n    it('disables useTopicAnalysis', async () => {\n      const mock = fetchMock.getOnce(`${topicPath}/analysis`, {});\n      renderQueryHook(() => hooks.useTopicAnalysis(topicParams, false));\n      expect(mock.calls()).toHaveLength(0);\n    });\n  });\n\n  describe('mutatations', () => {\n    it('useCreateTopic', async () => {\n      const mock = fetchMock.postOnce(topicsPath, {});\n      const { result } = renderHook(() => hooks.useCreateTopic(clusterName), {\n        wrapper: TestQueryClientProvider,\n      });\n      const formData: TopicFormData = {\n        name: 'Topic Name',\n        partitions: 0,\n        replicationFactor: 0,\n        minInSyncReplicas: 0,\n        cleanupPolicy: '',\n        retentionMs: 0,\n        retentionBytes: 0,\n        maxMessageBytes: 0,\n        customParams: [],\n      };\n      await act(() => {\n        result.current.mutateAsync(formData);\n      });\n      await waitFor(() => expect(result.current.isSuccess).toBeTruthy());\n      expect(mock.calls()).toHaveLength(1);\n    });\n\n    it('useUpdateTopic', async () => {\n      const mock = fetchMock.patchOnce(topicPath, {});\n      const { result } = renderHook(() => hooks.useUpdateTopic(topicParams), {\n        wrapper: TestQueryClientProvider,\n      });\n      const formData: TopicFormDataRaw = {\n        name: 'Topic Name',\n        partitions: 0,\n        replicationFactor: 0,\n        minInSyncReplicas: 0,\n        cleanupPolicy: '',\n        retentionMs: 0,\n        retentionBytes: 0,\n        maxMessageBytes: 0,\n        customParams: {\n          byIndex: {},\n          allIndexes: [],\n        },\n      };\n      await act(() => {\n        result.current.mutateAsync(formData);\n      });\n      await waitFor(() => expect(result.current.isSuccess).toBeTruthy());\n      expect(mock.calls()).toHaveLength(1);\n    });\n    it('useIncreaseTopicPartitionsCount', async () => {\n      const mock = fetchMock.patchOnce(`${topicPath}/partitions`, {});\n      const { result } = renderHook(\n        () => hooks.useIncreaseTopicPartitionsCount(topicParams),\n        { wrapper: TestQueryClientProvider }\n      );\n      await act(() => {\n        result.current.mutateAsync(3);\n      });\n      await waitFor(() => expect(result.current.isSuccess).toBeTruthy());\n      expect(mock.calls()).toHaveLength(1);\n    });\n    it('useUpdateTopicReplicationFactor', async () => {\n      const mock = fetchMock.patchOnce(`${topicPath}/replications`, {});\n      const { result } = renderHook(\n        () => hooks.useUpdateTopicReplicationFactor(topicParams),\n        { wrapper: TestQueryClientProvider }\n      );\n      await act(() => {\n        result.current.mutateAsync(3);\n      });\n      await waitFor(() => expect(result.current.isSuccess).toBeTruthy());\n      expect(mock.calls()).toHaveLength(1);\n    });\n    it('useDeleteTopic', async () => {\n      const mock = fetchMock.deleteOnce(topicPath, {});\n      const { result } = renderHook(() => hooks.useDeleteTopic(clusterName), {\n        wrapper: TestQueryClientProvider,\n      });\n      await act(() => {\n        result.current.mutateAsync(topicName);\n      });\n      await waitFor(() => expect(result.current.isSuccess).toBeTruthy());\n      expect(mock.calls()).toHaveLength(1);\n    });\n    it('useRecreateTopic', async () => {\n      const mock = fetchMock.postOnce(topicPath, {});\n      const { result } = renderHook(() => hooks.useRecreateTopic(topicParams), {\n        wrapper: TestQueryClientProvider,\n      });\n      await act(() => {\n        result.current.mutateAsync();\n      });\n      await waitFor(() => expect(result.current.isSuccess).toBeTruthy());\n      expect(mock.calls()).toHaveLength(1);\n    });\n    it('useSendMessage', async () => {\n      const mock = fetchMock.postOnce(`${topicPath}/messages`, {});\n      const { result } = renderHook(() => hooks.useSendMessage(topicParams), {\n        wrapper: TestQueryClientProvider,\n      });\n      const message: CreateTopicMessage = {\n        partition: 0,\n        content: 'Hello World',\n      };\n      await act(() => {\n        result.current.mutateAsync(message);\n      });\n      await waitFor(() => expect(result.current.isSuccess).toBeTruthy());\n      expect(mock.calls()).toHaveLength(1);\n    });\n    it('useAnalyzeTopic', async () => {\n      const mock = fetchMock.postOnce(`${topicPath}/analysis`, {});\n      const { result } = renderHook(() => hooks.useAnalyzeTopic(topicParams), {\n        wrapper: TestQueryClientProvider,\n      });\n      await act(() => {\n        result.current.mutateAsync();\n      });\n      await waitFor(() => expect(result.current.isSuccess).toBeTruthy());\n      expect(mock.calls()).toHaveLength(1);\n    });\n    it('useCancelTopicAnalysis', async () => {\n      const mock = fetchMock.deleteOnce(`${topicPath}/analysis`, {});\n      const { result } = renderHook(\n        () => hooks.useCancelTopicAnalysis(topicParams),\n        {\n          wrapper: TestQueryClientProvider,\n        }\n      );\n      await act(() => {\n        result.current.mutateAsync();\n      });\n      await waitFor(() => expect(result.current.isSuccess).toBeTruthy());\n      expect(mock.calls()).toHaveLength(1);\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/api/acl.ts",
    "content": "import { aclApiClient as api } from 'lib/api';\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { ClusterName } from 'redux/interfaces';\nimport { showSuccessAlert } from 'lib/errorHandling';\nimport { KafkaAcl } from 'generated-sources';\n\nexport function useAcls(clusterName: ClusterName) {\n  return useQuery(\n    ['clusters', clusterName, 'acls'],\n    () => api.listAcls({ clusterName }),\n    {\n      suspense: false,\n    }\n  );\n}\n\nexport function useCreateAclMutation(clusterName: ClusterName) {\n  return useMutation(\n    (data: KafkaAcl) =>\n      api.createAcl({\n        clusterName,\n        kafkaAcl: data,\n      }),\n    {\n      onSuccess() {\n        showSuccessAlert({\n          message: 'Your ACL was created successfully',\n        });\n      },\n    }\n  );\n}\n\nexport function useCreateAcl(clusterName: ClusterName) {\n  const mutate = useCreateAclMutation(clusterName);\n\n  return {\n    createResource: async (param: KafkaAcl) => {\n      return mutate.mutateAsync(param);\n    },\n    ...mutate,\n  };\n}\n\nexport function useDeleteAclMutation(clusterName: ClusterName) {\n  const queryClient = useQueryClient();\n  return useMutation(\n    (acl: KafkaAcl) => api.deleteAcl({ clusterName, kafkaAcl: acl }),\n    {\n      onSuccess: () => {\n        showSuccessAlert({ message: 'ACL deleted' });\n        queryClient.invalidateQueries(['clusters', clusterName, 'acls']);\n      },\n    }\n  );\n}\n\nexport function useDeleteAcl(clusterName: ClusterName) {\n  const mutate = useDeleteAclMutation(clusterName);\n\n  return {\n    deleteResource: async (param: KafkaAcl) => {\n      return mutate.mutateAsync(param);\n    },\n    ...mutate,\n  };\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/api/appConfig.ts",
    "content": "import { appConfigApiClient as api } from 'lib/api';\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { ApplicationConfigPropertiesKafkaClusters } from 'generated-sources';\nimport { QUERY_REFETCH_OFF_OPTIONS } from 'lib/constants';\n\nexport function useAppInfo() {\n  return useQuery(\n    ['app', 'info'],\n    () => api.getApplicationInfo(),\n    QUERY_REFETCH_OFF_OPTIONS\n  );\n}\n\nexport function useAppConfig() {\n  return useQuery(['app', 'config'], () => api.getCurrentConfig());\n}\n\nexport function useUpdateAppConfig({ initialName }: { initialName?: string }) {\n  const client = useQueryClient();\n  return useMutation(\n    async (cluster: ApplicationConfigPropertiesKafkaClusters) => {\n      const existingConfig = await api.getCurrentConfig();\n      const existingClusters = existingConfig.properties?.kafka?.clusters || [];\n\n      let clusters: ApplicationConfigPropertiesKafkaClusters[] = [];\n\n      if (existingClusters.length > 0) {\n        if (!initialName) {\n          clusters = [...existingClusters, cluster];\n        } else {\n          clusters = existingClusters.map((c) =>\n            c.name === initialName ? cluster : c\n          );\n        }\n      } else {\n        clusters = [cluster];\n      }\n\n      const config = {\n        ...existingConfig,\n        properties: {\n          ...existingConfig.properties,\n          kafka: { clusters },\n        },\n      };\n      return api.restartWithConfig({ restartRequest: { config } });\n    },\n    {\n      onSuccess: () => client.invalidateQueries(['app', 'config']),\n    }\n  );\n}\n\nexport function useAppConfigFilesUpload() {\n  return useMutation((payload: FormData) =>\n    fetch('/api/config/relatedfiles', {\n      method: 'POST',\n      body: payload,\n    }).then((res) => res.json())\n  );\n}\n\nexport function useValidateAppConfig() {\n  return useMutation((config: ApplicationConfigPropertiesKafkaClusters) =>\n    api.validateConfig({\n      applicationConfig: { properties: { kafka: { clusters: [config] } } },\n    })\n  );\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/api/brokers.ts",
    "content": "import { brokersApiClient as api } from 'lib/api';\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { ClusterName } from 'redux/interfaces';\nimport { BrokerConfigItem } from 'generated-sources';\n\ninterface UpdateBrokerConfigProps {\n  name: string;\n  brokerConfigItem: BrokerConfigItem;\n}\n\nexport function useBrokers(clusterName: ClusterName) {\n  return useQuery(\n    ['clusters', clusterName, 'brokers'],\n    () => api.getBrokers({ clusterName }),\n    { refetchInterval: 5000 }\n  );\n}\n\nexport function useBrokerMetrics(clusterName: ClusterName, brokerId: number) {\n  return useQuery(\n    ['clusters', clusterName, 'brokers', brokerId, 'metrics'],\n    () =>\n      api.getBrokersMetrics({\n        clusterName,\n        id: brokerId,\n      })\n  );\n}\n\nexport function useBrokerLogDirs(clusterName: ClusterName, brokerId: number) {\n  return useQuery(\n    ['clusters', clusterName, 'brokers', brokerId, 'logDirs'],\n    () =>\n      api.getAllBrokersLogdirs({\n        clusterName,\n        broker: [brokerId],\n      })\n  );\n}\n\nexport function useBrokerConfig(clusterName: ClusterName, brokerId: number) {\n  return useQuery(\n    ['clusters', clusterName, 'brokers', brokerId, 'settings'],\n    () =>\n      api.getBrokerConfig({\n        clusterName,\n        id: brokerId,\n      })\n  );\n}\n\nexport function useUpdateBrokerConfigByName(\n  clusterName: ClusterName,\n  brokerId: number\n) {\n  const client = useQueryClient();\n  return useMutation(\n    (payload: UpdateBrokerConfigProps) =>\n      api.updateBrokerConfigByName({\n        ...payload,\n        clusterName,\n        id: brokerId,\n      }),\n    {\n      onSuccess: () =>\n        client.invalidateQueries([\n          'clusters',\n          clusterName,\n          'brokers',\n          brokerId,\n          'settings',\n        ]),\n    }\n  );\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/api/clusters.ts",
    "content": "import { clustersApiClient as api } from 'lib/api';\nimport { useQuery } from '@tanstack/react-query';\nimport { ClusterName } from 'redux/interfaces';\n\nexport function useClusters() {\n  return useQuery(['clusters'], () => api.getClusters(), { suspense: false });\n}\nexport function useClusterStats(clusterName: ClusterName) {\n  return useQuery(\n    ['clusterStats', clusterName],\n    () => api.getClusterStats({ clusterName }),\n    { refetchInterval: 5000 }\n  );\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/api/consumers.ts",
    "content": "import { consumerGroupsApiClient as api } from 'lib/api';\nimport { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';\nimport { ClusterName } from 'redux/interfaces';\nimport {\n  ConsumerGroup,\n  ConsumerGroupOffsetsReset,\n  ConsumerGroupOrdering,\n  SortOrder,\n} from 'generated-sources';\nimport { showSuccessAlert } from 'lib/errorHandling';\n\nexport type ConsumerGroupID = ConsumerGroup['groupId'];\n\ntype UseConsumerGroupsProps = {\n  clusterName: ClusterName;\n  orderBy?: ConsumerGroupOrdering;\n  sortOrder?: SortOrder;\n  page?: number;\n  perPage?: number;\n  search: string;\n};\n\ntype UseConsumerGroupDetailsProps = {\n  clusterName: ClusterName;\n  consumerGroupID: ConsumerGroupID;\n};\n\nexport function useConsumerGroups(props: UseConsumerGroupsProps) {\n  const { clusterName, ...rest } = props;\n  return useQuery(\n    ['clusters', clusterName, 'consumerGroups', rest],\n    () => api.getConsumerGroupsPage(props),\n    { suspense: false, keepPreviousData: true }\n  );\n}\n\nexport function useConsumerGroupDetails(props: UseConsumerGroupDetailsProps) {\n  const { clusterName, consumerGroupID } = props;\n  return useQuery(\n    ['clusters', clusterName, 'consumerGroups', consumerGroupID],\n    () => api.getConsumerGroup({ clusterName, id: consumerGroupID })\n  );\n}\n\nexport const useDeleteConsumerGroupMutation = ({\n  clusterName,\n  consumerGroupID,\n}: UseConsumerGroupDetailsProps) => {\n  const queryClient = useQueryClient();\n  return useMutation(\n    () => api.deleteConsumerGroup({ clusterName, id: consumerGroupID }),\n    {\n      onSuccess: () => {\n        showSuccessAlert({\n          message: `Consumer ${consumerGroupID} group deleted`,\n        });\n        queryClient.invalidateQueries([\n          'clusters',\n          clusterName,\n          'consumerGroups',\n        ]);\n      },\n    }\n  );\n};\n\nexport const useResetConsumerGroupOffsetsMutation = ({\n  clusterName,\n  consumerGroupID,\n}: UseConsumerGroupDetailsProps) => {\n  const queryClient = useQueryClient();\n  return useMutation(\n    (props: ConsumerGroupOffsetsReset) =>\n      api.resetConsumerGroupOffsets({\n        clusterName,\n        id: consumerGroupID,\n        consumerGroupOffsetsReset: props,\n      }),\n    {\n      onSuccess: () => {\n        showSuccessAlert({\n          message: `Consumer ${consumerGroupID} group offsets reset`,\n        });\n        queryClient.invalidateQueries([\n          'clusters',\n          clusterName,\n          'consumerGroups',\n        ]);\n      },\n    }\n  );\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/api/kafkaConnect.ts",
    "content": "import {\n  Connect,\n  Connector,\n  ConnectorAction,\n  NewConnector,\n} from 'generated-sources';\nimport { kafkaConnectApiClient as api } from 'lib/api';\nimport sortBy from 'lodash/sortBy';\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { ClusterName } from 'redux/interfaces';\nimport { showSuccessAlert } from 'lib/errorHandling';\n\ninterface UseConnectorProps {\n  clusterName: ClusterName;\n  connectName: Connect['name'];\n  connectorName: Connector['name'];\n}\ninterface CreateConnectorProps {\n  connectName: Connect['name'];\n  newConnector: NewConnector;\n}\n\nconst connectsKey = (clusterName: ClusterName) => [\n  'clusters',\n  clusterName,\n  'connects',\n];\nconst connectorsKey = (clusterName: ClusterName, search?: string) => {\n  const base = ['clusters', clusterName, 'connectors'];\n  if (search) {\n    return [...base, { search }];\n  }\n  return base;\n};\nconst connectorKey = (props: UseConnectorProps) => [\n  'clusters',\n  props.clusterName,\n  'connects',\n  props.connectName,\n  'connectors',\n  props.connectorName,\n];\nconst connectorTasksKey = (props: UseConnectorProps) => [\n  ...connectorKey(props),\n  'tasks',\n];\n\nexport function useConnects(clusterName: ClusterName) {\n  return useQuery(connectsKey(clusterName), () =>\n    api.getConnects({ clusterName })\n  );\n}\nexport function useConnectors(clusterName: ClusterName, search?: string) {\n  return useQuery(\n    connectorsKey(clusterName, search),\n    () => api.getAllConnectors({ clusterName, search }),\n    {\n      select: (data) => sortBy(data, 'name'),\n    }\n  );\n}\nexport function useConnector(props: UseConnectorProps) {\n  return useQuery(connectorKey(props), () => api.getConnector(props));\n}\nexport function useConnectorTasks(props: UseConnectorProps) {\n  return useQuery(\n    connectorTasksKey(props),\n    () => api.getConnectorTasks(props),\n    {\n      select: (data) => sortBy(data, 'status.id'),\n    }\n  );\n}\nexport function useUpdateConnectorState(props: UseConnectorProps) {\n  const client = useQueryClient();\n  return useMutation(\n    (action: ConnectorAction) => api.updateConnectorState({ ...props, action }),\n    {\n      onSuccess: () =>\n        client.invalidateQueries(['clusters', props.clusterName, 'connectors']),\n    }\n  );\n}\nexport function useRestartConnectorTask(props: UseConnectorProps) {\n  const client = useQueryClient();\n  return useMutation(\n    (taskId: number) => api.restartConnectorTask({ ...props, taskId }),\n    {\n      onSuccess: () => client.invalidateQueries(connectorTasksKey(props)),\n    }\n  );\n}\nexport function useConnectorConfig(props: UseConnectorProps) {\n  return useQuery([...connectorKey(props), 'config'], () =>\n    api.getConnectorConfig(props)\n  );\n}\nexport function useUpdateConnectorConfig(props: UseConnectorProps) {\n  const client = useQueryClient();\n  return useMutation(\n    (requestBody: Connector['config']) =>\n      api.setConnectorConfig({ ...props, requestBody }),\n    {\n      onSuccess: () => {\n        showSuccessAlert({\n          message: `Config successfully updated.`,\n        });\n        client.invalidateQueries(connectorKey(props));\n      },\n    }\n  );\n}\nfunction useCreateConnectorMutation(clusterName: ClusterName) {\n  const client = useQueryClient();\n  return useMutation(\n    (props: CreateConnectorProps) =>\n      api.createConnector({ ...props, clusterName }),\n    {\n      onSuccess: () => client.invalidateQueries(connectorsKey(clusterName)),\n    }\n  );\n}\n\n// this will change later when we validate the request before\nexport function useCreateConnector(clusterName: ClusterName) {\n  const mutate = useCreateConnectorMutation(clusterName);\n\n  return {\n    createResource: async (param: CreateConnectorProps) => {\n      return mutate.mutateAsync(param);\n    },\n    ...mutate,\n  };\n}\n\nexport function useDeleteConnector(props: UseConnectorProps) {\n  const client = useQueryClient();\n\n  return useMutation(() => api.deleteConnector(props), {\n    onSuccess: () => client.invalidateQueries(connectorsKey(props.clusterName)),\n  });\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/api/ksqlDb.tsx",
    "content": "import { ksqlDbApiClient as api } from 'lib/api';\nimport { useMutation, useQueries } from '@tanstack/react-query';\nimport { ClusterName } from 'redux/interfaces';\nimport { BASE_PARAMS } from 'lib/constants';\nimport React from 'react';\nimport { fetchEventSource } from '@microsoft/fetch-event-source';\nimport {\n  showAlert,\n  showServerError,\n  showSuccessAlert,\n} from 'lib/errorHandling';\nimport {\n  ExecuteKsqlRequest,\n  KsqlResponse,\n  KsqlTableResponse,\n} from 'generated-sources';\nimport { StopLoading } from 'components/Topics/Topic/Messages/Messages.styled';\nimport toast from 'react-hot-toast';\n\nexport function useKsqlkDb(clusterName: ClusterName) {\n  return useQueries({\n    queries: [\n      {\n        queryKey: ['clusters', clusterName, 'ksqlDb', 'tables'],\n        queryFn: () => api.listTables({ clusterName }),\n        suspense: false,\n      },\n      {\n        queryKey: ['clusters', clusterName, 'ksqlDb', 'streams'],\n        queryFn: () => api.listStreams({ clusterName }),\n        suspense: false,\n      },\n    ],\n  });\n}\n\nexport function useExecuteKsqlkDbQueryMutation() {\n  return useMutation((props: ExecuteKsqlRequest) => api.executeKsql(props));\n}\n\nconst getFormattedErrorFromTableData = (\n  responseValues: KsqlTableResponse['values']\n): { title: string; message: string } => {\n  // We expect someting like that\n  // [[\n  //   \"@type\",\n  //   \"error_code\",\n  //   \"message\",\n  //   \"statementText\"?,\n  //   \"entities\"?\n  // ]],\n  // or\n  // [[\"message\"]]\n\n  if (!responseValues || !responseValues.length) {\n    return {\n      title: 'Unknown error',\n      message: 'Recieved empty response',\n    };\n  }\n\n  let title = '';\n  let message = '';\n  if (responseValues[0].length < 2) {\n    const [messageText] = responseValues[0];\n    title = messageText;\n  } else {\n    const [type, errorCode, messageText, statementText, entities] =\n      responseValues[0];\n    title = `[Error #${errorCode}] ${type}`;\n    message =\n      (entities?.length ? `[${entities.join(', ')}] ` : '') +\n      (statementText ? `\"${statementText}\" ` : '') +\n      messageText;\n  }\n\n  return { title, message };\n};\n\ntype UseKsqlkDbSSEProps = {\n  pipeId: string | false;\n  clusterName: ClusterName;\n};\n\nexport const useKsqlkDbSSE = ({ clusterName, pipeId }: UseKsqlkDbSSEProps) => {\n  const [data, setData] = React.useState<KsqlTableResponse>();\n  const [isFetching, setIsFetching] = React.useState<boolean>(false);\n\n  const abortController = new AbortController();\n\n  React.useEffect(() => {\n    const fetchData = async () => {\n      const url = `${BASE_PARAMS.basePath}/api/clusters/${encodeURIComponent(\n        clusterName\n      )}/ksql/response`;\n      await fetchEventSource(\n        `${url}?${new URLSearchParams({ pipeId: pipeId || '' }).toString()}`,\n        {\n          method: 'GET',\n          signal: abortController.signal,\n          openWhenHidden: true,\n          async onopen(response) {\n            const { ok, status } = response;\n            if (ok) setData(undefined); // Reset\n            if (status >= 400 && status < 500 && status !== 429) {\n              showServerError(response);\n            }\n          },\n          onmessage(event) {\n            const { table }: KsqlResponse = JSON.parse(event.data);\n            if (!table) {\n              return;\n            }\n            switch (table?.header) {\n              case 'Execution error': {\n                showAlert('error', {\n                  ...getFormattedErrorFromTableData(table.values),\n                  id: `${url}-executionError`,\n                });\n                break;\n              }\n              case 'Schema':\n                setData(table);\n                break;\n              case 'Row':\n                setData((state) => ({\n                  header: state?.header,\n                  columnNames: state?.columnNames,\n                  values: [...(state?.values || []), ...(table?.values || [])],\n                }));\n                break;\n              case 'Query Result':\n                showSuccessAlert({\n                  id: `${url}-querySuccess`,\n                  title: 'Query succeed',\n                  message: '',\n                });\n                break;\n              case 'Source Description':\n              case 'properties':\n              default:\n                setData(table);\n                break;\n            }\n          },\n          onclose() {\n            setIsFetching(false);\n          },\n          onerror(err) {\n            setIsFetching(false);\n            showServerError(err);\n          },\n        }\n      );\n    };\n\n    const abortFetchData = () => {\n      setIsFetching(false);\n      if (pipeId) abortController.abort();\n    };\n    if (pipeId) {\n      toast.promise(\n        fetchData(),\n        {\n          loading: (\n            <>\n              <div>Consuming query execution result...</div>\n              &nbsp;\n              <StopLoading onClick={abortFetchData}>Abort</StopLoading>\n            </>\n          ),\n          success: 'Cancelled',\n          error: 'Something went wrong. Please try again.',\n        },\n        {\n          id: 'messages',\n          success: { duration: 20 },\n        }\n      );\n    }\n\n    return abortFetchData;\n  }, [pipeId]);\n\n  return { data, isFetching };\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/api/latestVersion.ts",
    "content": "import { useQuery } from '@tanstack/react-query';\nimport { BASE_PARAMS, QUERY_REFETCH_OFF_OPTIONS } from 'lib/constants';\n\nconst fetchLatestVersionInfo = async () => {\n  const data = await fetch(\n    `${BASE_PARAMS.basePath}/api/info`,\n    BASE_PARAMS\n  ).then((res) => res.json());\n\n  return data;\n};\n\nexport function useLatestVersion() {\n  return useQuery(\n    ['versionInfo'],\n    fetchLatestVersionInfo,\n    QUERY_REFETCH_OFF_OPTIONS\n  );\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/api/roles.ts",
    "content": "import { useQuery } from '@tanstack/react-query';\nimport { authApiClient } from 'lib/api';\nimport { QUERY_REFETCH_OFF_OPTIONS } from 'lib/constants';\n\nexport function useGetUserInfo() {\n  return useQuery(\n    ['userInfo'],\n    () => authApiClient.getUserAuthInfo(),\n    QUERY_REFETCH_OFF_OPTIONS\n  );\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/api/topicMessages.tsx",
    "content": "import React from 'react';\nimport { fetchEventSource } from '@microsoft/fetch-event-source';\nimport { BASE_PARAMS, MESSAGES_PER_PAGE } from 'lib/constants';\nimport { ClusterName } from 'redux/interfaces';\nimport {\n  GetSerdesRequest,\n  SeekDirection,\n  SeekType,\n  TopicMessage,\n  TopicMessageConsuming,\n  TopicMessageEvent,\n  TopicMessageEventTypeEnum,\n} from 'generated-sources';\nimport { showServerError } from 'lib/errorHandling';\nimport toast from 'react-hot-toast';\nimport { useQuery } from '@tanstack/react-query';\nimport { messagesApiClient } from 'lib/api';\nimport { StopLoading } from 'components/Topics/Topic/Messages/Messages.styled';\n\ninterface UseTopicMessagesProps {\n  clusterName: ClusterName;\n  topicName: string;\n  searchParams: URLSearchParams;\n}\n\ntype ConsumingMode =\n  | 'live'\n  | 'oldest'\n  | 'newest'\n  | 'fromOffset' // from 900 -> 1000\n  | 'toOffset' // from 900 -> 800\n  | 'sinceTime' // from 10:15 -> 11:15\n  | 'untilTime'; // from 10:15 -> 9:15\n\nexport const useTopicMessages = ({\n  clusterName,\n  topicName,\n  searchParams,\n}: UseTopicMessagesProps) => {\n  const [messages, setMessages] = React.useState<TopicMessage[]>([]);\n  const [phase, setPhase] = React.useState<string>();\n  const [meta, setMeta] = React.useState<TopicMessageConsuming>();\n  const [isFetching, setIsFetching] = React.useState<boolean>(false);\n  const abortController = new AbortController();\n\n  // get initial properties\n  const mode = searchParams.get('m') as ConsumingMode;\n  const limit = searchParams.get('perPage') || MESSAGES_PER_PAGE;\n  const seekTo = searchParams.get('seekTo') || '0-0';\n\n  React.useEffect(() => {\n    const fetchData = async () => {\n      setIsFetching(true);\n      const url = `${BASE_PARAMS.basePath}/api/clusters/${encodeURIComponent(\n        clusterName\n      )}/topics/${topicName}/messages`;\n      const requestParams = new URLSearchParams({\n        limit,\n        seekTo: seekTo.replaceAll('-', '::').replaceAll('.', ','),\n        q: searchParams.get('q') || '',\n        keySerde: searchParams.get('keySerde') || '',\n        valueSerde: searchParams.get('valueSerde') || '',\n      });\n\n      switch (mode) {\n        case 'live':\n          requestParams.set('seekDirection', SeekDirection.TAILING);\n          requestParams.set('seekType', SeekType.LATEST);\n          break;\n        case 'oldest':\n          requestParams.set('seekType', SeekType.BEGINNING);\n          requestParams.set('seekDirection', SeekDirection.FORWARD);\n          break;\n        case 'newest':\n          requestParams.set('seekType', SeekType.LATEST);\n          requestParams.set('seekDirection', SeekDirection.BACKWARD);\n          break;\n        case 'fromOffset':\n          requestParams.set('seekType', SeekType.OFFSET);\n          requestParams.set('seekDirection', SeekDirection.FORWARD);\n          break;\n        case 'toOffset':\n          requestParams.set('seekType', SeekType.OFFSET);\n          requestParams.set('seekDirection', SeekDirection.BACKWARD);\n          break;\n        case 'sinceTime':\n          requestParams.set('seekType', SeekType.TIMESTAMP);\n          requestParams.set('seekDirection', SeekDirection.FORWARD);\n          break;\n        case 'untilTime':\n          requestParams.set('seekType', SeekType.TIMESTAMP);\n          requestParams.set('seekDirection', SeekDirection.BACKWARD);\n          break;\n        default:\n          break;\n      }\n\n      await fetchEventSource(`${url}?${requestParams.toString()}`, {\n        method: 'GET',\n        signal: abortController.signal,\n        openWhenHidden: true,\n        async onopen(response) {\n          const { ok, status } = response;\n          if (ok && status === 200) {\n            // Reset list of messages.\n            setMessages([]);\n          } else if (status >= 400 && status < 500 && status !== 429) {\n            showServerError(response);\n          }\n        },\n        onmessage(event) {\n          const parsedData: TopicMessageEvent = JSON.parse(event.data);\n          const { message, consuming } = parsedData;\n\n          switch (parsedData.type) {\n            case TopicMessageEventTypeEnum.MESSAGE:\n              if (message) {\n                setMessages((prevMessages) => {\n                  if (mode === 'live') {\n                    return [message, ...prevMessages];\n                  }\n                  return [...prevMessages, message];\n                });\n              }\n              break;\n            case TopicMessageEventTypeEnum.PHASE:\n              if (parsedData.phase?.name) setPhase(parsedData.phase.name);\n              break;\n            case TopicMessageEventTypeEnum.CONSUMING:\n              if (consuming) setMeta(consuming);\n              break;\n            default:\n          }\n        },\n        onclose() {\n          setIsFetching(false);\n        },\n        onerror(err) {\n          setIsFetching(false);\n          showServerError(err);\n        },\n      });\n    };\n    const abortFetchData = () => {\n      setIsFetching(false);\n      abortController.abort();\n    };\n\n    if (mode === 'live') {\n      toast.promise(\n        fetchData(),\n        {\n          loading: (\n            <>\n              <div>Consuming messages...</div>\n              &nbsp;\n              <StopLoading onClick={abortFetchData}>Abort</StopLoading>\n            </>\n          ),\n          success: 'Cancelled',\n          error: 'Something went wrong. Please try again.',\n        },\n        {\n          id: 'messages',\n          position: 'top-center',\n          // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n          // @ts-ignore - missing type for icon\n          success: { duration: 10, icon: false },\n        }\n      );\n    } else {\n      fetchData();\n    }\n\n    return abortFetchData;\n  }, [searchParams]);\n\n  return {\n    phase,\n    messages,\n    meta,\n    isFetching,\n  };\n};\n\nexport function useSerdes(props: GetSerdesRequest) {\n  const { clusterName, topicName, use } = props;\n\n  return useQuery(\n    ['clusters', clusterName, 'topics', topicName, 'serdes', use],\n    () => messagesApiClient.getSerdes(props),\n    {\n      refetchOnWindowFocus: false,\n      refetchOnReconnect: false,\n      refetchInterval: false,\n    }\n  );\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/api/topics.ts",
    "content": "import {\n  topicsApiClient as api,\n  messagesApiClient as messagesApi,\n  consumerGroupsApiClient,\n  messagesApiClient,\n} from 'lib/api';\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport {\n  ClusterName,\n  TopicFormData,\n  TopicFormDataRaw,\n  TopicFormFormattedParams,\n} from 'redux/interfaces';\nimport {\n  CreateTopicMessage,\n  GetTopicDetailsRequest,\n  GetTopicsRequest,\n  Topic,\n  TopicConfig,\n  TopicCreation,\n  TopicUpdate,\n} from 'generated-sources';\nimport { showServerError, showSuccessAlert } from 'lib/errorHandling';\n\nexport const topicKeys = {\n  all: (clusterName: ClusterName) =>\n    ['clusters', clusterName, 'topics'] as const,\n  list: (\n    clusterName: ClusterName,\n    filters: Omit<GetTopicsRequest, 'clusterName'>\n  ) => [...topicKeys.all(clusterName), filters] as const,\n  details: ({ clusterName, topicName }: GetTopicDetailsRequest) =>\n    [...topicKeys.all(clusterName), topicName] as const,\n  config: (props: GetTopicDetailsRequest) =>\n    [...topicKeys.details(props), 'config'] as const,\n  schema: (props: GetTopicDetailsRequest) =>\n    [...topicKeys.details(props), 'schema'] as const,\n  consumerGroups: (props: GetTopicDetailsRequest) =>\n    [...topicKeys.details(props), 'consumerGroups'] as const,\n  statistics: (props: GetTopicDetailsRequest) =>\n    [...topicKeys.details(props), 'statistics'] as const,\n};\n\nexport function useTopics(props: GetTopicsRequest) {\n  const { clusterName, ...filters } = props;\n  return useQuery(\n    topicKeys.list(clusterName, filters),\n    () => api.getTopics(props),\n    { keepPreviousData: true }\n  );\n}\nexport function useTopicDetails(props: GetTopicDetailsRequest) {\n  return useQuery(topicKeys.details(props), () => api.getTopicDetails(props));\n}\nexport function useTopicConfig(props: GetTopicDetailsRequest) {\n  return useQuery(topicKeys.config(props), () => api.getTopicConfigs(props));\n}\nexport function useTopicConsumerGroups(props: GetTopicDetailsRequest) {\n  return useQuery(topicKeys.consumerGroups(props), () =>\n    consumerGroupsApiClient.getTopicConsumerGroups(props)\n  );\n}\n\nconst topicReducer = (\n  result: TopicFormFormattedParams,\n  customParam: TopicConfig\n) => {\n  return {\n    ...result,\n    [customParam.name]: customParam.value,\n  };\n};\nconst formatTopicCreation = (form: TopicFormData): TopicCreation => {\n  const {\n    name,\n    partitions,\n    replicationFactor,\n    cleanupPolicy,\n    retentionMs,\n    maxMessageBytes,\n    minInSyncReplicas,\n    customParams,\n  } = form;\n\n  const configs = {\n    'cleanup.policy': cleanupPolicy,\n    'retention.ms': retentionMs.toString(),\n    'max.message.bytes': maxMessageBytes.toString(),\n    'min.insync.replicas': minInSyncReplicas.toString(),\n    ...Object.values(customParams || {}).reduce(topicReducer, {}),\n  };\n\n  const cleanConfigs = () => {\n    return Object.fromEntries(\n      Object.entries(configs).filter(([, val]) => val !== '')\n    );\n  };\n\n  const topicsvalue = {\n    name,\n    partitions,\n    configs: cleanConfigs(),\n  };\n\n  return replicationFactor.toString() !== ''\n    ? {\n        ...topicsvalue,\n        replicationFactor,\n      }\n    : topicsvalue;\n};\n\nexport function useCreateTopicMutation(clusterName: ClusterName) {\n  const client = useQueryClient();\n  return useMutation(\n    (data: TopicFormData) =>\n      api.createTopic({\n        clusterName,\n        topicCreation: formatTopicCreation(data),\n      }),\n    {\n      onSuccess: () => {\n        client.invalidateQueries(topicKeys.all(clusterName));\n      },\n    }\n  );\n}\n\n// this will change later when we validate the request before\nexport function useCreateTopic(clusterName: ClusterName) {\n  const mutate = useCreateTopicMutation(clusterName);\n\n  return {\n    createResource: async (param: TopicFormData) => {\n      return mutate.mutateAsync(param);\n    },\n    ...mutate,\n  };\n}\n\nconst formatTopicUpdate = (form: TopicFormDataRaw): TopicUpdate => {\n  const {\n    cleanupPolicy,\n    retentionBytes,\n    retentionMs,\n    maxMessageBytes,\n    minInSyncReplicas,\n    customParams,\n  } = form;\n\n  return {\n    configs: {\n      ...Object.values(customParams || {}).reduce(topicReducer, {}),\n      'cleanup.policy': cleanupPolicy,\n      'retention.ms': retentionMs,\n      'retention.bytes': retentionBytes,\n      'max.message.bytes': maxMessageBytes,\n      'min.insync.replicas': minInSyncReplicas,\n    },\n  };\n};\n\nexport function useUpdateTopic(props: GetTopicDetailsRequest) {\n  const client = useQueryClient();\n  return useMutation(\n    (data: TopicFormDataRaw) => {\n      return api.updateTopic({\n        ...props,\n        topicUpdate: formatTopicUpdate(data),\n      });\n    },\n    {\n      onSuccess: () => {\n        showSuccessAlert({\n          message: `Topic successfully updated.`,\n        });\n        client.invalidateQueries(topicKeys.all(props.clusterName));\n      },\n    }\n  );\n}\nexport function useIncreaseTopicPartitionsCount(props: GetTopicDetailsRequest) {\n  const client = useQueryClient();\n  return useMutation(\n    (totalPartitionsCount: number) =>\n      api.increaseTopicPartitions({\n        ...props,\n        partitionsIncrease: { totalPartitionsCount },\n      }),\n    {\n      onSuccess: () => {\n        showSuccessAlert({\n          message: `Number of partitions successfully increased`,\n        });\n        client.invalidateQueries(topicKeys.all(props.clusterName));\n      },\n    }\n  );\n}\nexport function useUpdateTopicReplicationFactor(props: GetTopicDetailsRequest) {\n  const client = useQueryClient();\n  return useMutation(\n    (totalReplicationFactor: number) =>\n      api.changeReplicationFactor({\n        ...props,\n        replicationFactorChange: { totalReplicationFactor },\n      }),\n    {\n      onSuccess: () => {\n        showSuccessAlert({\n          message: `Replication factor successfully updated`,\n        });\n        client.invalidateQueries(topicKeys.all(props.clusterName));\n      },\n    }\n  );\n}\nexport function useDeleteTopic(clusterName: ClusterName) {\n  const client = useQueryClient();\n  return useMutation(\n    (topicName: Topic['name']) => api.deleteTopic({ clusterName, topicName }),\n    {\n      onSuccess: (_, topicName) => {\n        showSuccessAlert({\n          message: `Topic ${topicName} successfully deleted!`,\n        });\n        client.invalidateQueries(topicKeys.all(clusterName));\n      },\n    }\n  );\n}\n\nexport function useClearTopicMessages(\n  clusterName: ClusterName,\n  partitions?: number[]\n) {\n  const client = useQueryClient();\n  return useMutation(\n    async (topicName: Topic['name']) => {\n      await messagesApiClient.deleteTopicMessages({\n        clusterName,\n        partitions,\n        topicName,\n      });\n      return topicName;\n    },\n\n    {\n      onSuccess: (topicName) => {\n        showSuccessAlert({\n          id: `message-${topicName}-${clusterName}-${partitions}`,\n          message: `${topicName} messages have been successfully cleared!`,\n        });\n        client.invalidateQueries(topicKeys.all(clusterName));\n      },\n    }\n  );\n}\n\nexport function useRecreateTopic(props: GetTopicDetailsRequest) {\n  const client = useQueryClient();\n  return useMutation(() => api.recreateTopic(props), {\n    onSuccess: () => {\n      showSuccessAlert({\n        message: `Topic ${props.topicName} successfully recreated!`,\n      });\n      client.invalidateQueries(topicKeys.all(props.clusterName));\n    },\n  });\n}\n\nexport function useSendMessage(props: GetTopicDetailsRequest) {\n  const client = useQueryClient();\n  return useMutation(\n    (message: CreateTopicMessage) =>\n      messagesApi.sendTopicMessages({ ...props, createTopicMessage: message }),\n    {\n      onSuccess: () => {\n        showSuccessAlert({\n          message: `Message successfully sent`,\n        });\n        client.invalidateQueries(topicKeys.all(props.clusterName));\n      },\n      onError: (e) => {\n        showServerError(e as Response);\n      },\n    }\n  );\n}\n\n// Statistics\nexport function useTopicAnalysis(\n  props: GetTopicDetailsRequest,\n  enabled = true\n) {\n  return useQuery(\n    topicKeys.statistics(props),\n    () => api.getTopicAnalysis(props),\n    {\n      enabled,\n      refetchInterval: 1000,\n      useErrorBoundary: true,\n      retry: false,\n      suspense: false,\n      onError: (error: Response) => {\n        if (error.status !== 404) {\n          showServerError(error as Response);\n        }\n      },\n    }\n  );\n}\nexport function useAnalyzeTopic(props: GetTopicDetailsRequest) {\n  const client = useQueryClient();\n  return useMutation(() => api.analyzeTopic(props), {\n    onSuccess: () => {\n      client.invalidateQueries(topicKeys.statistics(props));\n    },\n  });\n}\nexport function useCancelTopicAnalysis(props: GetTopicDetailsRequest) {\n  const client = useQueryClient();\n  return useMutation(() => api.cancelTopicAnalysis(props), {\n    onSuccess: () => {\n      showSuccessAlert({\n        message: `Topic analysis canceled`,\n      });\n      client.invalidateQueries(topicKeys.statistics(props));\n    },\n  });\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/redux.ts",
    "content": "import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';\nimport { AppDispatch, RootState } from 'redux/interfaces';\n\nexport const useAppDispatch = () => useDispatch<AppDispatch>();\nexport const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/useActionTooltip.ts",
    "content": "import { useState } from 'react';\nimport {\n  autoPlacement,\n  offset,\n  Placement,\n  useFloating,\n  useHover,\n  useInteractions,\n} from '@floating-ui/react';\n\nexport function useActionTooltip(isDisabled?: boolean, placement?: Placement) {\n  const [open, setOpen] = useState(false);\n\n  const setTooltipOpen = (state: boolean) => {\n    if (!isDisabled) return;\n    setOpen(state);\n  };\n\n  const { x, y, reference, floating, strategy, context } = useFloating({\n    open,\n    onOpenChange: setTooltipOpen,\n    placement,\n    middleware: [offset(10), autoPlacement()],\n  });\n\n  useInteractions([useHover(context)]);\n\n  return {\n    x,\n    y,\n    reference,\n    floating,\n    strategy,\n    open,\n  };\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/useAppParams.tsx",
    "content": "import { Params, useParams } from 'react-router-dom';\n\nexport default function useAppParams<\n  T extends { [K in keyof Params]?: string }\n>() {\n  return useParams<T>() as T;\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/useBoolean.ts",
    "content": "import React, { useCallback, useState } from 'react';\n\ninterface ReturnType {\n  value: boolean;\n  setTrue: () => void;\n  setFalse: () => void;\n  toggle: () => void;\n  setValue: React.Dispatch<React.SetStateAction<boolean>>;\n}\n\nfunction useBoolean(defaultValue?: boolean): ReturnType {\n  const [value, setValue] = useState(!!defaultValue);\n\n  const setTrue = useCallback(() => setValue(true), []);\n  const setFalse = useCallback(() => setValue(false), []);\n  const toggle = useCallback(() => setValue((x) => !x), []);\n\n  return { value, setValue, setTrue, setFalse, toggle };\n}\n\nexport default useBoolean;\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/useClickOutside.ts",
    "content": "import { RefObject, useEffect } from 'react';\n\ntype Event = MouseEvent | TouchEvent;\n\nconst useClickOutside = <T extends HTMLElement = HTMLElement>(\n  ref: RefObject<T>,\n  handler: (event: Event) => void\n) => {\n  useEffect(() => {\n    const listener = (event: Event) => {\n      const el = ref?.current;\n      if (!el || el.contains((event?.target as Node) || null)) {\n        return;\n      }\n\n      handler(event);\n    };\n\n    document.addEventListener('mousedown', listener);\n    document.addEventListener('touchstart', listener);\n\n    return () => {\n      document.removeEventListener('mousedown', listener);\n      document.removeEventListener('touchstart', listener);\n    };\n  }, [ref, handler]);\n};\n\nexport default useClickOutside;\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/useConfirm.ts",
    "content": "import { ConfirmContext } from 'components/contexts/ConfirmContext';\nimport React, { useContext } from 'react';\n\nexport const useConfirm = (danger = false) => {\n  const context = useContext(ConfirmContext);\n  return (\n    message: React.ReactNode,\n    callback: () => void | Promise<unknown>\n  ) => {\n    context?.setDangerButton(danger);\n    context?.setContent(message);\n    context?.setConfirm(() => async () => {\n      await callback();\n      context?.cancel();\n    });\n  };\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/useCreatePermisson.ts",
    "content": "import { useContext } from 'react';\nimport { ResourceType } from 'generated-sources';\nimport { UserInfoRolesAccessContext } from 'components/contexts/UserInfoRolesAccessContext';\nimport { ClusterNameRoute } from 'lib/paths';\nimport { isPermittedToCreate } from 'lib/permissions';\n\nimport useAppParams from './useAppParams';\n\nexport function useCreatePermission(resource: ResourceType): boolean {\n  const { clusterName } = useAppParams<ClusterNameRoute>();\n  const { roles, rbacFlag } = useContext(UserInfoRolesAccessContext);\n\n  return isPermittedToCreate({ roles, resource, clusterName, rbacFlag });\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/useDataSaver.ts",
    "content": "import { showAlert, showSuccessAlert } from 'lib/errorHandling';\n\nconst useDataSaver = (\n  subject: string,\n  data: Record<string, string> | string\n) => {\n  const copyToClipboard = () => {\n    if (navigator.clipboard) {\n      const str =\n        typeof data === 'string' ? String(data) : JSON.stringify(data);\n      navigator.clipboard.writeText(str);\n      showSuccessAlert({\n        id: subject,\n        title: '',\n        message: 'Copied successfully!',\n      });\n    } else {\n      showAlert('warning', {\n        id: subject,\n        title: 'Warning',\n        message:\n          'Copying to clipboard is unavailable due to unsecured (non-HTTPS) connection',\n      });\n    }\n  };\n  const saveFile = () => {\n    const blob = new Blob([data as BlobPart], { type: 'text/json' });\n    const elem = window.document.createElement('a');\n    elem.href = window.URL.createObjectURL(blob);\n    elem.download = subject;\n    document.body.appendChild(elem);\n    elem.click();\n    document.body.removeChild(elem);\n  };\n\n  return { copyToClipboard, saveFile };\n};\n\nexport default useDataSaver;\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/useLocalStorage.ts",
    "content": "import { LOCAL_STORAGE_KEY_PREFIX } from 'lib/constants';\nimport { useState, useEffect } from 'react';\n\nexport const useLocalStorage = (featureKey: string, defaultValue: string) => {\n  const key = `${LOCAL_STORAGE_KEY_PREFIX}-${featureKey}`;\n  const [value, setValue] = useState(() => {\n    const saved = localStorage.getItem(key);\n\n    if (saved !== null) {\n      return JSON.parse(saved);\n    }\n    return defaultValue;\n  });\n\n  useEffect(() => {\n    localStorage.setItem(key, JSON.stringify(value));\n  }, [key, value]);\n\n  return [value, setValue];\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/useMessageFiltersStore.ts",
    "content": "import { LOCAL_STORAGE_KEY_PREFIX } from 'lib/constants';\nimport create from 'zustand';\nimport { persist } from 'zustand/middleware';\n\ninterface AdvancedFilter {\n  name: string;\n  value: string;\n}\n\ninterface MessageFiltersState {\n  filters: AdvancedFilter[];\n  activeFilter?: AdvancedFilter;\n  save: (filter: AdvancedFilter) => void;\n  apply: (filter: AdvancedFilter) => void;\n  remove: (name: string) => void;\n  update: (name: string, filter: AdvancedFilter) => void;\n}\n\nexport const useMessageFiltersStore = create<MessageFiltersState>()(\n  persist(\n    (set) => ({\n      filters: [],\n      save: (filter) =>\n        set((state) => ({\n          filters: [...state.filters, filter],\n        })),\n      apply: (filter) => set(() => ({ activeFilter: filter })),\n      remove: (name) =>\n        set((state) => ({\n          filters: state.filters.filter((f) => f.name !== name),\n        })),\n      update: (name, filter) =>\n        set((state) => ({\n          filters: state.filters.map((f) => (f.name === name ? filter : f)),\n        })),\n    }),\n    {\n      name: `${LOCAL_STORAGE_KEY_PREFIX}-message-filters`,\n    }\n  )\n);\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/usePermission.ts",
    "content": "import { useContext } from 'react';\nimport { Action, ResourceType } from 'generated-sources';\nimport { UserInfoRolesAccessContext } from 'components/contexts/UserInfoRolesAccessContext';\nimport { ClusterNameRoute } from 'lib/paths';\nimport { isPermitted } from 'lib/permissions';\nimport useAppParams from 'lib/hooks/useAppParams';\n\nexport function usePermission(\n  resource: ResourceType,\n  action: Action | Array<Action>,\n  value?: string\n): boolean {\n  const { clusterName } = useAppParams<ClusterNameRoute>();\n  const { roles, rbacFlag } = useContext(UserInfoRolesAccessContext);\n\n  return isPermitted({ roles, resource, action, clusterName, value, rbacFlag });\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/hooks/useUserInfo.ts",
    "content": "import { useContext } from 'react';\nimport { UserInfoRolesAccessContext } from 'components/contexts/UserInfoRolesAccessContext';\n\nexport function useUserInfo() {\n  return useContext(UserInfoRolesAccessContext);\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/paths.ts",
    "content": "import { Broker, Connect, Connector } from 'generated-sources';\nimport { ClusterName, SchemaName, TopicName } from 'redux/interfaces';\n\nimport { GIT_REPO_LINK } from './constants';\nimport { ConsumerGroupID } from './hooks/api/consumers';\n\nexport const gitCommitPath = (commit: string) =>\n  `${GIT_REPO_LINK}/commit/${commit}`;\n\nexport enum RouteParams {\n  clusterName = ':clusterName',\n  consumerGroupID = ':consumerGroupID',\n  subject = ':subject',\n  topicName = ':topicName',\n  connectName = ':connectName',\n  connectorName = ':connectorName',\n  brokerId = ':brokerId',\n}\n\nexport const getNonExactPath = (path: string) => `${path}/*`;\n\nexport const errorPage = '/404';\nexport const accessErrorPage = '/403';\n\nexport const clusterPath = (\n  clusterName: ClusterName = RouteParams.clusterName\n) => `/ui/clusters/${clusterName}`;\n\nexport type ClusterNameRoute = { clusterName: ClusterName };\n\n// Brokers\nexport const clusterBrokerRelativePath = 'brokers';\nexport const clusterBrokerMetricsRelativePath = 'metrics';\nexport const clusterBrokerConfigsRelativePath = 'configs';\n\nexport const clusterBrokersPath = (\n  clusterName: ClusterName = RouteParams.clusterName\n) => `${clusterPath(clusterName)}/${clusterBrokerRelativePath}`;\n\nexport const clusterBrokerPath = (\n  clusterName: ClusterName = RouteParams.clusterName,\n  brokerId: Broker['id'] | string = RouteParams.brokerId\n) => `${clusterBrokersPath(clusterName)}/${brokerId}`;\nexport const clusterBrokerMetricsPath = (\n  clusterName: ClusterName = RouteParams.clusterName,\n  brokerId: Broker['id'] | string = RouteParams.brokerId\n) =>\n  `${clusterBrokerPath(\n    clusterName,\n    brokerId\n  )}/${clusterBrokerMetricsRelativePath}`;\n\nexport const clusterBrokerConfigsPath = (\n  clusterName: ClusterName = RouteParams.clusterName,\n  brokerId: Broker['id'] | string = RouteParams.brokerId\n) =>\n  `${clusterBrokerPath(\n    clusterName,\n    brokerId\n  )}/${clusterBrokerConfigsRelativePath}`;\n\nexport type ClusterBrokerParam = {\n  clusterName: ClusterName;\n  brokerId: string;\n};\n\n// Consumer Groups\nexport const clusterConsumerGroupsRelativePath = 'consumer-groups';\nexport const clusterConsumerGroupResetRelativePath = 'reset-offsets';\nexport const clusterConsumerGroupResetOffsetsRelativePath = `${RouteParams.consumerGroupID}/${clusterConsumerGroupResetRelativePath}`;\nexport const clusterConsumerGroupsPath = (\n  clusterName: ClusterName = RouteParams.clusterName\n) => `${clusterPath(clusterName)}/${clusterConsumerGroupsRelativePath}`;\nexport const clusterConsumerGroupDetailsPath = (\n  clusterName: ClusterName = RouteParams.clusterName,\n  groupId: string = RouteParams.consumerGroupID\n) => `${clusterConsumerGroupsPath(clusterName)}/${groupId}`;\nexport const clusterConsumerGroupResetOffsetsPath = (\n  clusterName: ClusterName = RouteParams.clusterName,\n  groupId: string = RouteParams.consumerGroupID\n) =>\n  `${clusterConsumerGroupDetailsPath(\n    clusterName,\n    groupId\n  )}/${clusterConsumerGroupResetRelativePath}`;\nexport type ClusterGroupParam = {\n  consumerGroupID: ConsumerGroupID;\n  clusterName: ClusterName;\n};\n\n// Schemas\nexport const clusterSchemasRelativePath = 'schemas';\nexport const clusterSchemaNewRelativePath = 'create-new';\nexport const clusterSchemaEditPageRelativePath = `edit`;\nexport const clusterSchemaSchemaComparePageRelativePath = `compare`;\nexport const clusterSchemaEditRelativePath = `${RouteParams.subject}/${clusterSchemaEditPageRelativePath}`;\nexport const clusterSchemaSchemaDiffRelativePath = `${RouteParams.subject}/${clusterSchemaSchemaComparePageRelativePath}`;\nexport const clusterSchemasPath = (\n  clusterName: ClusterName = RouteParams.clusterName\n) => `${clusterPath(clusterName)}/schemas`;\nexport const clusterSchemaNewPath = (\n  clusterName: ClusterName = RouteParams.clusterName\n) => `${clusterSchemasPath(clusterName)}/${clusterSchemaNewRelativePath}`;\nexport const clusterSchemaPath = (\n  clusterName: ClusterName = RouteParams.clusterName,\n  subject: SchemaName = RouteParams.subject\n) => {\n  let subjectName = subject;\n  if (subject !== ':subject') subjectName = encodeURIComponent(subject);\n  return `${clusterSchemasPath(clusterName)}/${subjectName}`;\n};\nexport const clusterSchemaEditPath = (\n  clusterName: ClusterName = RouteParams.clusterName,\n  subject: SchemaName = RouteParams.subject\n) => {\n  let subjectName = subject;\n  if (subject !== ':subject') subjectName = encodeURIComponent(subject);\n  return `${clusterSchemasPath(clusterName)}/${subjectName}/edit`;\n};\nexport const clusterSchemaComparePath = (\n  clusterName: ClusterName = RouteParams.clusterName,\n  subject: SchemaName = RouteParams.subject\n) => `${clusterSchemaPath(clusterName, subject)}/compare`;\n\nexport type ClusterSubjectParam = {\n  subject: string;\n  clusterName: ClusterName;\n};\n\n// Topics\nexport const clusterTopicsRelativePath = 'all-topics';\nexport const clusterTopicNewRelativePath = 'create-new-topic';\nexport const clusterTopicCopyRelativePath = 'copy';\nexport const clusterTopicsPath = (\n  clusterName: ClusterName = RouteParams.clusterName\n) => `${clusterPath(clusterName)}/${clusterTopicsRelativePath}`;\nexport const clusterTopicNewPath = (\n  clusterName: ClusterName = RouteParams.clusterName\n) => `${clusterTopicsPath(clusterName)}/${clusterTopicNewRelativePath}`;\nexport const clusterTopicCopyPath = (\n  clusterName: ClusterName = RouteParams.clusterName\n) => `${clusterTopicsPath(clusterName)}/${clusterTopicCopyRelativePath}`;\n\n// Topics topic\nexport const clusterTopicSettingsRelativePath = 'settings';\nexport const clusterTopicMessagesRelativePath = 'messages';\nexport const clusterTopicConsumerGroupsRelativePath = 'consumer-groups';\nexport const clusterTopicStatisticsRelativePath = 'statistics';\nexport const clusterTopicEditRelativePath = 'edit';\nexport const clusterTopicPath = (\n  clusterName: ClusterName = RouteParams.clusterName,\n  topicName: TopicName = RouteParams.topicName\n) => `${clusterTopicsPath(clusterName)}/${topicName}`;\nexport const clusterTopicSettingsPath = (\n  clusterName: ClusterName = RouteParams.clusterName,\n  topicName: TopicName = RouteParams.topicName\n) =>\n  `${clusterTopicPath(\n    clusterName,\n    topicName\n  )}/${clusterTopicSettingsRelativePath}`;\nexport const clusterTopicMessagesPath = (\n  clusterName: ClusterName = RouteParams.clusterName,\n  topicName: TopicName = RouteParams.topicName\n) =>\n  `${clusterTopicPath(\n    clusterName,\n    topicName\n  )}/${clusterTopicMessagesRelativePath}`;\nexport const clusterTopicEditPath = (\n  clusterName: ClusterName = RouteParams.clusterName,\n  topicName: TopicName = RouteParams.topicName\n) =>\n  `${clusterTopicPath(clusterName, topicName)}/${clusterTopicEditRelativePath}`;\nexport const clusterTopicConsumerGroupsPath = (\n  clusterName: ClusterName = RouteParams.clusterName,\n  topicName: TopicName = RouteParams.topicName\n) =>\n  `${clusterTopicPath(\n    clusterName,\n    topicName\n  )}/${clusterTopicConsumerGroupsRelativePath}`;\nexport const clusterTopicStatisticsPath = (\n  clusterName: ClusterName = RouteParams.clusterName,\n  topicName: TopicName = RouteParams.topicName\n) =>\n  `${clusterTopicPath(\n    clusterName,\n    topicName\n  )}/${clusterTopicStatisticsRelativePath}`;\n\nexport type RouteParamsClusterTopic = {\n  clusterName: ClusterName;\n  topicName: TopicName;\n};\n\n// Kafka Connect\nexport const clusterConnectsRelativePath = 'connects';\nexport const clusterConnectorsRelativePath = 'connectors';\nexport const clusterConnectorNewRelativePath = 'create-new';\nexport const clusterConnectConnectorsRelativePath = `${RouteParams.connectName}/connectors`;\nexport const clusterConnectConnectorRelativePath = `${clusterConnectConnectorsRelativePath}/${RouteParams.connectorName}`;\nconst clusterConnectConnectorTasksRelativePath = 'tasks';\nexport const clusterConnectConnectorConfigRelativePath = 'config';\n\nexport const clusterConnectsPath = (\n  clusterName: ClusterName = RouteParams.clusterName\n) => `${clusterPath(clusterName)}/connects`;\nexport const clusterConnectorsPath = (\n  clusterName: ClusterName = RouteParams.clusterName\n) => `${clusterPath(clusterName)}/connectors`;\nexport const clusterConnectorNewPath = (\n  clusterName: ClusterName = RouteParams.clusterName\n) => `${clusterConnectorsPath(clusterName)}/create-new`;\nexport const clusterConnectConnectorsPath = (\n  clusterName: ClusterName = RouteParams.clusterName,\n  connectName: Connect['name'] = RouteParams.connectName\n) => `${clusterConnectsPath(clusterName)}/${connectName}/connectors`;\nexport const clusterConnectConnectorPath = (\n  clusterName: ClusterName = RouteParams.clusterName,\n  connectName: Connect['name'] = RouteParams.connectName,\n  connectorName: Connector['name'] = RouteParams.connectorName\n) =>\n  `${clusterConnectConnectorsPath(clusterName, connectName)}/${connectorName}`;\nexport const clusterConnectConnectorEditPath = (\n  clusterName: ClusterName = RouteParams.clusterName,\n  connectName: Connect['name'] = RouteParams.connectName,\n  connectorName: Connector['name'] = RouteParams.connectorName\n) =>\n  `${clusterConnectConnectorsPath(\n    clusterName,\n    connectName\n  )}/${connectorName}/edit`;\nexport const clusterConnectConnectorTasksPath = (\n  clusterName: ClusterName = RouteParams.clusterName,\n  connectName: Connect['name'] = RouteParams.connectName,\n  connectorName: Connector['name'] = RouteParams.connectorName\n) =>\n  `${clusterConnectConnectorPath(\n    clusterName,\n    connectName,\n    connectorName\n  )}/${clusterConnectConnectorTasksRelativePath}`;\nexport const clusterConnectConnectorConfigPath = (\n  clusterName: ClusterName = RouteParams.clusterName,\n  connectName: Connect['name'] = RouteParams.connectName,\n  connectorName: Connector['name'] = RouteParams.connectorName\n) =>\n  `${clusterConnectConnectorPath(\n    clusterName,\n    connectName,\n    connectorName\n  )}/${clusterConnectConnectorConfigRelativePath}`;\n\nexport type RouterParamsClusterConnectConnector = {\n  clusterName: ClusterName;\n  connectName: Connect['name'];\n  connectorName: Connector['name'];\n};\n\n// KsqlDb\nexport const clusterKsqlDbRelativePath = 'ksqldb';\nexport const clusterKsqlDbQueryRelativePath = 'query';\nexport const clusterKsqlDbTablesRelativePath = 'tables';\nexport const clusterKsqlDbStreamsRelativePath = 'streams';\n\nexport const clusterKsqlDbPath = (\n  clusterName: ClusterName = RouteParams.clusterName\n) => `${clusterPath(clusterName)}/${clusterKsqlDbRelativePath}`;\nexport const clusterKsqlDbQueryPath = (\n  clusterName: ClusterName = RouteParams.clusterName\n) => `${clusterKsqlDbPath(clusterName)}/${clusterKsqlDbQueryRelativePath}`;\nexport const clusterKsqlDbTablesPath = (\n  clusterName: ClusterName = RouteParams.clusterName\n) => `${clusterKsqlDbPath(clusterName)}/${clusterKsqlDbTablesRelativePath}`;\nexport const clusterKsqlDbStreamsPath = (\n  clusterName: ClusterName = RouteParams.clusterName\n) => `${clusterKsqlDbPath(clusterName)}/${clusterKsqlDbStreamsRelativePath}`;\n\n// Cluster Config\nexport const clusterConfigRelativePath = 'config';\nexport const clusterConfigPath = (\n  clusterName: ClusterName = RouteParams.clusterName\n) => `${clusterPath(clusterName)}/${clusterConfigRelativePath}`;\n\nconst clusterNewConfigRelativePath = 'create-new-cluster';\nexport const clusterNewConfigPath = `/ui/clusters/${clusterNewConfigRelativePath}`;\n\n// ACL\nexport const clusterAclRelativePath = 'acl';\nexport const clusterAclNewRelativePath = 'create-new-acl';\nexport const clusterACLPath = (\n  clusterName: ClusterName = RouteParams.clusterName\n) => `${clusterPath(clusterName)}/${clusterAclRelativePath}`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/permissions.ts",
    "content": "import { Action, ResourceType, UserPermission } from 'generated-sources';\n\nexport type RolesType = UserPermission[];\n\nexport type RolesModifiedTypes = Map<string, Map<ResourceType, RolesType>>;\n\nconst ResourceExemptList: ResourceType[] = [\n  ResourceType.KSQL,\n  ResourceType.CLUSTERCONFIG,\n  ResourceType.APPLICATIONCONFIG,\n  ResourceType.ACL,\n  ResourceType.AUDIT,\n];\n\nexport function modifyRolesData(\n  data?: RolesType\n): Map<string, Map<ResourceType, RolesType>> {\n  const map = new Map<string, Map<ResourceType, RolesType>>();\n\n  data?.forEach((item) => {\n    item.clusters.forEach((name) => {\n      const cluster = map.get(name);\n      if (cluster) {\n        const { resource } = item;\n\n        const resourceItem = cluster.get(resource);\n        if (resourceItem) {\n          cluster.set(resource, resourceItem.concat(item));\n          return;\n        }\n        cluster.set(resource, [item]);\n        return;\n      }\n\n      map.set(name, new Map().set(item.resource, [item]));\n    });\n  });\n  return map;\n}\n\ninterface IsPermittedConfig {\n  roles?: RolesModifiedTypes;\n  resource: ResourceType;\n  action: Action | Array<Action>;\n  clusterName: string;\n  value?: string;\n  rbacFlag: boolean;\n}\n\nconst valueMatches = (regexp: string | undefined, val: string | undefined) => {\n  if (!val) return false;\n  if (!regexp) return true;\n  return new RegExp(regexp).test(val);\n};\n\n/**\n * @description it the logic behind depending on the roles whether a certain action\n * is permitted or not the philosophy is inspired from Headless UI libraries where\n * you separate the logic from the renderer besides the Creation process which is handled by isPermittedToCreate\n *\n * Algorithm: we Mapped the cluster name and the resource name , because all the actions in them are\n * constant and limited and hence faster lookup approach\n *\n * @example you can use this in the hook format where it used in , or if you want to calculate it dynamically\n * you can call this dynamically in your component but the render is on you from that point on\n *\n * Don't use this anywhere , use the hook version in the component for declarative purposes\n *\n * Array action approach bear in mind they should be from the same resource with the same name restrictions, then the logic it\n * will try to find every element from the given array inside the permissions data\n *\n * DON'T use the array approach until it is necessary to do so\n *\n * */\nexport function isPermitted({\n  roles,\n  resource,\n  action,\n  clusterName,\n  value,\n  rbacFlag,\n}: {\n  roles?: RolesModifiedTypes;\n  resource: ResourceType;\n  action: Action | Array<Action>;\n  clusterName: string;\n  value?: string;\n  rbacFlag: boolean;\n}) {\n  if (!rbacFlag) return true;\n\n  // short circuit\n  if (!roles || roles.size === 0) return false;\n\n  // short circuit\n  const clusterMap = roles.get(clusterName);\n  if (!clusterMap) return false;\n\n  // short circuit\n  const resourcePermissions = clusterMap.get(resource);\n  if (!resourcePermissions) return false;\n\n  const actions = Array.isArray(action) ? action : [action];\n\n  return actions.every((a) => {\n    return resourcePermissions.some((item) => {\n      if (!item.actions.includes(a)) return false;\n      if (ResourceExemptList.includes(resource)) return true;\n      return valueMatches(item.value, value);\n    });\n  });\n}\n\n/**\n * @description it the logic behind depending on create roles, since create has extra custom permission logic that is why\n * it is seperated from the others\n *\n * Algorithm: we Mapped the cluster name and the resource name , because all the actions in them are\n * constant and limited and hence faster lookup approach\n *\n * @example you can use this in the hook format where it used in , or if you want to calculate it dynamically\n * you can call this dynamically in your component but the render is on you from that point on\n *\n * Don't use this anywhere , use the hook version in the component for declarative purposes\n *\n * */\nexport function isPermittedToCreate({\n  roles,\n  resource,\n  clusterName,\n  rbacFlag,\n}: Omit<IsPermittedConfig, 'value' | 'action'>) {\n  if (!rbacFlag) return true;\n\n  // short circuit\n  if (!roles || roles.size === 0) return false;\n\n  // short circuit\n  const clusterMap = roles.get(clusterName);\n  if (!clusterMap) return false;\n\n  // short circuit\n  const resourceData = clusterMap.get(resource);\n  if (!resourceData) return false;\n\n  const action = Action.CREATE;\n\n  return resourceData.some((item) => {\n    return item.actions.includes(action);\n  });\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/testHelpers.tsx",
    "content": "import React, { PropsWithChildren, ReactElement, useMemo } from 'react';\nimport {\n  MemoryRouter,\n  MemoryRouterProps,\n  Route,\n  Routes,\n} from 'react-router-dom';\nimport fetchMock from 'fetch-mock';\nimport { Provider } from 'react-redux';\nimport { ThemeProvider } from 'styled-components';\nimport { theme } from 'theme/theme';\nimport {\n  render,\n  renderHook,\n  RenderOptions,\n  waitFor,\n} from '@testing-library/react';\nimport { AnyAction, Store } from 'redux';\nimport { RootState } from 'redux/interfaces';\nimport { configureStore } from '@reduxjs/toolkit';\nimport rootReducer from 'redux/reducers';\nimport {\n  QueryClient,\n  QueryClientProvider,\n  UseQueryResult,\n} from '@tanstack/react-query';\nimport { ConfirmContextProvider } from 'components/contexts/ConfirmContext';\nimport ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';\nimport { GlobalSettingsContext } from 'components/contexts/GlobalSettingsContext';\nimport { UserInfoRolesAccessContext } from 'components/contexts/UserInfoRolesAccessContext';\n\nimport { RolesType, modifyRolesData } from './permissions';\n\ninterface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {\n  preloadedState?: Partial<RootState>;\n  store?: Store<Partial<RootState>, AnyAction>;\n  initialEntries?: MemoryRouterProps['initialEntries'];\n  userInfo?: {\n    roles?: RolesType;\n    rbacFlag: boolean;\n  };\n  globalSettings?: {\n    hasDynamicConfig: boolean;\n  };\n}\n\ninterface WithRouteProps {\n  children: React.ReactNode;\n  path: string;\n}\n\nexport const expectQueryWorks = async (\n  mock: fetchMock.FetchMockStatic,\n  result: { current: UseQueryResult<unknown, unknown> }\n) => {\n  await waitFor(() => expect(result.current.isFetched).toBeTruthy());\n  expect(mock.calls()).toHaveLength(1);\n  expect(result.current.data).toBeDefined();\n};\n\nexport const WithRoute: React.FC<WithRouteProps> = ({ children, path }) => {\n  return (\n    <Routes>\n      <Route path={path} element={children} />\n    </Routes>\n  );\n};\n\nexport const TestQueryClientProvider: React.FC<PropsWithChildren<unknown>> = ({\n  children,\n}) => {\n  // use new QueryClient instance for each test run to avoid issues with cache\n  const queryClient = new QueryClient({\n    defaultOptions: { queries: { retry: false } },\n  });\n  return (\n    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>\n  );\n};\n\n/**\n * @description it will create a UserInfo Provider that will actually\n * disable the rbacFlag , to user if you can pass it as an argument\n * */\nconst TestUserInfoProvider: React.FC<\n  PropsWithChildren<{ data?: { roles?: RolesType; rbacFlag: boolean } }>\n> = ({ children, data }) => {\n  const contextValue = useMemo(() => {\n    const roles = modifyRolesData(data?.roles);\n\n    return {\n      username: 'test',\n      rbacFlag: !!(typeof data?.rbacFlag === 'undefined'\n        ? false\n        : data?.rbacFlag),\n      roles,\n    };\n  }, [data]);\n\n  return (\n    <UserInfoRolesAccessContext.Provider value={contextValue}>\n      {children}\n    </UserInfoRolesAccessContext.Provider>\n  );\n};\n\nconst customRender = (\n  ui: ReactElement,\n  {\n    preloadedState,\n    store = configureStore<RootState>({\n      reducer: rootReducer,\n      preloadedState,\n    }),\n    initialEntries,\n    userInfo,\n    globalSettings,\n    ...renderOptions\n  }: CustomRenderOptions = {}\n) => {\n  // overrides @testing-library/react render.\n  const AllTheProviders: React.FC<PropsWithChildren<unknown>> = ({\n    children,\n  }) => (\n    <TestQueryClientProvider>\n      <GlobalSettingsContext.Provider\n        value={globalSettings || { hasDynamicConfig: false }}\n      >\n        <ThemeProvider theme={theme}>\n          <TestUserInfoProvider data={userInfo}>\n            <ConfirmContextProvider>\n              <Provider store={store}>\n                <MemoryRouter initialEntries={initialEntries}>\n                  <div>\n                    {children}\n                    <ConfirmationModal />\n                  </div>\n                </MemoryRouter>\n              </Provider>\n            </ConfirmContextProvider>\n          </TestUserInfoProvider>\n        </ThemeProvider>\n      </GlobalSettingsContext.Provider>\n    </TestQueryClientProvider>\n  );\n  return render(ui, { wrapper: AllTheProviders, ...renderOptions });\n};\n\nconst customRenderHook = (hook: () => UseQueryResult<unknown, unknown>) =>\n  renderHook(hook, { wrapper: TestQueryClientProvider });\n\nexport { customRender as render, customRenderHook as renderQueryHook };\n\nexport class EventSourceMock {\n  url: string;\n\n  close: () => void;\n\n  open: () => void;\n\n  error: () => void;\n\n  onmessage: () => void;\n\n  constructor(url: string) {\n    this.url = url;\n    this.open = jest.fn();\n    this.error = jest.fn();\n    this.onmessage = jest.fn();\n    this.close = jest.fn();\n  }\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/lib/yupExtended.ts",
    "content": "import * as yup from 'yup';\n\nimport { TOPIC_NAME_VALIDATION_PATTERN } from './constants';\n\ndeclare module 'yup' {\n  interface StringSchema<\n    TType extends yup.Maybe<string> = string | undefined,\n    TContext = yup.AnyObject,\n    TDefault = undefined,\n    TFlags extends yup.Flags = ''\n  > extends yup.Schema<TType, TContext, TDefault, TFlags> {\n    isJsonObject(message?: string): StringSchema<TType, TContext>;\n  }\n}\n\nexport const isValidJsonObject = (value?: string) => {\n  try {\n    if (!value) return false;\n\n    const trimmedValue = value.trim();\n    if (\n      trimmedValue.indexOf('{') === 0 &&\n      trimmedValue.lastIndexOf('}') === trimmedValue.length - 1\n    ) {\n      JSON.parse(trimmedValue);\n      return true;\n    }\n  } catch {\n    // do nothing\n  }\n  return false;\n};\n\nconst isJsonObject = (message?: string) => {\n  return yup.string().test(\n    'isJsonObject',\n    // eslint-disable-next-line no-template-curly-in-string\n    message || '${path} is not JSON object',\n    isValidJsonObject\n  );\n};\n/**\n * due to yup rerunning all the object validiation during any render,\n * it makes sense to cache the async results\n * */\nexport function cacheTest(\n  asyncValidate: (val?: string, ctx?: yup.AnyObject) => Promise<boolean>\n) {\n  let valid = false;\n  let closureValue = '';\n\n  return async (value?: string, ctx?: yup.AnyObject) => {\n    if (value !== closureValue) {\n      const response = await asyncValidate(value, ctx);\n      closureValue = value || '';\n      valid = response;\n      return response;\n    }\n    return valid;\n  };\n}\n\nyup.addMethod(yup.StringSchema, 'isJsonObject', isJsonObject);\n\nexport const topicFormValidationSchema = yup.object().shape({\n  name: yup\n    .string()\n    .max(249)\n    .required('Topic Name is required')\n    .matches(\n      TOPIC_NAME_VALIDATION_PATTERN,\n      'Only alphanumeric, _, -, and . allowed'\n    ),\n  partitions: yup\n    .number()\n    .min(1, 'Number of Partitions must be greater than or equal to 1')\n    .max(2147483647)\n    .required()\n    .typeError('Number of Partitions is required and must be a number'),\n  replicationFactor: yup.string(),\n  minInSyncReplicas: yup.string(),\n  cleanupPolicy: yup.string().required(),\n  retentionMs: yup.string(),\n  retentionBytes: yup.number(),\n  maxMessageBytes: yup.string(),\n  customParams: yup.array().of(\n    yup.object().shape({\n      name: yup.string().required('Custom parameter is required'),\n      value: yup.string().required('Value is required'),\n    })\n  ),\n});\n\nexport default yup;\n"
  },
  {
    "path": "kafka-ui-react-app/src/react-app-env.d.ts",
    "content": "/// <reference types=\"node\" />\n/// <reference types=\"react\" />\n/// <reference types=\"react-dom\" />\n"
  },
  {
    "path": "kafka-ui-react-app/src/redux/interfaces/cluster.ts",
    "content": "import { Cluster } from 'generated-sources';\n\nexport type ClusterName = Cluster['name'];\n"
  },
  {
    "path": "kafka-ui-react-app/src/redux/interfaces/consumerGroup.ts",
    "content": "import {\n  ConsumerGroup,\n  ConsumerGroupOffsetsResetType,\n} from 'generated-sources';\n\nimport { ClusterName } from './cluster';\n\nexport interface ConsumerGroupResetOffsetRequestParams {\n  clusterName: ClusterName;\n  consumerGroupID: ConsumerGroup['groupId'];\n  requestBody: {\n    topic: string;\n    resetType: ConsumerGroupOffsetsResetType;\n    partitionsOffsets?: { offset: string; partition: number }[];\n    resetToTimestamp?: Date;\n    partitions: number[];\n  };\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/redux/interfaces/index.ts",
    "content": "import rootReducer from 'redux/reducers';\nimport { store } from 'redux/store';\n\nexport * from './topic';\nexport * from './cluster';\nexport * from './consumerGroup';\nexport * from './schema';\nexport * from './loader';\n\nexport type RootState = ReturnType<typeof rootReducer>;\nexport type AppDispatch = typeof store.dispatch;\n"
  },
  {
    "path": "kafka-ui-react-app/src/redux/interfaces/loader.ts",
    "content": "import { AsyncRequestStatus } from 'lib/constants';\n\nexport interface LoaderSliceState {\n  [key: string]: AsyncRequestStatus;\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/redux/interfaces/schema.ts",
    "content": "import {\n  CompatibilityLevelCompatibilityEnum,\n  NewSchemaSubject,\n} from 'generated-sources';\n\nexport type SchemaName = string;\n\nexport interface NewSchemaSubjectRaw extends NewSchemaSubject {\n  subject: string;\n  compatibilityLevel: CompatibilityLevelCompatibilityEnum;\n  newSchema: string;\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/redux/interfaces/topic.ts",
    "content": "import {\n  Topic,\n  TopicConfig,\n  TopicCreation,\n  TopicMessage,\n  TopicMessageConsuming,\n} from 'generated-sources';\n\nexport type TopicName = Topic['name'];\n\nexport interface TopicConfigParams {\n  [paramName: string]: TopicConfig;\n}\n\nexport interface TopicConfigByName {\n  byName: TopicConfigParams;\n}\n\ninterface TopicFormCustomParams {\n  byIndex: TopicConfigParams;\n  allIndexes: TopicName[];\n}\n\nexport type TopicFormFormattedParams = TopicCreation['configs'];\n\ninterface TopicFormDataModified {\n  name: string;\n  partitions: number;\n  replicationFactor: number;\n  minInSyncReplicas: number;\n  cleanupPolicy: string;\n  retentionMs: number;\n  retentionBytes: number;\n  maxMessageBytes: number;\n  customParams: TopicFormCustomParams;\n}\n\nexport type TopicFormDataRaw = Partial<TopicFormDataModified>;\n\nexport interface TopicFormData {\n  name: string;\n  partitions: number;\n  replicationFactor: number;\n  minInSyncReplicas: number;\n  cleanupPolicy: string;\n  retentionMs: number;\n  maxMessageBytes: number;\n  customParams: {\n    name: string;\n    value: string;\n  }[];\n}\n\nexport interface TopicMessagesState {\n  messages: TopicMessage[];\n  phase?: string;\n  meta: TopicMessageConsuming;\n  messageEventType?: string;\n  isFetching: boolean;\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/redux/reducers/index.ts",
    "content": "import { combineReducers } from '@reduxjs/toolkit';\nimport loader from 'redux/reducers/loader/loaderSlice';\nimport schemas from 'redux/reducers/schemas/schemasSlice';\nimport topicMessages from 'redux/reducers/topicMessages/topicMessagesSlice';\n\nexport default combineReducers({\n  loader,\n  topicMessages,\n  schemas,\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/redux/reducers/loader/loaderSlice.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit';\nimport {\n  UnknownAsyncThunkFulfilledAction,\n  UnknownAsyncThunkPendingAction,\n  UnknownAsyncThunkRejectedAction,\n} from '@reduxjs/toolkit/dist/matchers';\nimport { AsyncRequestStatus } from 'lib/constants';\nimport { LoaderSliceState } from 'redux/interfaces';\n\nconst initialState: LoaderSliceState = {};\n\nconst loaderSlice = createSlice({\n  name: 'loader',\n  initialState,\n  reducers: {\n    resetLoaderById: (\n      state: LoaderSliceState,\n      { payload }: PayloadAction<string>\n    ) => {\n      delete state[payload];\n    },\n  },\n  extraReducers: (builder) => {\n    builder\n      .addMatcher(\n        (action): action is UnknownAsyncThunkPendingAction =>\n          action.type.endsWith('/pending'),\n        (state, { type }) => {\n          state[type.replace('/pending', '')] = AsyncRequestStatus.pending;\n        }\n      )\n      .addMatcher(\n        (action): action is UnknownAsyncThunkFulfilledAction =>\n          action.type.endsWith('/fulfilled'),\n        (state, { type }) => {\n          state[type.replace('/fulfilled', '')] = AsyncRequestStatus.fulfilled;\n        }\n      )\n      .addMatcher(\n        (action): action is UnknownAsyncThunkRejectedAction =>\n          action.type.endsWith('/rejected'),\n        (state, { type }) => {\n          state[type.replace('/rejected', '')] = AsyncRequestStatus.rejected;\n        }\n      );\n  },\n});\n\nexport const { resetLoaderById } = loaderSlice.actions;\n\nexport default loaderSlice.reducer;\n"
  },
  {
    "path": "kafka-ui-react-app/src/redux/reducers/loader/selectors.ts",
    "content": "import { RootState } from 'redux/interfaces';\nimport { AsyncRequestStatus } from 'lib/constants';\n\nexport const createFetchingSelector = (action: string) => (state: RootState) =>\n  state.loader[action] || AsyncRequestStatus.initial;\n"
  },
  {
    "path": "kafka-ui-react-app/src/redux/reducers/schemas/__test__/fixtures.ts",
    "content": "import { SchemaType, SchemaSubject } from 'generated-sources';\nimport { RootState } from 'redux/interfaces';\n\nexport const schemasInitialState: RootState['schemas'] = {\n  totalPages: 0,\n  ids: [],\n  entities: {},\n  versions: {\n    latest: null,\n    ids: [],\n    entities: {},\n  },\n};\n\nexport const schemaVersion1: SchemaSubject = {\n  subject: 'schema7_1',\n  version: '1',\n  id: 2,\n  schema:\n    '{\"$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\"}}}',\n  compatibilityLevel: 'FULL',\n  schemaType: SchemaType.JSON,\n};\nexport const schemaVersion2: SchemaSubject = {\n  subject: 'MySchemaSubject',\n  version: '2',\n  id: 28,\n  schema: '12',\n  compatibilityLevel: 'FORWARD_TRANSITIVE',\n  schemaType: SchemaType.JSON,\n};\nexport const schemaVersionWithNonAsciiChars: SchemaSubject = {\n  subject: 'test/test',\n  version: '1',\n  id: 29,\n  schema: '13',\n  compatibilityLevel: 'FORWARD_TRANSITIVE',\n  schemaType: SchemaType.JSON,\n};\n\nexport { schemaVersion1 as schemaVersion };\n\nexport const schemasFulfilledState = {\n  totalPages: 1,\n  ids: [schemaVersion2.subject, schemaVersion1.subject],\n  entities: {\n    [schemaVersion2.subject]: schemaVersion2,\n    [schemaVersion1.subject]: schemaVersion1,\n  },\n  versions: {\n    latest: null,\n    ids: [],\n    entities: {},\n  },\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/redux/reducers/schemas/schemasSlice.ts",
    "content": "import {\n  createAsyncThunk,\n  createEntityAdapter,\n  createSelector,\n  createSlice,\n} from '@reduxjs/toolkit';\nimport {\n  SchemaSubject,\n  SchemaSubjectsResponse,\n  GetSchemasRequest,\n  GetLatestSchemaRequest,\n} from 'generated-sources';\nimport { schemasApiClient } from 'lib/api';\nimport { AsyncRequestStatus } from 'lib/constants';\nimport { getResponse, showServerError } from 'lib/errorHandling';\nimport { ClusterName, RootState } from 'redux/interfaces';\nimport { createFetchingSelector } from 'redux/reducers/loader/selectors';\n\nexport const SCHEMA_LATEST_FETCH_ACTION = 'schemas/latest/fetch';\nexport const fetchLatestSchema = createAsyncThunk<\n  SchemaSubject,\n  GetLatestSchemaRequest\n>(SCHEMA_LATEST_FETCH_ACTION, async (schemaParams, { rejectWithValue }) => {\n  try {\n    return await schemasApiClient.getLatestSchema(schemaParams);\n  } catch (error) {\n    showServerError(error as Response);\n    return rejectWithValue(await getResponse(error as Response));\n  }\n});\n\nexport const SCHEMAS_FETCH_ACTION = 'schemas/fetch';\nexport const fetchSchemas = createAsyncThunk<\n  SchemaSubjectsResponse,\n  GetSchemasRequest\n>(\n  SCHEMAS_FETCH_ACTION,\n  async ({ clusterName, page, perPage, search }, { rejectWithValue }) => {\n    try {\n      return await schemasApiClient.getSchemas({\n        clusterName,\n        page,\n        perPage,\n        search: search || undefined,\n      });\n    } catch (error) {\n      showServerError(error as Response);\n      return rejectWithValue(await getResponse(error as Response));\n    }\n  }\n);\n\nexport const SCHEMAS_VERSIONS_FETCH_ACTION = 'schemas/versions/fetch';\nexport const fetchSchemaVersions = createAsyncThunk<\n  SchemaSubject[],\n  { clusterName: ClusterName; subject: SchemaSubject['subject'] }\n>(\n  SCHEMAS_VERSIONS_FETCH_ACTION,\n  async ({ clusterName, subject }, { rejectWithValue }) => {\n    try {\n      return await schemasApiClient.getAllVersionsBySubject({\n        clusterName,\n        subject,\n      });\n    } catch (error) {\n      showServerError(error as Response);\n      return rejectWithValue(await getResponse(error as Response));\n    }\n  }\n);\n\nconst schemaVersionsAdapter = createEntityAdapter<SchemaSubject>({\n  selectId: ({ id }) => id,\n  sortComparer: (a, b) => b.id - a.id,\n});\nconst schemasAdapter = createEntityAdapter<SchemaSubject>({\n  selectId: ({ subject }) => subject,\n});\n\nconst SCHEMAS_PAGE_COUNT = 1;\n\nconst initialState = {\n  totalPages: SCHEMAS_PAGE_COUNT,\n  ...schemasAdapter.getInitialState(),\n  versions: {\n    ...schemaVersionsAdapter.getInitialState(),\n    latest: <SchemaSubject | null>null,\n  },\n};\n\nconst schemasSlice = createSlice({\n  name: 'schemas',\n  initialState,\n  reducers: {\n    schemaAdded: schemasAdapter.addOne,\n    schemaUpdated: schemasAdapter.upsertOne,\n  },\n  extraReducers: (builder) => {\n    builder.addCase(fetchSchemas.fulfilled, (state, { payload }) => {\n      state.totalPages = payload.pageCount || SCHEMAS_PAGE_COUNT;\n      schemasAdapter.setAll(state, payload.schemas || []);\n    });\n    builder.addCase(fetchLatestSchema.fulfilled, (state, { payload }) => {\n      state.versions.latest = payload;\n    });\n    builder.addCase(fetchSchemaVersions.fulfilled, (state, { payload }) => {\n      schemaVersionsAdapter.setAll(state.versions, payload);\n    });\n  },\n});\n\nexport const { selectAll: selectAllSchemas } =\n  schemasAdapter.getSelectors<RootState>((state) => state.schemas);\n\nexport const { selectAll: selectAllSchemaVersions } =\n  schemaVersionsAdapter.getSelectors<RootState>(\n    (state) => state.schemas.versions\n  );\n\nconst getSchemaVersions = (state: RootState) => state.schemas.versions;\nexport const getSchemaLatest = createSelector(\n  getSchemaVersions,\n  (state) => state.latest\n);\n\nexport const { schemaAdded, schemaUpdated } = schemasSlice.actions;\n\nexport const getAreSchemasFulfilled = createSelector(\n  createFetchingSelector(SCHEMAS_FETCH_ACTION),\n  (status) => status === AsyncRequestStatus.fulfilled\n);\n\nexport const getAreSchemaLatestFulfilled = createSelector(\n  createFetchingSelector(SCHEMA_LATEST_FETCH_ACTION),\n  (status) => status === AsyncRequestStatus.fulfilled\n);\nexport const getAreSchemaLatestRejected = createSelector(\n  createFetchingSelector(SCHEMA_LATEST_FETCH_ACTION),\n  (status) => status === AsyncRequestStatus.rejected\n);\n\nexport const getAreSchemaVersionsFulfilled = createSelector(\n  createFetchingSelector(SCHEMAS_VERSIONS_FETCH_ACTION),\n  (status) => status === AsyncRequestStatus.fulfilled\n);\n\nexport default schemasSlice.reducer;\n"
  },
  {
    "path": "kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/fixtures.ts",
    "content": "import {\n  TopicMessage,\n  TopicMessageConsuming,\n  TopicMessageTimestampTypeEnum,\n} from 'generated-sources';\n\nexport const topicMessagePayload: TopicMessage = {\n  partition: 29,\n  offset: 14,\n  timestamp: new Date('2021-07-21T23:25:14.865Z'),\n  timestampType: TopicMessageTimestampTypeEnum.CREATE_TIME,\n  key: 'schema-registry',\n  headers: {},\n  content:\n    '{\"host\":\"schemaregistry1\",\"port\":8085,\"master_eligibility\":true,\"scheme\":\"http\",\"version\":1}',\n};\n\nexport const topicMessagePayloadV2: TopicMessage = {\n  ...topicMessagePayload,\n  partition: 28,\n  offset: 88,\n};\n\nexport const topicMessagesMetaPayload: TopicMessageConsuming = {\n  bytesConsumed: 1830,\n  elapsedMs: 440,\n  messagesConsumed: 2301,\n  isCancelled: false,\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/reducer.spec.ts",
    "content": "import reducer, {\n  addTopicMessage,\n  resetTopicMessages,\n  updateTopicMessagesMeta,\n  updateTopicMessagesPhase,\n} from 'redux/reducers/topicMessages/topicMessagesSlice';\n\nimport {\n  topicMessagePayload,\n  topicMessagePayloadV2,\n  topicMessagesMetaPayload,\n} from './fixtures';\n\ndescribe('TopicMessages reducer', () => {\n  it('Adds new message', () => {\n    const state = reducer(\n      undefined,\n      addTopicMessage({ message: topicMessagePayload })\n    );\n    expect(state.messages.length).toEqual(1);\n  });\n\n  it('Adds new message with live tailing one', () => {\n    const state = reducer(\n      undefined,\n      addTopicMessage({ message: topicMessagePayload })\n    );\n    const modifiedState = reducer(\n      state,\n      addTopicMessage({ message: topicMessagePayloadV2, prepend: true })\n    );\n    expect(modifiedState.messages.length).toEqual(2);\n    expect(modifiedState.messages).toEqual([\n      topicMessagePayloadV2,\n      topicMessagePayload,\n    ]);\n  });\n\n  it('Adds new message with live tailing off', () => {\n    const state = reducer(\n      undefined,\n      addTopicMessage({ message: topicMessagePayload })\n    );\n    const modifiedState = reducer(\n      state,\n      addTopicMessage({ message: topicMessagePayloadV2 })\n    );\n    expect(modifiedState.messages.length).toEqual(2);\n    expect(modifiedState.messages).toEqual([\n      topicMessagePayload,\n      topicMessagePayloadV2,\n    ]);\n  });\n\n  it('reset messages', () => {\n    const state = reducer(\n      undefined,\n      addTopicMessage({ message: topicMessagePayload })\n    );\n    expect(state.messages.length).toEqual(1);\n\n    const newState = reducer(state, resetTopicMessages());\n    expect(newState.messages.length).toEqual(0);\n  });\n\n  it('Updates Topic Messages Phase', () => {\n    const phase = 'Polling';\n\n    const state = reducer(undefined, updateTopicMessagesPhase(phase));\n    expect(state.phase).toEqual(phase);\n  });\n  it('Updates Topic Messages Meta', () => {\n    const state = reducer(\n      undefined,\n      updateTopicMessagesMeta(topicMessagesMetaPayload)\n    );\n    expect(state.meta).toEqual(topicMessagesMetaPayload);\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/selectors.spec.ts",
    "content": "import { store } from 'redux/store';\nimport * as selectors from 'redux/reducers/topicMessages/selectors';\nimport {\n  initialState,\n  addTopicMessage,\n  updateTopicMessagesMeta,\n  updateTopicMessagesPhase,\n} from 'redux/reducers/topicMessages/topicMessagesSlice';\n\nimport { topicMessagePayload, topicMessagesMetaPayload } from './fixtures';\n\nconst newTopicMessagePayload = {\n  ...topicMessagePayload,\n  timestamp: topicMessagePayload.timestamp.toString(),\n};\ndescribe('TopicMessages selectors', () => {\n  describe('Initial state', () => {\n    it('returns empty message array', () => {\n      expect(selectors.getTopicMessges(store.getState())).toEqual([]);\n    });\n\n    it('returns undefined phase', () => {\n      expect(selectors.getTopicMessgesPhase(store.getState())).toBeUndefined();\n    });\n\n    it('returns initial vesrion of meta', () => {\n      expect(selectors.getTopicMessgesMeta(store.getState())).toEqual(\n        initialState.meta\n      );\n    });\n  });\n\n  describe('state', () => {\n    beforeAll(() => {\n      store.dispatch(\n        addTopicMessage({\n          message: newTopicMessagePayload,\n        })\n      );\n      store.dispatch(updateTopicMessagesPhase('consuming'));\n      store.dispatch(updateTopicMessagesMeta(topicMessagesMetaPayload));\n    });\n\n    it('returns messages', () => {\n      expect(selectors.getTopicMessges(store.getState())).toEqual([\n        newTopicMessagePayload,\n      ]);\n    });\n\n    it('returns phase', () => {\n      expect(selectors.getTopicMessgesPhase(store.getState())).toEqual(\n        'consuming'\n      );\n    });\n\n    it('returns ordered versions of schema', () => {\n      expect(selectors.getTopicMessgesMeta(store.getState())).toEqual(\n        topicMessagesMetaPayload\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/redux/reducers/topicMessages/selectors.ts",
    "content": "import { createSelector } from '@reduxjs/toolkit';\nimport { RootState, TopicMessagesState } from 'redux/interfaces';\n\nconst topicMessagesState = ({ topicMessages }: RootState): TopicMessagesState =>\n  topicMessages;\n\nexport const getTopicMessges = createSelector(\n  topicMessagesState,\n  ({ messages }) => messages\n);\n\nexport const getTopicMessgesPhase = createSelector(\n  topicMessagesState,\n  ({ phase }) => phase\n);\n\nexport const getTopicMessgesMeta = createSelector(\n  topicMessagesState,\n  ({ meta }) => meta\n);\n\nexport const getIsTopicMessagesFetching = createSelector(\n  topicMessagesState,\n  ({ isFetching }) => isFetching\n);\n\nexport const getIsTopicMessagesType = createSelector(\n  topicMessagesState,\n  ({ messageEventType }) => messageEventType\n);\n"
  },
  {
    "path": "kafka-ui-react-app/src/redux/reducers/topicMessages/topicMessagesSlice.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit';\nimport { TopicMessagesState } from 'redux/interfaces';\nimport { TopicMessage } from 'generated-sources';\n\nexport const initialState: TopicMessagesState = {\n  messages: [],\n  meta: {\n    bytesConsumed: 0,\n    elapsedMs: 0,\n    messagesConsumed: 0,\n    isCancelled: false,\n  },\n  messageEventType: '',\n  isFetching: false,\n};\n\nconst topicMessagesSlice = createSlice({\n  name: 'topicMessages',\n  initialState,\n  reducers: {\n    addTopicMessage: (state, action) => {\n      const messages: TopicMessage[] = action.payload.prepend\n        ? [action.payload.message, ...state.messages]\n        : [...state.messages, action.payload.message];\n\n      return {\n        ...state,\n        messages,\n      };\n    },\n    resetTopicMessages: () => initialState,\n    updateTopicMessagesPhase: (state, action) => {\n      state.phase = action.payload;\n    },\n    updateTopicMessagesMeta: (state, action) => {\n      state.meta = action.payload;\n    },\n    setTopicMessagesFetchingStatus: (state, action) => {\n      state.isFetching = action.payload;\n    },\n\n    setMessageEventType: (state, action) => {\n      state.messageEventType = action.payload;\n    },\n  },\n});\n\nexport const {\n  addTopicMessage,\n  resetTopicMessages,\n  updateTopicMessagesPhase,\n  updateTopicMessagesMeta,\n  setTopicMessagesFetchingStatus,\n  setMessageEventType,\n} = topicMessagesSlice.actions;\n\nexport default topicMessagesSlice.reducer;\n"
  },
  {
    "path": "kafka-ui-react-app/src/redux/store/index.ts",
    "content": "import { configureStore } from '@reduxjs/toolkit';\nimport { RootState } from 'redux/interfaces';\nimport rootReducer from 'redux/reducers';\n\nexport const store = configureStore<RootState>({\n  reducer: rootReducer,\n});\n"
  },
  {
    "path": "kafka-ui-react-app/src/setupTests.ts",
    "content": "import 'whatwg-fetch';\nimport 'jest-styled-components';\nimport '@testing-library/jest-dom/extend-expect';\nimport '@testing-library/jest-dom';\n"
  },
  {
    "path": "kafka-ui-react-app/src/styled.d.ts",
    "content": "import 'styled-components';\nimport { ThemeType } from 'theme/theme';\n\ndeclare module 'styled-components' {\n  // eslint-disable-next-line @typescript-eslint/no-empty-interface\n  export interface DefaultTheme extends ThemeType {}\n}\n"
  },
  {
    "path": "kafka-ui-react-app/src/theme/index.scss",
    "content": "@import \"./minireset.css\";\n"
  },
  {
    "path": "kafka-ui-react-app/src/theme/minireset.css",
    "content": "/*! 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}\n"
  },
  {
    "path": "kafka-ui-react-app/src/theme/theme.ts",
    "content": "const Colors = {\n  neutral: {\n    '0': '#FFFFFF',\n    '3': '#f9fafa',\n    '4': '#f0f0f0',\n    '5': '#F1F2F3',\n    '10': '#E3E6E8',\n    '15': '#D5DADD',\n    '20': '#C7CED1',\n    '25': '#C4C4C4',\n    '30': '#ABB5BA',\n    '40': '#8F9CA3',\n    '50': '#73848C',\n    '60': '#5C6970',\n    '70': '#454F54',\n    '75': '#394246',\n    '80': '#2F3639',\n    '85': '#22282A',\n    '87': '#1E2224',\n    '90': '#171A1C',\n    '95': '#0B0D0E',\n    '100': '#000',\n  },\n  transparency: {\n    '10': 'rgba(10, 10, 10, 0.1)',\n    '20': 'rgba(0, 0, 0, 0.1)',\n    '50': 'rgba(34, 41, 47, 0.5)',\n  },\n  green: {\n    '10': '#D6F5E0',\n    '15': '#C2F0D1',\n    '30': '#85E0A3',\n    '40': '#5CD685',\n    '50': '#33CC66',\n    '60': '#29A352',\n  },\n  brand: {\n    '5': '#E8E8FC',\n    '10': '#D1D1FA',\n    '15': '#B8BEF9',\n    '20': '#A3A3F5',\n    '30': '#7E7EF1',\n    '40': '#6666FF',\n    '50': '#4C4CFF',\n    '60': '#1717CF',\n    '70': '#1414B8',\n  },\n  red: {\n    '10': '#FAD1D1',\n    '20': '#F5A3A3',\n    '50': '#E51A1A',\n    '55': '#CF1717',\n    '60': '#B81414',\n  },\n  yellow: {\n    '10': '#FFEECC',\n    '20': '#FFDD57',\n  },\n  blue: {\n    '10': '#e3f2fd',\n    '20': '#bbdefb',\n    '30': '#90caf9',\n    '40': '#64b5f6',\n    '45': '#5865F2',\n    '50': '#5B67E3',\n    '60': '#7A7AB8',\n    '70': '#5959A6',\n    '80': '#3E3E74',\n  },\n};\n\nconst baseTheme = {\n  defaultIconColor: Colors.neutral[50],\n  heading: {\n    h1: {\n      color: Colors.neutral[90],\n    },\n    h3: {\n      color: Colors.neutral[50],\n      fontSize: '14px',\n    },\n    h4: Colors.neutral[90],\n    base: {\n      fontFamily: 'Inter, sans-serif',\n      fontStyle: 'normal',\n      fontWeight: 500,\n      color: Colors.neutral[100],\n    },\n    variants: {\n      1: {\n        fontSize: '20px',\n        lineHeight: '32px',\n      },\n      2: {\n        fontSize: '20px',\n        lineHeight: '32px',\n      },\n      3: {\n        fontSize: '16px',\n        lineHeight: '24px',\n        fontWeight: 400,\n        marginBottom: '16px',\n      },\n      4: {\n        fontSize: '14px',\n        lineHeight: '20px',\n        fontWeight: 500,\n      },\n      5: {\n        fontSize: '12px',\n        lineHeight: '16px',\n      },\n      6: {\n        fontSize: '12px',\n        lineHeight: '16px',\n      },\n    },\n  },\n  code: {\n    backgroundColor: Colors.neutral[5],\n    color: Colors.red[55],\n  },\n  layout: {\n    minWidth: '1200px',\n    navBarWidth: '201px',\n    navBarHeight: '51px',\n    rightSidebarWidth: '70vw',\n    filtersSidebarWidth: '300px',\n\n    stuffColor: Colors.neutral[5],\n    stuffBorderColor: Colors.neutral[10],\n    overlay: {\n      backgroundColor: Colors.neutral[50],\n    },\n    socialLink: Colors.neutral[20],\n  },\n  alert: {\n    color: {\n      error: Colors.red[10],\n      success: Colors.green[10],\n      warning: Colors.yellow[10],\n      info: Colors.neutral[10],\n      loading: Colors.neutral[10],\n      blank: Colors.neutral[10],\n      custom: Colors.neutral[10],\n    },\n    shadow: Colors.transparency[20],\n  },\n  circularAlert: {\n    color: {\n      error: Colors.red[50],\n      success: Colors.green[40],\n      warning: Colors.yellow[10],\n      info: Colors.neutral[10],\n    },\n  },\n  connectEditWarning: Colors.yellow[10],\n  lastestVersionItem: {\n    metaDataLabel: {\n      color: Colors.neutral[50],\n    },\n  },\n  icons: {\n    chevronDownIcon: Colors.neutral[0],\n    editIcon: {\n      normal: Colors.neutral[30],\n      hover: Colors.neutral[90],\n      active: Colors.neutral[100],\n      border: Colors.neutral[10],\n    },\n    closeIcon: {\n      normal: Colors.neutral[30],\n      hover: Colors.neutral[90],\n      active: Colors.neutral[100],\n      border: Colors.neutral[10],\n    },\n    cancelIcon: Colors.neutral[30],\n    autoIcon: Colors.neutral[95],\n    fileIcon: Colors.neutral[90],\n    clockIcon: Colors.neutral[90],\n    arrowDownIcon: Colors.neutral[90],\n    moonIcon: Colors.neutral[95],\n    sunIcon: Colors.neutral[95],\n    infoIcon: Colors.neutral[30],\n    closeCircleIcon: Colors.neutral[30],\n    deleteIcon: Colors.red[20],\n    warningIcon: Colors.yellow[20],\n    warningRedIcon: {\n      rectFill: Colors.red[10],\n      pathFill: Colors.red[50],\n    },\n    messageToggleIcon: {\n      normal: Colors.brand[30],\n      hover: Colors.brand[40],\n      active: Colors.brand[50],\n    },\n    verticalElipsisIcon: Colors.neutral[50],\n    liveIcon: {\n      circleBig: Colors.red[10],\n      circleSmall: Colors.red[50],\n    },\n    newFilterIcon: Colors.brand[50],\n    closeModalIcon: Colors.neutral[25],\n    savedIcon: Colors.brand[50],\n    dropdownArrowIcon: Colors.neutral[50],\n    git: {\n      hover: Colors.neutral[90],\n      active: Colors.neutral[70],\n    },\n    discord: {\n      normal: Colors.neutral[20],\n      hover: Colors.blue[45],\n      active: Colors.brand[15],\n    },\n  },\n  textArea: {\n    borderColor: {\n      normal: Colors.neutral[30],\n      hover: Colors.neutral[50],\n      focus: Colors.neutral[70],\n      disabled: Colors.neutral[10],\n    },\n    color: {\n      placeholder: {\n        normal: Colors.neutral[30],\n        focus: {\n          normal: 'transparent',\n          readOnly: Colors.neutral[30],\n        },\n      },\n      disabled: Colors.neutral[30],\n      readOnly: Colors.neutral[90],\n    },\n    backgroundColor: {\n      readOnly: Colors.neutral[5],\n    },\n  },\n  tag: {\n    backgroundColor: {\n      green: Colors.green[10],\n      gray: Colors.neutral[5],\n      yellow: Colors.yellow[10],\n      white: Colors.neutral[10],\n      red: Colors.red[10],\n      blue: Colors.blue[10],\n      secondary: Colors.neutral[15],\n    },\n    color: Colors.neutral[90],\n  },\n  switch: {\n    unchecked: Colors.neutral[20],\n    hover: Colors.neutral[40],\n    checked: Colors.brand[50],\n    circle: Colors.neutral[0],\n    disabled: Colors.neutral[10],\n    checkedIcon: {\n      backgroundColor: Colors.neutral[10],\n    },\n  },\n  pageLoader: {\n    borderColor: Colors.brand[50],\n    borderBottomColor: Colors.neutral[0],\n  },\n  topicFormLabel: {\n    color: Colors.neutral[50],\n  },\n  dangerZone: {\n    borderColor: Colors.red[60],\n    color: {\n      title: Colors.red[50],\n      warningMessage: Colors.neutral[50],\n    },\n  },\n  configList: {\n    color: Colors.neutral[30],\n  },\n  tooltip: {\n    bg: Colors.neutral[80],\n    text: Colors.neutral[0],\n  },\n  topicsList: {\n    color: {\n      normal: Colors.neutral[90],\n      hover: Colors.neutral[50],\n      active: Colors.neutral[90],\n    },\n    backgroundColor: {\n      hover: Colors.neutral[5],\n      active: Colors.neutral[10],\n    },\n  },\n  statictics: {\n    createdAtColor: Colors.neutral[50],\n    progressPctColor: Colors.neutral[100],\n  },\n  progressBar: {\n    backgroundColor: Colors.neutral[3],\n    compleatedColor: Colors.green[40],\n    borderColor: Colors.neutral[10],\n  },\n  clusterConfigForm: {\n    inputHintText: {\n      secondary: Colors.neutral[60],\n    },\n    groupField: {\n      backgroundColor: Colors.neutral[3],\n    },\n    fileInput: {\n      color: Colors.neutral[85],\n    },\n  },\n};\n\nexport const theme = {\n  ...baseTheme,\n  version: {\n    currentVersion: {\n      color: Colors.neutral[30],\n    },\n    commitLink: {\n      color: Colors.brand[50],\n    },\n  },\n  default: {\n    color: {\n      normal: Colors.neutral[90],\n    },\n    backgroundColor: Colors.neutral[0],\n    transparentColor: 'transparent',\n  },\n  link: {\n    color: Colors.brand[50],\n    hoverColor: Colors.brand[60],\n  },\n  hr: {\n    backgroundColor: Colors.neutral[5],\n  },\n  pageHeading: {\n    height: '64px',\n    dividerColor: Colors.neutral[30],\n    backLink: {\n      color: {\n        normal: Colors.brand[70],\n        hover: Colors.brand[60],\n      },\n    },\n  },\n  panelColor: {\n    borderTop: 'none',\n  },\n  dropdown: {\n    backgroundColor: Colors.neutral[0],\n    borderColor: Colors.neutral[5],\n    shadow: Colors.transparency[20],\n    item: {\n      color: {\n        normal: Colors.neutral[90],\n        danger: Colors.red[60],\n      },\n      backgroundColor: {\n        default: Colors.neutral[0],\n        hover: Colors.neutral[5],\n      },\n    },\n  },\n  ksqlDb: {\n    query: {\n      editor: {\n        readonly: {\n          background: Colors.neutral[3],\n        },\n        activeLine: {\n          backgroundColor: Colors.neutral[5],\n        },\n        cell: {\n          backgroundColor: Colors.neutral[10],\n        },\n        layer: {\n          backgroundColor: Colors.neutral[5],\n        },\n        cursor: Colors.neutral[90],\n        variable: Colors.red[50],\n        aceString: Colors.green[60],\n        codeMarker: Colors.yellow[20],\n      },\n    },\n  },\n  button: {\n    primary: {\n      backgroundColor: {\n        normal: Colors.brand[50],\n        hover: Colors.brand[60],\n        active: Colors.brand[70],\n        disabled: Colors.neutral[5],\n      },\n      color: {\n        normal: Colors.neutral[0],\n        disabled: Colors.neutral[30],\n      },\n      invertedColors: {\n        normal: Colors.brand[50],\n        hover: Colors.brand[60],\n        active: Colors.brand[60],\n      },\n    },\n    secondary: {\n      backgroundColor: {\n        normal: Colors.brand[5],\n        hover: Colors.brand[10],\n        active: Colors.brand[30],\n        disabled: Colors.neutral[5],\n      },\n      color: {\n        normal: Colors.neutral[90],\n        disabled: Colors.neutral[30],\n      },\n      isActiveColor: Colors.neutral[0],\n      invertedColors: {\n        normal: Colors.neutral[50],\n        hover: Colors.neutral[70],\n        active: Colors.neutral[90],\n        disabled: Colors.neutral[75],\n      },\n    },\n    danger: {\n      backgroundColor: {\n        normal: Colors.red[50],\n        hover: Colors.red[55],\n        active: Colors.red[60],\n        disabled: Colors.red[20],\n      },\n      color: {\n        normal: Colors.neutral[0],\n        disabled: Colors.neutral[0],\n      },\n      invertedColors: {\n        normal: Colors.brand[50],\n        hover: Colors.brand[60],\n        active: Colors.brand[60],\n      },\n    },\n    height: {\n      S: '24px',\n      M: '32px',\n      L: '40px',\n    },\n    fontSize: {\n      S: '14px',\n      M: '14px',\n      L: '16px',\n    },\n    border: {\n      normal: Colors.neutral[50],\n      hover: Colors.neutral[70],\n      active: Colors.neutral[90],\n    },\n  },\n  chips: {\n    backgroundColor: {\n      normal: Colors.neutral[5],\n      hover: Colors.neutral[10],\n      active: Colors.neutral[50],\n      hoverActive: Colors.neutral[60],\n    },\n    color: {\n      normal: Colors.neutral[70],\n      hover: Colors.neutral[70],\n      active: Colors.neutral[0],\n      hoverActive: Colors.neutral[0],\n    },\n  },\n  menu: {\n    backgroundColor: {\n      normal: Colors.neutral[0],\n      hover: Colors.neutral[3],\n      active: Colors.neutral[5],\n    },\n    color: {\n      normal: Colors.neutral[50],\n      hover: Colors.neutral[70],\n      active: Colors.brand[70],\n      isOpen: Colors.neutral[90],\n    },\n    statusIconColor: {\n      online: Colors.green[40],\n      offline: Colors.red[50],\n      initializing: Colors.yellow[20],\n    },\n    chevronIconColor: Colors.neutral[50],\n    titleColor: Colors.neutral[90],\n  },\n  schema: {\n    backgroundColor: {\n      tr: Colors.neutral[5],\n      div: Colors.neutral[0],\n      p: Colors.neutral[80],\n      textarea: Colors.neutral[3],\n    },\n  },\n  modal: {\n    color: Colors.neutral[80],\n    backgroundColor: Colors.neutral[0],\n    border: {\n      top: Colors.neutral[5],\n      bottom: Colors.neutral[5],\n      contrast: Colors.neutral[30],\n    },\n    overlay: Colors.transparency[10],\n    shadow: Colors.transparency[20],\n    contentColor: Colors.neutral[70],\n  },\n  confirmModal: {\n    backgroundColor: Colors.neutral[0],\n  },\n  table: {\n    actionBar: {\n      backgroundColor: Colors.neutral[0],\n    },\n    th: {\n      backgroundColor: {\n        normal: Colors.neutral[0],\n      },\n      color: {\n        sortable: Colors.neutral[30],\n        normal: Colors.neutral[60],\n        hover: Colors.brand[50],\n        active: Colors.brand[50],\n      },\n      previewColor: {\n        normal: Colors.brand[50],\n      },\n    },\n    td: {\n      borderTop: Colors.neutral[5],\n      color: {\n        normal: Colors.neutral[90],\n      },\n    },\n    tr: {\n      backgroundColor: {\n        normal: Colors.neutral[0],\n        hover: Colors.neutral[5],\n      },\n    },\n    link: {\n      color: {\n        normal: Colors.neutral[90],\n        hover: Colors.neutral[50],\n        active: Colors.neutral[90],\n      },\n    },\n    colored: {\n      color: {\n        attention: Colors.red[50],\n        warning: Colors.yellow[20],\n      },\n    },\n    expander: {\n      normal: Colors.brand[30],\n      hover: Colors.brand[40],\n      active: Colors.brand[50],\n      disabled: Colors.neutral[10],\n    },\n    pagination: {\n      button: {\n        background: Colors.neutral[90],\n        border: Colors.neutral[80],\n      },\n      info: Colors.neutral[90],\n    },\n  },\n  primaryTab: {\n    height: '41px',\n    color: {\n      normal: Colors.neutral[50],\n      hover: Colors.neutral[90],\n      active: Colors.neutral[90],\n      disabled: Colors.neutral[20],\n    },\n    borderColor: {\n      active: Colors.brand[50],\n      nav: Colors.neutral[5],\n    },\n  },\n  secondaryTab: {\n    backgroundColor: {\n      normal: Colors.neutral[0],\n      hover: Colors.neutral[5],\n      active: Colors.neutral[10],\n    },\n    color: {\n      normal: Colors.neutral[50],\n      hover: Colors.neutral[90],\n      active: Colors.neutral[90],\n    },\n  },\n  select: {\n    backgroundColor: {\n      normal: Colors.neutral[0],\n      hover: Colors.neutral[10],\n      active: Colors.neutral[10],\n    },\n    color: {\n      normal: Colors.neutral[90],\n      hover: Colors.neutral[90],\n      active: Colors.neutral[90],\n      disabled: Colors.neutral[30],\n    },\n    borderColor: {\n      normal: Colors.neutral[30],\n      hover: Colors.neutral[50],\n      active: Colors.neutral[70],\n      disabled: Colors.neutral[10],\n    },\n    optionList: {\n      scrollbar: {\n        backgroundColor: Colors.neutral[30],\n      },\n    },\n    label: Colors.neutral[50],\n  },\n  input: {\n    borderColor: {\n      normal: Colors.neutral[30],\n      hover: Colors.neutral[50],\n      focus: Colors.neutral[70],\n      disabled: Colors.neutral[10],\n    },\n    color: {\n      normal: Colors.neutral[90],\n      placeholder: {\n        normal: Colors.neutral[30],\n        readOnly: Colors.neutral[30],\n      },\n      disabled: Colors.neutral[30],\n      readOnly: Colors.neutral[90],\n    },\n    backgroundColor: {\n      normal: Colors.neutral[0],\n      readOnly: Colors.neutral[5],\n      disabled: Colors.neutral[0],\n    },\n    error: Colors.red[50],\n    icon: {\n      color: Colors.neutral[70],\n      hover: Colors.neutral[90],\n    },\n    label: {\n      color: Colors.neutral[70],\n    },\n  },\n  metrics: {\n    backgroundColor: Colors.neutral[5],\n    sectionTitle: Colors.neutral[90],\n    indicator: {\n      titleColor: Colors.neutral[50],\n      warningTextColor: Colors.red[50],\n      lightTextColor: Colors.neutral[30],\n    },\n    wrapper: Colors.neutral[0],\n    filters: {\n      color: {\n        icon: Colors.neutral[90],\n        normal: Colors.neutral[50],\n      },\n    },\n  },\n  scrollbar: {\n    trackColor: {\n      normal: Colors.neutral[0],\n      active: Colors.neutral[5],\n    },\n    thumbColor: {\n      normal: Colors.neutral[0],\n      active: Colors.neutral[50],\n    },\n  },\n  consumerTopicContent: {\n    td: {\n      backgroundColor: Colors.neutral[5],\n    },\n  },\n  topicMetaData: {\n    backgroundColor: Colors.neutral[5],\n    color: {\n      label: Colors.neutral[50],\n      value: Colors.neutral[80],\n      meta: Colors.neutral[30],\n    },\n    liderReplica: {\n      color: Colors.green[60],\n    },\n    outOfSync: {\n      color: Colors.red[50],\n    },\n  },\n  viewer: {\n    wrapper: {\n      backgroundColor: Colors.neutral[3],\n      color: Colors.neutral[80],\n    },\n  },\n  activeFilter: {\n    color: Colors.neutral[70],\n    backgroundColor: Colors.neutral[5],\n  },\n  savedFilter: {\n    filterName: Colors.neutral[90],\n    color: Colors.neutral[30],\n  },\n  editFilter: {\n    textColor: Colors.brand[50],\n    deleteIconColor: Colors.brand[50],\n  },\n  acl: {\n    table: {\n      deleteIcon: Colors.neutral[50],\n    },\n    create: {\n      radioButtons: {\n        green: {\n          normal: {\n            background: Colors.neutral[0],\n            text: Colors.neutral[50],\n          },\n          active: {\n            background: Colors.green[50],\n            text: Colors.neutral[0],\n          },\n          hover: {\n            background: Colors.green[10],\n            text: Colors.neutral[90],\n          },\n        },\n        gray: {\n          normal: {\n            background: Colors.neutral[0],\n            text: Colors.neutral[50],\n          },\n          active: {\n            background: Colors.neutral[10],\n            text: Colors.neutral[90],\n          },\n          hover: {\n            background: Colors.neutral[5],\n            text: Colors.neutral[90],\n          },\n        },\n        red: {},\n      },\n    },\n  },\n};\n\nexport type ThemeType = typeof theme;\n\nexport const darkTheme: ThemeType = {\n  ...baseTheme,\n  version: {\n    currentVersion: {\n      color: Colors.neutral[50],\n    },\n    commitLink: {\n      color: Colors.brand[30],\n    },\n  },\n  default: {\n    color: {\n      normal: Colors.neutral[0],\n    },\n    backgroundColor: Colors.neutral[90],\n    transparentColor: 'transparent',\n  },\n  link: {\n    color: Colors.brand[50],\n    hoverColor: Colors.brand[30],\n  },\n  hr: {\n    backgroundColor: Colors.neutral[80],\n  },\n  pageHeading: {\n    height: '64px',\n    dividerColor: Colors.neutral[50],\n    backLink: {\n      color: {\n        normal: Colors.brand[30],\n        hover: Colors.brand[15],\n      },\n    },\n  },\n  panelColor: {\n    borderTop: Colors.neutral[80],\n  },\n  dropdown: {\n    backgroundColor: Colors.neutral[85],\n    borderColor: Colors.neutral[80],\n    shadow: Colors.transparency[20],\n    item: {\n      color: {\n        normal: Colors.neutral[0],\n        danger: Colors.red[60],\n      },\n      backgroundColor: {\n        default: Colors.neutral[85],\n        hover: Colors.neutral[80],\n      },\n    },\n  },\n  ksqlDb: {\n    query: {\n      editor: {\n        readonly: {\n          background: Colors.neutral[3],\n        },\n        activeLine: {\n          backgroundColor: Colors.neutral[80],\n        },\n        cell: {\n          backgroundColor: Colors.neutral[75],\n        },\n        layer: {\n          backgroundColor: Colors.neutral[80],\n        },\n        cursor: Colors.neutral[0],\n        variable: Colors.red[50],\n        aceString: Colors.green[60],\n        codeMarker: Colors.yellow[20],\n      },\n    },\n  },\n  button: {\n    primary: {\n      backgroundColor: {\n        normal: Colors.brand[30],\n        hover: Colors.brand[20],\n        active: Colors.brand[10],\n        disabled: Colors.neutral[75],\n      },\n      color: {\n        normal: Colors.neutral[0],\n        disabled: Colors.neutral[60],\n      },\n      invertedColors: {\n        normal: Colors.brand[30],\n        hover: Colors.brand[60],\n        active: Colors.brand[60],\n      },\n    },\n    secondary: {\n      backgroundColor: {\n        normal: Colors.blue[80],\n        hover: Colors.blue[70],\n        active: Colors.blue[60],\n        disabled: Colors.neutral[75],\n      },\n      color: {\n        normal: Colors.neutral[0],\n        disabled: Colors.neutral[60],\n      },\n      isActiveColor: Colors.neutral[90],\n      invertedColors: {\n        normal: Colors.neutral[50],\n        hover: Colors.neutral[70],\n        active: Colors.neutral[90],\n        disabled: Colors.neutral[75],\n      },\n    },\n    danger: {\n      backgroundColor: {\n        normal: Colors.red[50],\n        hover: Colors.red[55],\n        active: Colors.red[60],\n        disabled: Colors.red[20],\n      },\n      color: {\n        normal: Colors.neutral[0],\n        disabled: Colors.neutral[0],\n      },\n      invertedColors: {\n        normal: Colors.brand[50],\n        hover: Colors.brand[60],\n        active: Colors.brand[60],\n      },\n    },\n    height: {\n      S: '24px',\n      M: '32px',\n      L: '40px',\n    },\n    fontSize: {\n      S: '14px',\n      M: '14px',\n      L: '16px',\n    },\n    border: {\n      normal: Colors.neutral[50],\n      hover: Colors.neutral[70],\n      active: Colors.neutral[90],\n    },\n  },\n  chips: {\n    backgroundColor: {\n      normal: Colors.neutral[80],\n      hover: Colors.neutral[70],\n      active: Colors.neutral[50],\n      hoverActive: Colors.neutral[40],\n    },\n    color: {\n      normal: Colors.neutral[0],\n      hover: Colors.neutral[0],\n      active: Colors.neutral[90],\n      hoverActive: Colors.neutral[90],\n    },\n  },\n  menu: {\n    backgroundColor: {\n      normal: Colors.neutral[90],\n      hover: Colors.neutral[87],\n      active: Colors.neutral[85],\n    },\n    color: {\n      normal: Colors.neutral[40],\n      hover: Colors.neutral[20],\n      active: Colors.brand[20],\n      isOpen: Colors.neutral[90],\n    },\n    statusIconColor: {\n      online: Colors.green[40],\n      offline: Colors.red[50],\n      initializing: Colors.yellow[20],\n    },\n    chevronIconColor: Colors.neutral[50],\n    titleColor: Colors.neutral[0],\n  },\n  schema: {\n    backgroundColor: {\n      tr: Colors.neutral[5],\n      div: Colors.neutral[0],\n      p: Colors.neutral[0],\n      textarea: Colors.neutral[85],\n    },\n  },\n  modal: {\n    color: Colors.neutral[0],\n    backgroundColor: Colors.neutral[85],\n    border: {\n      top: Colors.neutral[75],\n      bottom: Colors.neutral[75],\n      contrast: Colors.neutral[75],\n    },\n    overlay: Colors.transparency[10],\n    shadow: Colors.transparency[20],\n    contentColor: Colors.neutral[30],\n  },\n  confirmModal: {\n    backgroundColor: Colors.neutral[80],\n  },\n  table: {\n    actionBar: {\n      backgroundColor: Colors.neutral[90],\n    },\n    th: {\n      backgroundColor: {\n        normal: Colors.neutral[90],\n      },\n      color: {\n        sortable: Colors.neutral[30],\n        normal: Colors.neutral[60],\n        hover: Colors.brand[50],\n        active: Colors.brand[50],\n      },\n      previewColor: {\n        normal: Colors.brand[50],\n      },\n    },\n    td: {\n      borderTop: Colors.neutral[80],\n      color: {\n        normal: Colors.neutral[0],\n      },\n    },\n    tr: {\n      backgroundColor: {\n        normal: Colors.neutral[90],\n        hover: Colors.neutral[85],\n      },\n    },\n    link: {\n      color: {\n        normal: Colors.neutral[0],\n        hover: Colors.neutral[0],\n        active: Colors.neutral[0],\n      },\n    },\n    colored: {\n      color: {\n        attention: Colors.red[50],\n        warning: Colors.yellow[20],\n      },\n    },\n    expander: {\n      normal: Colors.brand[30],\n      hover: Colors.brand[40],\n      active: Colors.brand[50],\n      disabled: Colors.neutral[10],\n    },\n    pagination: {\n      button: {\n        background: Colors.neutral[90],\n        border: Colors.neutral[80],\n      },\n      info: Colors.neutral[0],\n    },\n  },\n  primaryTab: {\n    height: '41px',\n    color: {\n      normal: Colors.neutral[50],\n      hover: Colors.neutral[0],\n      active: Colors.brand[30],\n      disabled: Colors.neutral[75],\n    },\n    borderColor: {\n      active: Colors.brand[50],\n      nav: Colors.neutral[80],\n    },\n  },\n  secondaryTab: {\n    backgroundColor: {\n      normal: Colors.neutral[90],\n      hover: Colors.neutral[85],\n      active: Colors.neutral[80],\n    },\n    color: {\n      normal: Colors.neutral[50],\n      hover: Colors.neutral[0],\n      active: Colors.neutral[0],\n    },\n  },\n  select: {\n    backgroundColor: {\n      normal: Colors.neutral[85],\n      hover: Colors.neutral[80],\n      active: Colors.neutral[70],\n    },\n    color: {\n      normal: Colors.neutral[0],\n      hover: Colors.neutral[0],\n      active: Colors.neutral[0],\n      disabled: Colors.neutral[60],\n    },\n    borderColor: {\n      normal: Colors.neutral[70],\n      hover: Colors.neutral[50],\n      active: Colors.neutral[70],\n      disabled: Colors.neutral[70],\n    },\n    optionList: {\n      scrollbar: {\n        backgroundColor: Colors.neutral[30],\n      },\n    },\n    label: Colors.neutral[50],\n  },\n  input: {\n    borderColor: {\n      normal: Colors.neutral[70],\n      hover: Colors.neutral[50],\n      focus: Colors.neutral[0],\n      disabled: Colors.neutral[80],\n    },\n    color: {\n      normal: Colors.neutral[0],\n      placeholder: {\n        normal: Colors.neutral[60],\n        readOnly: Colors.neutral[0],\n      },\n      disabled: Colors.neutral[80],\n      readOnly: Colors.neutral[0],\n    },\n    backgroundColor: {\n      normal: Colors.neutral[90],\n      readOnly: Colors.neutral[80],\n      disabled: Colors.neutral[90],\n    },\n    error: Colors.red[50],\n    icon: {\n      color: Colors.neutral[30],\n      hover: Colors.neutral[0],\n    },\n    label: {\n      color: Colors.neutral[30],\n    },\n  },\n  metrics: {\n    backgroundColor: Colors.neutral[95],\n    sectionTitle: Colors.neutral[0],\n    indicator: {\n      titleColor: Colors.neutral[0],\n      warningTextColor: Colors.red[50],\n      lightTextColor: Colors.neutral[60],\n    },\n    wrapper: Colors.neutral[0],\n    filters: {\n      color: {\n        icon: Colors.neutral[0],\n        normal: Colors.neutral[50],\n      },\n    },\n  },\n  scrollbar: {\n    trackColor: {\n      normal: Colors.neutral[90],\n      active: Colors.neutral[85],\n    },\n    thumbColor: {\n      normal: Colors.neutral[75],\n      active: Colors.neutral[50],\n    },\n  },\n  consumerTopicContent: {\n    td: {\n      backgroundColor: Colors.neutral[95],\n    },\n  },\n  topicMetaData: {\n    backgroundColor: Colors.neutral[90],\n    color: {\n      label: Colors.neutral[50],\n      value: Colors.neutral[0],\n      meta: Colors.neutral[60],\n    },\n    liderReplica: {\n      color: Colors.green[60],\n    },\n    outOfSync: {\n      color: Colors.red[50],\n    },\n  },\n  viewer: {\n    wrapper: {\n      backgroundColor: Colors.neutral[85],\n      color: Colors.neutral[0],\n    },\n  },\n  activeFilter: {\n    color: Colors.neutral[0],\n    backgroundColor: Colors.neutral[80],\n  },\n  savedFilter: {\n    filterName: Colors.neutral[0],\n    color: Colors.neutral[70],\n  },\n  editFilter: {\n    textColor: Colors.brand[30],\n    deleteIconColor: Colors.brand[30],\n  },\n  heading: {\n    ...baseTheme.heading,\n    h4: Colors.neutral[0],\n    base: {\n      ...baseTheme.heading.base,\n      color: Colors.neutral[0],\n    },\n  },\n  code: {\n    ...baseTheme.code,\n    backgroundColor: Colors.neutral[95],\n  },\n  layout: {\n    ...baseTheme.layout,\n    stuffColor: Colors.neutral[75],\n    stuffBorderColor: Colors.neutral[75],\n    socialLink: Colors.neutral[30],\n  },\n  icons: {\n    ...baseTheme.icons,\n    editIcon: {\n      normal: Colors.neutral[50],\n      hover: Colors.neutral[30],\n      active: Colors.neutral[40],\n      border: Colors.neutral[70],\n    },\n    closeIcon: {\n      normal: Colors.neutral[50],\n      hover: Colors.neutral[30],\n      active: Colors.neutral[40],\n      border: Colors.neutral[70],\n    },\n    cancelIcon: Colors.neutral[0],\n    autoIcon: Colors.neutral[0],\n    fileIcon: Colors.neutral[0],\n    clockIcon: Colors.neutral[0],\n    arrowDownIcon: Colors.neutral[0],\n    moonIcon: Colors.neutral[0],\n    sunIcon: Colors.neutral[0],\n    infoIcon: Colors.neutral[70],\n    savedIcon: Colors.brand[30],\n    git: {\n      ...baseTheme.icons.git,\n      hover: Colors.neutral[70],\n      active: Colors.neutral[90],\n    },\n    discord: {\n      ...baseTheme.icons.discord,\n      normal: Colors.neutral[30],\n    },\n  },\n  textArea: {\n    ...baseTheme.textArea,\n    borderColor: {\n      ...baseTheme.textArea.borderColor,\n      normal: Colors.neutral[70],\n      hover: Colors.neutral[30],\n      focus: Colors.neutral[0],\n    },\n  },\n  clusterConfigForm: {\n    ...baseTheme.clusterConfigForm,\n    groupField: {\n      backgroundColor: Colors.neutral[85],\n    },\n    fileInput: {\n      color: Colors.neutral[0],\n    },\n  },\n  acl: {\n    table: {\n      deleteIcon: Colors.neutral[50],\n    },\n    create: {\n      radioButtons: {\n        green: {\n          normal: {\n            background: Colors.neutral[0],\n            text: Colors.neutral[50],\n          },\n          active: {\n            background: Colors.green[50],\n            text: Colors.neutral[0],\n          },\n          hover: {\n            background: Colors.green[10],\n            text: Colors.neutral[0],\n          },\n        },\n        gray: {\n          normal: {\n            background: Colors.neutral[0],\n            text: Colors.neutral[50],\n          },\n          active: {\n            background: Colors.neutral[10],\n            text: Colors.neutral[90],\n          },\n          hover: {\n            background: Colors.neutral[5],\n            text: Colors.neutral[90],\n          },\n        },\n        red: {},\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/ClusterConfigForm.styled.ts",
    "content": "import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';\nimport styled from 'styled-components';\n\nexport const GroupFieldWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  gap: 8px;\n  background-color: ${({ theme }) =>\n    theme.clusterConfigForm.groupField.backgroundColor};\n  padding: 8px;\n  border-radius: 4px;\n  box-shadow: 0 1px 2px 0 rgb(0 0 0 / 15%);\n\n  hr {\n    margin: 10px 0 5px;\n  }\n`;\nconst InputContainer = styled.div`\n  display: grid;\n  grid-template-columns: 1fr 1fr 30px;\n  gap: 8px;\n  align-items: stretch;\n  max-width: 500px;\n`;\nexport const ButtonWrapper = styled.div`\n  display: flex;\n  gap: 10px;\n`;\nexport const RemoveButton = styled(IconButtonWrapper)`\n  align-self: center;\n`;\nexport const FlexRow = styled.div`\n  display: flex;\n  flex-direction: row;\n  gap: 8px;\n  align-items: flex-start;\n`;\nexport const FlexGrow1 = styled.div`\n  flex-grow: 1;\n  row-gap: 8px;\n  flex-direction: column;\n  display: flex;\n`;\n// KafkaCluster\nexport const BootstrapServer = styled(InputContainer)`\n  grid-template-columns: 3fr 110px 30px;\n`;\nexport const BootstrapServerActions = styled(IconButtonWrapper)`\n  align-self: stretch;\n  margin-top: 12px;\n  margin-left: 8px;\n`;\nexport const Port = styled.div`\n  width: 110px;\n`;\n\nexport const FileUploadInputWrapper = styled.div`\n  display: flex;\n  height: 40px;\n  align-items: center;\n  color: ${({ theme }) => theme.clusterConfigForm.fileInput.color}};\n`;\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/Authentication/Authentication.tsx",
    "content": "import React from 'react';\nimport { useFormContext } from 'react-hook-form';\nimport { AUTH_OPTIONS, SECURITY_PROTOCOL_OPTIONS } from 'lib/constants';\nimport ControlledSelect from 'components/common/Select/ControlledSelect';\nimport SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader';\n\nimport AuthenticationMethods from './AuthenticationMethods';\n\nconst Authentication: React.FC = () => {\n  const { watch, setValue } = useFormContext();\n  const hasAuth = !!watch('auth');\n  const authMethod = watch('auth.method');\n  const hasSecurityProtocolField =\n    authMethod && !['Delegation tokens', 'mTLS'].includes(authMethod);\n\n  const toggle = () =>\n    setValue('auth', hasAuth ? undefined : {}, {\n      shouldValidate: true,\n      shouldDirty: true,\n      shouldTouch: true,\n    });\n\n  return (\n    <>\n      <SectionHeader\n        title=\"Authentication\"\n        adding={!hasAuth}\n        addButtonText=\"Configure Authentication\"\n        onClick={toggle}\n      />\n      {hasAuth && (\n        <>\n          <ControlledSelect\n            name=\"auth.method\"\n            label=\"Authentication Method\"\n            placeholder=\"Select authentication method\"\n            options={AUTH_OPTIONS}\n          />\n          {hasSecurityProtocolField && (\n            <ControlledSelect\n              name=\"auth.securityProtocol\"\n              label=\"Security Protocol\"\n              placeholder=\"Select security protocol\"\n              options={SECURITY_PROTOCOL_OPTIONS}\n            />\n          )}\n          <AuthenticationMethods method={authMethod} />\n        </>\n      )}\n    </>\n  );\n};\n\nexport default Authentication;\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/Authentication/AuthenticationMethods.tsx",
    "content": "import React from 'react';\nimport Input from 'components/common/Input/Input';\nimport Checkbox from 'components/common/Checkbox/Checkbox';\nimport Fileupload from 'widgets/ClusterConfigForm/common/Fileupload';\nimport SSLForm from 'widgets/ClusterConfigForm/common/SSLForm';\nimport Credentials from 'widgets/ClusterConfigForm/common/Credentials';\n\nconst AuthenticationMethods: React.FC<{ method: string }> = ({ method }) => {\n  switch (method) {\n    case 'SASL/JAAS':\n      return (\n        <>\n          <Input\n            type=\"text\"\n            name=\"auth.props.saslJaasConfig\"\n            label=\"sasl.jaas.config\"\n            withError\n          />\n          <Input\n            type=\"text\"\n            name=\"auth.props.saslMechanism\"\n            label=\"sasl.mechanism\"\n            withError\n          />\n        </>\n      );\n    case 'SASL/GSSAPI':\n      return (\n        <>\n          <Input\n            label=\"Kerberos service name\"\n            type=\"text\"\n            name=\"auth.props.saslKerberosServiceName\"\n            withError\n          />\n          <Checkbox name=\"auth.props.storeKey\" label=\"Store Key\" />\n          <Fileupload name=\"auth.props.keyTabFile\" label=\"Key Tab (optional)\" />\n          <Input\n            type=\"text\"\n            name=\"auth.props.principal\"\n            label=\"Principal *\"\n            withError\n          />\n        </>\n      );\n    case 'SASL/OAUTHBEARER':\n      return (\n        <Input\n          label=\"Unsecured Login String Claim_sub *\"\n          type=\"text\"\n          name=\"auth.props.unsecuredLoginStringClaim_sub\"\n          withError\n        />\n      );\n    case 'SASL/PLAIN':\n    case 'SASL/SCRAM-256':\n    case 'SASL/SCRAM-512':\n    case 'SASL/LDAP':\n      return <Credentials prefix=\"auth.props\" />;\n    case 'Delegation tokens':\n      return (\n        <>\n          <Input\n            label=\"Token Id\"\n            type=\"text\"\n            name=\"auth.props.tokenId\"\n            withError\n          />\n          <Input\n            label=\"Token Value *\"\n            type=\"text\"\n            name=\"auth.props.tokenValue\"\n            withError\n          />\n        </>\n      );\n    case 'SASL/AWS IAM':\n      return (\n        <Input\n          label=\"AWS Profile Name\"\n          type=\"text\"\n          name=\"auth.props.awsProfileName\"\n          withError\n        />\n      );\n    case 'mTLS':\n      return <SSLForm prefix=\"auth.keystore\" title=\"Keystore\" />;\n    default:\n      return null;\n  }\n};\n\nexport default AuthenticationMethods;\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/CustomAuthentication.tsx",
    "content": "import React from 'react';\nimport { useFormContext } from 'react-hook-form';\nimport Input from 'components/common/Input/Input';\nimport { convertFormKeyToPropsKey } from 'widgets/ClusterConfigForm/utils/convertFormKeyToPropsKey';\nimport SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader';\n\nconst CustomAuthentication: React.FC = () => {\n  const { watch, setValue } = useFormContext();\n  const customConf = watch('customAuth');\n  const hasCustomConfig =\n    customConf && Object.values(customConf).some((v) => !!v);\n\n  const remove = () =>\n    setValue('customAuth', undefined, {\n      shouldValidate: true,\n      shouldDirty: true,\n      shouldTouch: true,\n    });\n  return (\n    <>\n      <SectionHeader\n        title=\"Authentication\"\n        addButtonText=\"Configure Authentication\"\n        onClick={remove}\n      />\n      {hasCustomConfig && (\n        <>\n          {Object.keys(customConf).map((key) => (\n            <Input\n              key={key}\n              type=\"text\"\n              name={`customAuth.${key}`}\n              label={convertFormKeyToPropsKey(key)}\n              withError\n            />\n          ))}\n        </>\n      )}\n    </>\n  );\n};\n\nexport default CustomAuthentication;\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KSQL.tsx",
    "content": "import React from 'react';\nimport Input from 'components/common/Input/Input';\nimport { useFormContext } from 'react-hook-form';\nimport SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader';\nimport SSLForm from 'widgets/ClusterConfigForm/common/SSLForm';\nimport Credentials from 'widgets/ClusterConfigForm/common/Credentials';\n\nconst KSQL = () => {\n  const { setValue, watch } = useFormContext();\n  const ksql = watch('ksql');\n  const toggleConfig = () => {\n    setValue('ksql', ksql ? undefined : { url: '', isAuth: false }, {\n      shouldValidate: true,\n      shouldDirty: true,\n      shouldTouch: true,\n    });\n  };\n  return (\n    <>\n      <SectionHeader\n        title=\"KSQL DB\"\n        adding={!ksql}\n        addButtonText=\"Configure KSQL DB\"\n        onClick={toggleConfig}\n      />\n      {ksql && (\n        <>\n          <Input\n            label=\"URL *\"\n            name=\"ksql.url\"\n            type=\"text\"\n            placeholder=\"http://localhost:8088\"\n            withError\n          />\n          <Credentials prefix=\"ksql\" title=\"Is KSQL DB secured with auth?\" />\n          <SSLForm prefix=\"ksql.keystore\" title=\"KSQL DB Keystore\" />\n        </>\n      )}\n    </>\n  );\n};\nexport default KSQL;\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KafkaCluster.tsx",
    "content": "import React from 'react';\nimport Input from 'components/common/Input/Input';\nimport { useFieldArray, useFormContext } from 'react-hook-form';\nimport { FormError, InputHint } from 'components/common/Input/Input.styled';\nimport { ErrorMessage } from '@hookform/error-message';\nimport CloseCircleIcon from 'components/common/Icons/CloseCircleIcon';\nimport { Button } from 'components/common/Button/Button';\nimport PlusIcon from 'components/common/Icons/PlusIcon';\nimport * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled';\nimport Heading from 'components/common/heading/Heading.styled';\nimport { InputLabel } from 'components/common/Input/InputLabel.styled';\nimport Checkbox from 'components/common/Checkbox/Checkbox';\nimport SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader';\nimport SSLForm from 'widgets/ClusterConfigForm/common/SSLForm';\n\nconst KafkaCluster: React.FC = () => {\n  const { control, watch, setValue } = useFormContext();\n\n  const { fields, append, remove } = useFieldArray({\n    control,\n    name: 'bootstrapServers',\n  });\n\n  const hasTrustStore = !!watch('truststore');\n\n  const toggleSection = (section: string) => () =>\n    setValue(\n      section,\n      watch(section)\n        ? undefined\n        : {\n            location: '',\n            password: '',\n          },\n      { shouldValidate: true, shouldDirty: true, shouldTouch: true }\n    );\n\n  return (\n    <>\n      <Heading level={3}>Kafka Cluster</Heading>\n      <Input\n        label=\"Cluster name *\"\n        type=\"text\"\n        name=\"name\"\n        withError\n        hint=\"this name will help you recognize the cluster in the application interface\"\n      />\n      <Checkbox\n        name=\"readOnly\"\n        label=\"Read-only mode\"\n        hint=\"allows you to run an application in read-only mode for a specific cluster\"\n      />\n      <div>\n        <InputLabel htmlFor=\"bootstrapServers\">Bootstrap Servers *</InputLabel>\n        <InputHint>\n          the list of Kafka brokers that you want to connect to\n        </InputHint>\n        <S.GroupFieldWrapper>\n          {fields.map((field, index) => (\n            <S.BootstrapServer key={field.id}>\n              <div>\n                <Input\n                  name={`bootstrapServers.${index}.host`}\n                  placeholder=\"Host\"\n                  type=\"text\"\n                  inputSize=\"L\"\n                  withError\n                />\n              </div>\n              <div>\n                <Input\n                  name={`bootstrapServers.${index}.port`}\n                  placeholder=\"Port\"\n                  type=\"number\"\n                  positiveOnly\n                  withError\n                />\n              </div>\n              <S.BootstrapServerActions\n                aria-label=\"deleteProperty\"\n                onClick={() => remove(index)}\n              >\n                <CloseCircleIcon aria-hidden />\n              </S.BootstrapServerActions>\n            </S.BootstrapServer>\n          ))}\n          <FormError>\n            <ErrorMessage name=\"bootstrapServers\" />\n          </FormError>\n          <div>\n            <Button\n              type=\"button\"\n              buttonSize=\"M\"\n              buttonType=\"secondary\"\n              onClick={() => append({ host: '', port: '' })}\n            >\n              <PlusIcon />\n              Add Bootstrap Server\n            </Button>\n          </div>\n        </S.GroupFieldWrapper>\n      </div>\n      <hr />\n      <SectionHeader\n        title=\"Truststore\"\n        addButtonText=\"Configure Truststore\"\n        adding={!hasTrustStore}\n        onClick={toggleSection('truststore')}\n      />\n      {hasTrustStore && <SSLForm prefix=\"truststore\" title=\"Truststore\" />}\n    </>\n  );\n};\nexport default KafkaCluster;\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/KafkaConnect.tsx",
    "content": "import * as React from 'react';\nimport * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled';\nimport { Button } from 'components/common/Button/Button';\nimport Input from 'components/common/Input/Input';\nimport { useFieldArray, useFormContext } from 'react-hook-form';\nimport PlusIcon from 'components/common/Icons/PlusIcon';\nimport IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';\nimport CloseCircleIcon from 'components/common/Icons/CloseCircleIcon';\nimport {\n  FlexGrow1,\n  FlexRow,\n} from 'widgets/ClusterConfigForm/ClusterConfigForm.styled';\nimport SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader';\nimport Credentials from 'widgets/ClusterConfigForm/common/Credentials';\nimport SSLForm from 'widgets/ClusterConfigForm/common/SSLForm';\n\nconst KafkaConnect = () => {\n  const { control } = useFormContext();\n  const { fields, append, remove } = useFieldArray({\n    control,\n    name: 'kafkaConnect',\n  });\n  const handleAppend = () => append({ name: '', address: '' });\n  const toggleConfig = () => (fields.length === 0 ? handleAppend() : remove());\n\n  const hasFields = fields.length > 0;\n\n  return (\n    <>\n      <SectionHeader\n        title=\"Kafka Connect\"\n        addButtonText=\"Configure Kafka Connect\"\n        adding={!hasFields}\n        onClick={toggleConfig}\n      />\n      {hasFields && (\n        <S.GroupFieldWrapper>\n          {fields.map((item, index) => (\n            <div key={item.id}>\n              <FlexRow>\n                <FlexGrow1>\n                  <Input\n                    label=\"Kafka Connect name *\"\n                    name={`kafkaConnect.${index}.name`}\n                    placeholder=\"Name\"\n                    type=\"text\"\n                    hint=\"Given name for the Kafka Connect cluster\"\n                    withError\n                  />\n                  <Input\n                    label=\"Kafka Connect URL *\"\n                    name={`kafkaConnect.${index}.address`}\n                    placeholder=\"URl\"\n                    type=\"text\"\n                    hint=\"Address of the Kafka Connect service endpoint\"\n                    withError\n                  />\n                  <Credentials\n                    prefix={`kafkaConnect.${index}`}\n                    title=\"Is connect secured with auth?\"\n                  />\n                  <SSLForm\n                    prefix={`kafkaConnect.${index}.keystore`}\n                    title=\"Keystore\"\n                  />\n                </FlexGrow1>\n                <S.RemoveButton onClick={() => remove(index)}>\n                  <IconButtonWrapper aria-label=\"deleteProperty\">\n                    <CloseCircleIcon aria-hidden />\n                  </IconButtonWrapper>\n                </S.RemoveButton>\n              </FlexRow>\n\n              <hr />\n            </div>\n          ))}\n          <Button\n            type=\"button\"\n            buttonSize=\"M\"\n            buttonType=\"secondary\"\n            onClick={handleAppend}\n          >\n            <PlusIcon />\n            Add Kafka Connect\n          </Button>\n        </S.GroupFieldWrapper>\n      )}\n    </>\n  );\n};\nexport default KafkaConnect;\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/Metrics.tsx",
    "content": "import React from 'react';\nimport Input from 'components/common/Input/Input';\nimport { useFormContext } from 'react-hook-form';\nimport ControlledSelect from 'components/common/Select/ControlledSelect';\nimport { METRICS_OPTIONS } from 'lib/constants';\nimport * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled';\nimport SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader';\nimport SSLForm from 'widgets/ClusterConfigForm/common/SSLForm';\nimport Credentials from 'widgets/ClusterConfigForm/common/Credentials';\n\nconst Metrics = () => {\n  const { setValue, watch } = useFormContext();\n  const visibleMetrics = !!watch('metrics');\n  const toggleMetrics = () =>\n    setValue(\n      'metrics',\n      visibleMetrics\n        ? undefined\n        : {\n            type: '',\n            port: 0,\n            isAuth: false,\n          },\n      { shouldValidate: true, shouldDirty: true, shouldTouch: true }\n    );\n\n  return (\n    <>\n      <SectionHeader\n        title=\"Metrics\"\n        adding={!visibleMetrics}\n        addButtonText=\"Configure Metrics\"\n        onClick={toggleMetrics}\n      />\n      {visibleMetrics && (\n        <>\n          <ControlledSelect\n            name=\"metrics.type\"\n            label=\"Metrics Type\"\n            placeholder=\"Choose metrics type\"\n            options={METRICS_OPTIONS}\n          />\n          <S.Port>\n            <Input\n              label=\"Port *\"\n              name=\"metrics.port\"\n              type=\"number\"\n              positiveOnly\n              withError\n            />\n          </S.Port>\n          <Credentials prefix=\"metrics\" />\n          <SSLForm prefix=\"metrics.keystore\" title=\"Metrics Keystore\" />\n        </>\n      )}\n    </>\n  );\n};\nexport default Metrics;\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/Sections/SchemaRegistry.tsx",
    "content": "import React from 'react';\nimport Input from 'components/common/Input/Input';\nimport { useFormContext } from 'react-hook-form';\nimport SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader';\nimport SSLForm from 'widgets/ClusterConfigForm/common/SSLForm';\nimport Credentials from 'widgets/ClusterConfigForm/common/Credentials';\n\nconst SchemaRegistry = () => {\n  const { setValue, watch } = useFormContext();\n  const schemaRegistry = watch('schemaRegistry');\n  const toggleConfig = () => {\n    setValue(\n      'schemaRegistry',\n      schemaRegistry ? undefined : { url: '', isAuth: false },\n      { shouldValidate: true, shouldDirty: true, shouldTouch: true }\n    );\n  };\n  return (\n    <>\n      <SectionHeader\n        title=\"Schema Registry\"\n        adding={!schemaRegistry}\n        addButtonText=\"Configure Schema Registry\"\n        onClick={toggleConfig}\n      />\n      {schemaRegistry && (\n        <>\n          <Input\n            label=\"URL *\"\n            name=\"schemaRegistry.url\"\n            type=\"text\"\n            placeholder=\"http://localhost:8081\"\n            withError\n          />\n          <Credentials\n            prefix=\"schemaRegistry\"\n            title=\"Is Schema Registry secured with auth?\"\n          />\n          <SSLForm prefix=\"schemaRegistry.keystore\" title=\"Keystore\" />\n        </>\n      )}\n    </>\n  );\n};\nexport default SchemaRegistry;\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/common/Credentials.tsx",
    "content": "import * as React from 'react';\nimport Input from 'components/common/Input/Input';\nimport * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled';\nimport Checkbox from 'components/common/Checkbox/Checkbox';\nimport { useFormContext } from 'react-hook-form';\n\ntype CredentialsProps = {\n  prefix: string;\n  title?: string;\n};\n\nconst Credentials: React.FC<CredentialsProps> = ({\n  prefix,\n  title = 'Secured with auth?',\n}) => {\n  const { watch } = useFormContext();\n\n  return (\n    <S.GroupFieldWrapper>\n      <Checkbox name={`${prefix}.isAuth`} label={title} />\n      {watch(`${prefix}.isAuth`) && (\n        <S.FlexRow>\n          <S.FlexGrow1>\n            <Input\n              label=\"Username *\"\n              type=\"text\"\n              name={`${prefix}.username`}\n              withError\n            />\n          </S.FlexGrow1>\n          <S.FlexGrow1>\n            <Input\n              label=\"Password *\"\n              type=\"password\"\n              name={`${prefix}.password`}\n              withError\n            />\n          </S.FlexGrow1>\n        </S.FlexRow>\n      )}\n    </S.GroupFieldWrapper>\n  );\n};\n\nexport default Credentials;\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/common/Fileupload.tsx",
    "content": "import * as React from 'react';\nimport { FormError } from 'components/common/Input/Input.styled';\nimport { InputLabel } from 'components/common/Input/InputLabel.styled';\nimport { ErrorMessage } from '@hookform/error-message';\nimport { useFormContext } from 'react-hook-form';\nimport Input from 'components/common/Input/Input';\nimport { Button } from 'components/common/Button/Button';\nimport * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled';\nimport { useAppConfigFilesUpload } from 'lib/hooks/api/appConfig';\n\nconst Fileupload: React.FC<{ name: string; label: string }> = ({\n  name,\n  label,\n}) => {\n  const upload = useAppConfigFilesUpload();\n\n  const id = React.useId();\n  const { watch, setValue } = useFormContext();\n  const loc = watch(name);\n\n  const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {\n    if (e.target.files) {\n      const formData = new FormData();\n      const file = e.target.files[0];\n      formData.append('file', file);\n      const resp = await upload.mutateAsync(formData);\n      setValue(name, resp.location, {\n        shouldValidate: true,\n        shouldDirty: true,\n      });\n    }\n  };\n\n  const onReset = () => {\n    setValue(name, '', { shouldValidate: true, shouldDirty: true });\n  };\n\n  return (\n    <div>\n      <InputLabel htmlFor={id}>{label}</InputLabel>\n\n      {loc ? (\n        <S.FlexRow>\n          <S.FlexGrow1>\n            <Input name={name} disabled />\n          </S.FlexGrow1>\n          <Button buttonType=\"secondary\" buttonSize=\"L\" onClick={onReset}>\n            Reset\n          </Button>\n        </S.FlexRow>\n      ) : (\n        <S.FileUploadInputWrapper>\n          {upload.isLoading ? (\n            <p>Uploading...</p>\n          ) : (\n            <input type=\"file\" onChange={handleFileChange} />\n          )}\n        </S.FileUploadInputWrapper>\n      )}\n      <FormError>\n        <ErrorMessage name={name} />\n      </FormError>\n    </div>\n  );\n};\n\nexport default Fileupload;\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/common/SSLForm.tsx",
    "content": "import * as React from 'react';\nimport Input from 'components/common/Input/Input';\nimport Fileupload from 'widgets/ClusterConfigForm/common/Fileupload';\nimport * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled';\n\ntype SSLFormProps = {\n  prefix: string;\n  title: string;\n};\n\nconst SSLForm: React.FC<SSLFormProps> = ({ prefix, title }) => {\n  return (\n    <S.GroupFieldWrapper>\n      <Fileupload name={`${prefix}.location`} label={`${title} Location`} />\n      <Input\n        label={`${title} Password`}\n        name={`${prefix}.password`}\n        type=\"password\"\n        withError\n      />\n    </S.GroupFieldWrapper>\n  );\n};\n\nexport default SSLForm;\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/common/SectionHeader.tsx",
    "content": "import * as React from 'react';\nimport { Button } from 'components/common/Button/Button';\nimport Heading from 'components/common/heading/Heading.styled';\nimport * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled';\n\ninterface SectionHeaderProps {\n  title: string;\n  addButtonText: string;\n  adding?: boolean;\n  onClick: () => void;\n}\n\nconst SectionHeader: React.FC<SectionHeaderProps> = ({\n  adding,\n  title,\n  addButtonText,\n  onClick,\n}) => {\n  return (\n    <S.FlexRow>\n      <S.FlexGrow1>\n        <Heading level={3}>{title}</Heading>\n      </S.FlexGrow1>\n      <Button buttonSize=\"M\" buttonType=\"primary\" onClick={onClick}>\n        {adding ? addButtonText : 'Remove from config'}\n      </Button>\n    </S.FlexRow>\n  );\n};\n\nexport default SectionHeader;\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/index.tsx",
    "content": "import React from 'react';\nimport { Button } from 'components/common/Button/Button';\nimport { useForm, FormProvider } from 'react-hook-form';\nimport { yupResolver } from '@hookform/resolvers/yup';\nimport formSchema from 'widgets/ClusterConfigForm/schema';\nimport { FlexFieldset, StyledForm } from 'components/common/Form/Form.styled';\nimport {\n  useUpdateAppConfig,\n  useValidateAppConfig,\n} from 'lib/hooks/api/appConfig';\nimport { ClusterConfigFormValues } from 'widgets/ClusterConfigForm/types';\nimport { transformFormDataToPayload } from 'widgets/ClusterConfigForm/utils/transformFormDataToPayload';\nimport { showAlert, showSuccessAlert } from 'lib/errorHandling';\nimport { getIsValidConfig } from 'widgets/ClusterConfigForm/utils/getIsValidConfig';\nimport * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled';\nimport { useNavigate } from 'react-router-dom';\nimport useBoolean from 'lib/hooks/useBoolean';\nimport KafkaCluster from 'widgets/ClusterConfigForm/Sections/KafkaCluster';\nimport SchemaRegistry from 'widgets/ClusterConfigForm/Sections/SchemaRegistry';\nimport KafkaConnect from 'widgets/ClusterConfigForm/Sections/KafkaConnect';\nimport Metrics from 'widgets/ClusterConfigForm/Sections/Metrics';\nimport CustomAuthentication from 'widgets/ClusterConfigForm/Sections/CustomAuthentication';\nimport Authentication from 'widgets/ClusterConfigForm/Sections/Authentication/Authentication';\nimport KSQL from 'widgets/ClusterConfigForm/Sections/KSQL';\n\ninterface ClusterConfigFormProps {\n  hasCustomConfig?: boolean;\n  initialValues?: Partial<ClusterConfigFormValues>;\n}\n\nconst CLUSTER_CONFIG_FORM_DEFAULT_VALUES: Partial<ClusterConfigFormValues> = {\n  bootstrapServers: [{ host: '', port: '' }],\n};\n\nconst ClusterConfigForm: React.FC<ClusterConfigFormProps> = ({\n  initialValues = {},\n  hasCustomConfig,\n}) => {\n  const navigate = useNavigate();\n  const methods = useForm<ClusterConfigFormValues>({\n    mode: 'all',\n    resolver: yupResolver(formSchema),\n    defaultValues: {\n      ...CLUSTER_CONFIG_FORM_DEFAULT_VALUES,\n      ...initialValues,\n    },\n  });\n  const {\n    formState: { isSubmitting, isDirty },\n    trigger,\n  } = methods;\n\n  const validate = useValidateAppConfig();\n  const update = useUpdateAppConfig({ initialName: initialValues.name });\n  const {\n    value: isFormDisabled,\n    setTrue: disableForm,\n    setFalse: enableForm,\n  } = useBoolean();\n\n  const onSubmit = async (data: ClusterConfigFormValues) => {\n    const config = transformFormDataToPayload(data);\n    try {\n      await update.mutateAsync(config);\n      navigate('/');\n    } catch (e) {\n      showAlert('error', {\n        id: 'app-config-update-error',\n        title: 'Error updating application config',\n        message: 'There was an error updating the application config',\n      });\n    }\n  };\n\n  const onReset = () => methods.reset();\n\n  const onValidate = async () => {\n    await trigger(undefined, { shouldFocus: true });\n    if (!methods.formState.isValid) return;\n    disableForm();\n    const data = methods.getValues();\n    const config = transformFormDataToPayload(data);\n\n    try {\n      const response = await validate.mutateAsync(config);\n      const isConfigValid = getIsValidConfig(response, data.name);\n      if (isConfigValid) {\n        showSuccessAlert({\n          message: 'Configuration is valid',\n        });\n      }\n    } catch (e) {\n      showAlert('error', {\n        id: 'app-config-validate-error',\n        title: 'Error validating application config',\n        message: 'There was an error validating the application config',\n      });\n    }\n    enableForm();\n  };\n\n  const showCustomConfig = methods.watch('customAuth') && hasCustomConfig;\n\n  const isValidateDisabled = isSubmitting;\n  const isSubmitDisabled = isSubmitting || !isDirty;\n\n  return (\n    <FormProvider {...methods}>\n      <StyledForm onSubmit={methods.handleSubmit(onSubmit)}>\n        <FlexFieldset disabled={isFormDisabled || isSubmitting}>\n          <KafkaCluster />\n          <hr />\n          {showCustomConfig ? <CustomAuthentication /> : <Authentication />}\n          <hr />\n          <SchemaRegistry />\n          <hr />\n          <KafkaConnect />\n          <hr />\n          <KSQL />\n          <hr />\n          <Metrics />\n          <hr />\n          <S.ButtonWrapper>\n            <Button\n              buttonSize=\"L\"\n              buttonType=\"secondary\"\n              onClick={onReset}\n              disabled={isSubmitting}\n            >\n              Reset\n            </Button>\n            <Button\n              buttonSize=\"L\"\n              buttonType=\"secondary\"\n              onClick={onValidate}\n              disabled={isValidateDisabled}\n            >\n              Validate\n            </Button>\n            <Button\n              type=\"submit\"\n              buttonSize=\"L\"\n              buttonType=\"primary\"\n              disabled={isSubmitDisabled}\n              inProgress={isSubmitting}\n            >\n              Submit\n            </Button>\n          </S.ButtonWrapper>\n        </FlexFieldset>\n      </StyledForm>\n    </FormProvider>\n  );\n};\n\nexport default ClusterConfigForm;\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/schema.ts",
    "content": "import { isArray } from 'lodash';\nimport { object, string, number, array, boolean, mixed, lazy } from 'yup';\n\nconst requiredString = string().required('required field');\n\nconst portSchema = number()\n  .positive('positive only')\n  .typeError('numbers only')\n  .required('required');\n\nconst bootstrapServerSchema = object({\n  host: requiredString,\n  port: portSchema,\n});\n\nconst sslSchema = lazy((value) => {\n  if (typeof value === 'object') {\n    return object({\n      location: string().when('password', {\n        is: (v: string) => !!v,\n        then: (schema) => schema.required('required field'),\n      }),\n      password: string(),\n    });\n  }\n  return mixed().optional();\n});\n\nconst urlWithAuthSchema = lazy((value) => {\n  if (typeof value === 'object') {\n    return object({\n      url: requiredString,\n      isAuth: boolean(),\n      username: string().when('isAuth', {\n        is: true,\n        then: (schema) => schema.required('required field'),\n      }),\n      password: string().when('isAuth', {\n        is: true,\n        then: (schema) => schema.required('required field'),\n      }),\n      keystore: sslSchema,\n    });\n  }\n  return mixed().optional();\n});\n\nconst kafkaConnectSchema = object({\n  name: requiredString,\n  address: requiredString,\n  isAuth: boolean(),\n  username: string().when('isAuth', {\n    is: true,\n    then: (schema) => schema.required('required field'),\n  }),\n  password: string().when('isAuth', {\n    is: true,\n    then: (schema) => schema.required('required field'),\n  }),\n  keystore: sslSchema,\n});\n\nconst kafkaConnectsSchema = lazy((value) => {\n  if (isArray(value)) {\n    return array().of(kafkaConnectSchema);\n  }\n  return mixed().optional();\n});\n\nconst metricsSchema = lazy((value) => {\n  if (typeof value === 'object') {\n    return object({\n      type: string().oneOf(['JMX', 'PROMETHEUS']).required('required field'),\n      port: portSchema,\n      isAuth: boolean(),\n      username: string().when('isAuth', {\n        is: true,\n        then: (schema) => schema.required('required field'),\n      }),\n      password: string().when('isAuth', {\n        is: true,\n        then: (schema) => schema.required('required field'),\n      }),\n      keystore: sslSchema,\n    });\n  }\n  return mixed().optional();\n});\n\nconst authPropsSchema = lazy((_, { parent }) => {\n  switch (parent.method) {\n    case 'SASL/JAAS':\n      return object({\n        saslJaasConfig: requiredString,\n        saslMechanism: requiredString,\n      });\n    case 'SASL/GSSAPI':\n      return object({\n        saslKerberosServiceName: requiredString,\n        keyTabFile: string(),\n        storeKey: boolean(),\n        principal: requiredString,\n      });\n    case 'SASL/OAUTHBEARER':\n      return object({\n        unsecuredLoginStringClaim_sub: requiredString,\n      });\n    case 'SASL/PLAIN':\n    case 'SASL/SCRAM-256':\n    case 'SASL/SCRAM-512':\n    case 'SASL/LDAP':\n      return object({\n        username: requiredString,\n        password: requiredString,\n      });\n    case 'Delegation tokens':\n      return object({\n        tokenId: requiredString,\n        tokenValue: requiredString,\n      });\n    case 'SASL/AWS IAM':\n      return object({\n        awsProfileName: string(),\n      });\n    case 'mTLS':\n    default:\n      return mixed().optional();\n  }\n});\n\nconst authSchema = lazy((value) => {\n  if (typeof value === 'object') {\n    return object({\n      method: string()\n        .required('required field')\n        .oneOf([\n          'SASL/JAAS',\n          'SASL/GSSAPI',\n          'SASL/OAUTHBEARER',\n          'SASL/PLAIN',\n          'SASL/SCRAM-256',\n          'SASL/SCRAM-512',\n          'Delegation tokens',\n          'SASL/LDAP',\n          'SASL/AWS IAM',\n          'mTLS',\n        ]),\n      securityProtocol: string()\n        .oneOf(['SASL_SSL', 'SASL_PLAINTEXT'])\n        .when('method', {\n          is: (v: string) => {\n            return [\n              'SASL/JAAS',\n              'SASL/GSSAPI',\n              'SASL/OAUTHBEARER',\n              'SASL/PLAIN',\n              'SASL/SCRAM-256',\n              'SASL/SCRAM-512',\n              'SASL/LDAP',\n              'SASL/AWS IAM',\n            ].includes(v);\n          },\n          then: (schema) => schema.required('required field'),\n        }),\n      keystore: lazy((_, { parent }) => {\n        if (parent.method === 'mTLS') {\n          return object({\n            location: requiredString,\n            password: string(),\n          });\n        }\n        return mixed().optional();\n      }),\n      props: authPropsSchema,\n    });\n  }\n  return mixed().optional();\n});\n\nconst formSchema = object({\n  name: string()\n    .required('required field')\n    .min(3, 'Cluster name must be at least 3 characters'),\n  readOnly: boolean().required('required field'),\n  bootstrapServers: array().of(bootstrapServerSchema).min(1),\n  truststore: sslSchema,\n  auth: authSchema,\n  schemaRegistry: urlWithAuthSchema,\n  ksql: urlWithAuthSchema,\n  kafkaConnect: kafkaConnectsSchema,\n  metrics: metricsSchema,\n});\n\nexport default formSchema;\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/types.ts",
    "content": "type SecurityProtocol = 'SASL_SSL' | 'SASL_PLAINTEXT';\ntype BootstrapServer = {\n  host: string;\n  port: string;\n};\n\ntype WithKeystore = {\n  keystore?: {\n    location: string;\n    password: string;\n  };\n};\n\ntype WithAuth = {\n  isAuth: boolean;\n  username?: string;\n  password?: string;\n};\n\ntype URLWithAuth = WithAuth &\n  WithKeystore & {\n    url?: string;\n  };\n\ntype KafkaConnect = WithAuth &\n  WithKeystore & {\n    name: string;\n    address: string;\n  };\n\ntype Metrics = WithAuth &\n  WithKeystore & {\n    type: string;\n    port: string;\n  };\n\nexport type ClusterConfigFormValues = {\n  name: string;\n  readOnly: boolean;\n  bootstrapServers: BootstrapServer[];\n  truststore?: {\n    location: string;\n    password: string;\n  };\n  auth?: WithKeystore & {\n    method: string;\n    securityProtocol: SecurityProtocol;\n    props: Record<string, string>;\n  };\n  schemaRegistry?: URLWithAuth;\n  ksql?: URLWithAuth;\n  properties?: Record<string, string>;\n  kafkaConnect?: KafkaConnect[];\n  metrics?: Metrics;\n  customAuth: Record<string, string>;\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/convertFormKeyToPropsKey.ts",
    "content": "export const convertFormKeyToPropsKey = (key: string) => {\n  return key.split('___').join('.');\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/convertPropsKeyToFormKey.ts",
    "content": "export const convertPropsKeyToFormKey = (key: string) => {\n  return key.split('.').join('___');\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getInitialFormData.ts",
    "content": "import {\n  ApplicationConfigPropertiesKafkaClusters,\n  ApplicationConfigPropertiesKafkaSchemaRegistrySsl,\n} from 'generated-sources';\nimport { ClusterConfigFormValues } from 'widgets/ClusterConfigForm/types';\n\nimport { convertPropsKeyToFormKey } from './convertPropsKeyToFormKey';\n\nconst parseBootstrapServers = (bootstrapServers?: string) =>\n  bootstrapServers?.split(',').map((url) => {\n    const [host, port] = url.split(':');\n    return { host, port };\n  });\n\nconst parseKeystore = (\n  keystore?: ApplicationConfigPropertiesKafkaSchemaRegistrySsl\n) => {\n  if (!keystore) return undefined;\n  const { keystoreLocation, keystorePassword } = keystore;\n  return {\n    keystore: {\n      location: keystoreLocation as string,\n      password: keystorePassword as string,\n    },\n  };\n};\n\nconst parseCredentials = (username?: string, password?: string) => {\n  if (!username || !password) return { isAuth: false };\n  return { isAuth: true, username, password };\n};\n\nexport const getInitialFormData = (\n  payload: ApplicationConfigPropertiesKafkaClusters\n) => {\n  const {\n    ssl,\n    schemaRegistry,\n    schemaRegistryAuth,\n    schemaRegistrySsl,\n    kafkaConnect,\n    metrics,\n    ksqldbServer,\n    ksqldbServerAuth,\n    ksqldbServerSsl,\n  } = payload;\n\n  const initialValues: Partial<ClusterConfigFormValues> = {\n    name: payload.name as string,\n    readOnly: !!payload.readOnly,\n    bootstrapServers: parseBootstrapServers(payload.bootstrapServers),\n  };\n\n  const { truststoreLocation, truststorePassword } = ssl || {};\n\n  if (truststoreLocation && truststorePassword) {\n    initialValues.truststore = {\n      location: truststoreLocation,\n      password: truststorePassword,\n    };\n  }\n\n  if (schemaRegistry) {\n    initialValues.schemaRegistry = {\n      url: schemaRegistry,\n      ...parseCredentials(\n        schemaRegistryAuth?.username,\n        schemaRegistryAuth?.password\n      ),\n      ...parseKeystore(schemaRegistrySsl),\n    };\n  }\n  if (ksqldbServer) {\n    initialValues.ksql = {\n      url: ksqldbServer,\n      ...parseCredentials(\n        ksqldbServerAuth?.username,\n        ksqldbServerAuth?.password\n      ),\n      ...parseKeystore(ksqldbServerSsl),\n    };\n  }\n\n  if (kafkaConnect && kafkaConnect.length > 0) {\n    initialValues.kafkaConnect = kafkaConnect.map((c) => ({\n      name: c.name as string,\n      address: c.address as string,\n      ...parseCredentials(c.username, c.password),\n      ...parseKeystore(c),\n    }));\n  }\n\n  if (metrics) {\n    initialValues.metrics = {\n      type: metrics.type as string,\n      ...parseCredentials(metrics.username, metrics.password),\n      ...parseKeystore(metrics),\n      port: `${metrics.port}`,\n    };\n  }\n\n  const properties = payload.properties || {};\n\n  // Authentification\n  initialValues.customAuth = {};\n\n  Object.entries(properties).forEach(([key, val]) => {\n    if (\n      key.startsWith('security.') ||\n      key.startsWith('sasl.') ||\n      key.startsWith('ssl.')\n    ) {\n      initialValues.customAuth = {\n        ...initialValues.customAuth,\n        [convertPropsKeyToFormKey(key)]: val,\n      };\n    }\n  });\n\n  return initialValues as ClusterConfigFormValues;\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getIsValidConfig.ts",
    "content": "import { ApplicationConfigValidation } from 'generated-sources';\nimport { showAlert } from 'lib/errorHandling';\n\nexport const getIsValidConfig = (\n  { clusters }: ApplicationConfigValidation,\n  name: string\n) => {\n  let isValid = true;\n  const prefix = `cluster-${name}`;\n  const clusterErrors = clusters?.[name];\n\n  if (clusterErrors?.kafka?.error) {\n    isValid = false;\n    showAlert('error', {\n      id: `${prefix}-kafka`,\n      title: 'Kafka Cluster',\n      message: clusterErrors?.kafka.errorMessage,\n    });\n  }\n  if (clusterErrors?.schemaRegistry?.error) {\n    isValid = false;\n    showAlert('error', {\n      id: `${prefix}-schemaRegistry`,\n      title: 'Schema Registry',\n      message: clusterErrors?.schemaRegistry.errorMessage,\n    });\n  }\n  if (clusterErrors?.ksqldb?.error) {\n    isValid = false;\n    showAlert('error', {\n      id: `${prefix}-ksqldb`,\n      title: 'KSQL DB',\n      message: clusterErrors?.ksqldb?.errorMessage,\n    });\n  }\n  if (clusterErrors?.kafkaConnects) {\n    Object.entries(clusterErrors.kafkaConnects).forEach(([key, val]) => {\n      if (val?.error) {\n        isValid = false;\n        showAlert('error', {\n          id: `${prefix}-kafkaConnects-${key}`,\n          title: `Kafka Connect. ${key}`,\n          message: val.errorMessage,\n        });\n      }\n    });\n  }\n  return isValid;\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/getJaasConfig.ts",
    "content": "import { isUndefined } from 'lodash';\n\nconst JAAS_CONFIGS = {\n  'SASL/GSSAPI': 'com.sun.security.auth.module.Krb5LoginModule',\n  'SASL/OAUTHBEARER':\n    'org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule',\n  'SASL/PLAIN': 'org.apache.kafka.common.security.plain.PlainLoginModule',\n  'SASL/SCRAM-256': 'org.apache.kafka.common.security.scram.ScramLoginModule',\n  'SASL/SCRAM-512': 'org.apache.kafka.common.security.scram.ScramLoginModule',\n  'Delegation tokens':\n    'org.apache.kafka.common.security.scram.ScramLoginModule',\n  'SASL/LDAP': 'org.apache.kafka.common.security.plain.PlainLoginModule',\n  'SASL/AWS IAM': 'software.amazon.msk.auth.iam.IAMLoginModule',\n};\n\ntype MethodName = keyof typeof JAAS_CONFIGS;\n\nexport const getJaasConfig = (\n  method: MethodName,\n  options: Record<string, string>\n) => {\n  const optionsString = Object.entries(options)\n    .map(([key, value]) => {\n      if (isUndefined(value)) return null;\n      if (value === 'true' || value === 'false') {\n        return ` ${key}=${value}`;\n      }\n      return ` ${key}=\"${value}\"`;\n    })\n    .join('');\n\n  return `${JAAS_CONFIGS[method]} required${optionsString};`;\n};\n"
  },
  {
    "path": "kafka-ui-react-app/src/widgets/ClusterConfigForm/utils/transformFormDataToPayload.ts",
    "content": "import { ClusterConfigFormValues } from 'widgets/ClusterConfigForm/types';\nimport { ApplicationConfigPropertiesKafkaClusters } from 'generated-sources';\n\nimport { getJaasConfig } from './getJaasConfig';\nimport { convertFormKeyToPropsKey } from './convertFormKeyToPropsKey';\n\nconst transformToKeystore = (keystore?: {\n  location: string;\n  password: string;\n}) => {\n  if (!keystore || !keystore.location) return undefined;\n  return {\n    keystoreLocation: keystore.location,\n    keystorePassword: keystore.password,\n  };\n};\n\nconst transformToCredentials = (\n  isAuth: boolean,\n  username?: string,\n  password?: string\n) => {\n  if (!isAuth || !username || !password) return undefined;\n  return { username, password };\n};\n\nconst transformCustomProps = (props: Record<string, string>) => {\n  const config: Record<string, string> = {};\n  if (!props) return config;\n\n  Object.entries(props).forEach(([key, val]) => {\n    if (props[key]) config[convertFormKeyToPropsKey(key)] = val;\n  });\n\n  return config;\n};\n\nexport const transformFormDataToPayload = (data: ClusterConfigFormValues) => {\n  const config: ApplicationConfigPropertiesKafkaClusters = {\n    name: data.name,\n    bootstrapServers: data.bootstrapServers\n      .map(({ host, port }) => `${host}:${port}`)\n      .join(','),\n    readOnly: data.readOnly,\n  };\n\n  if (data.truststore) {\n    config.ssl = {\n      truststoreLocation: data.truststore?.location,\n      truststorePassword: data.truststore?.password,\n    };\n  }\n\n  // Schema Registry\n  if (data.schemaRegistry) {\n    config.schemaRegistry = data.schemaRegistry.url;\n    config.schemaRegistryAuth = transformToCredentials(\n      data.schemaRegistry.isAuth,\n      data.schemaRegistry.username,\n      data.schemaRegistry.password\n    );\n    config.schemaRegistrySsl = transformToKeystore(\n      data.schemaRegistry.keystore\n    );\n  }\n\n  // KSQL\n  if (data.ksql) {\n    config.ksqldbServer = data.ksql.url;\n    config.ksqldbServerAuth = transformToCredentials(\n      data.ksql.isAuth,\n      data.ksql.username,\n      data.ksql.password\n    );\n    config.ksqldbServerSsl = transformToKeystore(data.ksql.keystore);\n  }\n\n  // Kafka Connect\n  if (data.kafkaConnect && data.kafkaConnect.length > 0) {\n    config.kafkaConnect = data.kafkaConnect.map(\n      ({ name, address, isAuth, username, password, keystore }) => ({\n        name,\n        address,\n        ...transformToKeystore(keystore),\n        ...transformToCredentials(isAuth, username, password),\n      })\n    );\n  }\n\n  // Metrics\n  if (data.metrics) {\n    config.metrics = {\n      type: data.metrics.type,\n      port: Number(data.metrics.port),\n      ...transformToKeystore(data.metrics.keystore),\n      ...transformToCredentials(\n        data.metrics.isAuth,\n        data.metrics.username,\n        data.metrics.password\n      ),\n    };\n  }\n\n  config.properties = {\n    ...transformCustomProps(data.customAuth),\n  };\n\n  // Authentication\n  if (data.auth) {\n    const { method, props, securityProtocol, keystore } = data.auth;\n    switch (method) {\n      case 'SASL/JAAS':\n        config.properties = {\n          'security.protocol': securityProtocol,\n          'sasl.jaas.config': props.saslJaasConfig,\n          'sasl.mechanism': props.saslMechanism,\n        };\n        break;\n      case 'SASL/GSSAPI':\n        config.properties = {\n          'security.protocol': securityProtocol,\n          'sasl.mechanism': 'GSSAPI',\n          'sasl.kerberos.service.name': props.saslKerberosServiceName,\n          'sasl.jaas.config': getJaasConfig('SASL/GSSAPI', {\n            useKeyTab: props.keyTabFile ? 'true' : 'false',\n            keyTab: props.keyTabFile,\n            storeKey: String(!!props.storeKey),\n            principal: props.principal,\n          }),\n        };\n        break;\n      case 'SASL/OAUTHBEARER':\n        config.properties = {\n          'security.protocol': securityProtocol,\n          'sasl.mechanism': 'OAUTHBEARER',\n          'sasl.jaas.config': getJaasConfig('SASL/OAUTHBEARER', {\n            unsecuredLoginStringClaim_sub: props.unsecuredLoginStringClaim_sub,\n          }),\n        };\n        break;\n      case 'SASL/PLAIN':\n        config.properties = {\n          'security.protocol': securityProtocol,\n          'sasl.mechanism': 'PLAIN',\n          'sasl.jaas.config': getJaasConfig(\n            'SASL/PLAIN',\n            transformToCredentials(\n              Boolean(props.isAuth),\n              props.username,\n              props.password\n            ) || {}\n          ),\n        };\n        break;\n      case 'SASL/SCRAM-256':\n        config.properties = {\n          'security.protocol': securityProtocol,\n          'sasl.mechanism': 'SCRAM-SHA-256',\n          'sasl.jaas.config': getJaasConfig(\n            'SASL/SCRAM-256',\n            transformToCredentials(\n              Boolean(props.isAuth),\n              props.username,\n              props.password\n            ) || {}\n          ),\n        };\n        break;\n      case 'SASL/SCRAM-512':\n        config.properties = {\n          'security.protocol': securityProtocol,\n          'sasl.mechanism': 'SCRAM-SHA-512',\n          'sasl.jaas.config': getJaasConfig(\n            'SASL/SCRAM-512',\n            transformToCredentials(\n              Boolean(props.isAuth),\n              props.username,\n              props.password\n            ) || {}\n          ),\n        };\n        break;\n      case 'Delegation tokens':\n        config.properties = {\n          'security.protocol': securityProtocol,\n          'sasl.jaas.config': getJaasConfig('Delegation tokens', {\n            username: props.tokenId,\n            password: props.tokenValue,\n            tokenauth: 'true',\n          }),\n        };\n        break;\n      case 'SASL/LDAP':\n        config.properties = {\n          'security.protocol': securityProtocol,\n          'sasl.mechanism': 'PLAIN',\n          'sasl.jaas.config': getJaasConfig(\n            'SASL/LDAP',\n            transformToCredentials(\n              Boolean(props.isAuth),\n              props.username,\n              props.password\n            ) || {}\n          ),\n        };\n        break;\n      case 'SASL/AWS IAM':\n        config.properties = {\n          'security.protocol': securityProtocol,\n          'sasl.mechanism': 'AWS_MSK_IAM',\n          'sasl.client.callback.handler.class':\n            'software.amazon.msk.auth.iam.IAMClientCallbackHandler',\n          'sasl.jaas.config': getJaasConfig('SASL/AWS IAM', {\n            awsProfileName: props.awsProfileName,\n          }),\n        };\n        break;\n      case 'mTLS':\n        config.properties = {\n          'security.protocol': 'SSL',\n          'ssl.keystore.location': keystore?.location,\n          'ssl.keystore.password': keystore?.password,\n        };\n        break;\n      default:\n      // do nothing\n    }\n  }\n\n  return config;\n};\n"
  },
  {
    "path": "kafka-ui-react-app/tsconfig.dev.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsxdev\"\n  }\n}\n"
  },
  {
    "path": "kafka-ui-react-app/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"baseUrl\": \"src\",\n    \"noFallthroughCasesInSwitch\": true,\n    \"types\": [\"vite/client\"]\n  },\n  \"include\": [\n    \"src\",\n    \"vite.config.ts\",\n    \"jest.config.ts\",\n  ]\n}\n"
  },
  {
    "path": "kafka-ui-react-app/vite.config.ts",
    "content": "import {\n  defineConfig,\n  loadEnv,\n  UserConfigExport,\n  splitVendorChunkPlugin,\n} from 'vite';\nimport react from '@vitejs/plugin-react-swc';\nimport tsconfigPaths from 'vite-tsconfig-paths';\nimport { ViteEjsPlugin } from 'vite-plugin-ejs';\n\nexport default defineConfig(({ mode }) => {\n  process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };\n\n  const defaultConfig: UserConfigExport = {\n    plugins: [\n      react(),\n      tsconfigPaths(),\n      splitVendorChunkPlugin(),\n      ViteEjsPlugin({\n        PUBLIC_PATH: mode !== 'development' ? 'PUBLIC-PATH-VARIABLE' : '',\n      }),\n    ],\n    server: {\n      port: 3000,\n    },\n    build: {\n      outDir: 'build',\n      rollupOptions: {\n        output: {\n          manualChunks: {\n            ace: ['ace-builds', 'react-ace'],\n          },\n        },\n      },\n    },\n    experimental: {\n      renderBuiltUrl(\n        filename: string,\n        {\n          hostType,\n        }: {\n          hostId: string;\n          hostType: 'js' | 'css' | 'html';\n          type: 'asset' | 'public';\n        }\n      ) {\n        if (hostType === 'js') {\n          return {\n            runtime: `window.__assetsPathBuilder(${JSON.stringify(filename)})`,\n          };\n        }\n\n        return filename;\n      },\n    },\n    define: {\n      'process.env.NODE_ENV': `\"${mode}\"`,\n      'process.env.VITE_TAG': `\"${process.env.VITE_TAG}\"`,\n      'process.env.VITE_COMMIT': `\"${process.env.VITE_COMMIT}\"`,\n    },\n  };\n  const proxy = process.env.VITE_DEV_PROXY;\n  if (mode === 'development' && proxy) {\n    return {\n      ...defaultConfig,\n      server: {\n        ...defaultConfig.server,\n        open: true,\n        proxy: {\n          '/api': {\n            target: proxy,\n            changeOrigin: true,\n            secure: false,\n          },\n          '/actuator/info': {\n            target: proxy,\n            changeOrigin: true,\n            secure: false,\n          },\n        },\n      },\n    };\n  }\n\n  return defaultConfig;\n});\n"
  },
  {
    "path": "kafka-ui-serde-api/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project\n\txmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<packaging>jar</packaging>\n\t<properties>\n\t\t<maven.compiler.source>17</maven.compiler.source>\n\t\t<maven.compiler.target>17</maven.compiler.target>\n\t</properties>\n\t<distributionManagement>\n\t\t<snapshotRepository>\n\t\t\t<id>ossrh</id>\n\t\t\t<url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>\n\t\t</snapshotRepository>\n\t\t<repository>\n\t\t\t<id>ossrh</id>\n\t\t\t<url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>\n\t\t</repository>\n\t</distributionManagement>\n\t<name>kafka-ui-serde-api</name>\n\t<description>kafka-ui-serde-api</description>\n\t<url>http://github.com/provectus/kafka-ui</url>\n\t<licenses>\n\t\t<license>\n\t\t\t<name>The Apache License, Version 2.0</name>\n\t\t\t<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>\n\t\t</license>\n\t</licenses>\n\t<developers>\n\t\t<developer>\n\t\t\t<name>Provectus</name>\n\t\t\t<email>maintainers.kafka-ui@provectus.com</email>\n\t\t\t<organization>Provectus</organization>\n\t\t\t<organizationUrl>https://provectus.com</organizationUrl>\n\t\t</developer>\n\t</developers>\n\t<scm>\n\t\t<connection>scm:git:git://github.com/provectus/kafka-ui.git</connection>\n\t\t<developerConnection>scm:git:ssh://github.com:provectus/kafka-ui.git</developerConnection>\n\t\t<url>https://github.com/provectus/kafka-ui</url>\n\t</scm>\n\t<groupId>com.provectus</groupId>\n\t<artifactId>kafka-ui-serde-api</artifactId>\n\t<version>1.0.0</version>\n\t<build>\n\t\t<pluginManagement>\n\t\t\t<plugins>\n\t\t\t\t<plugin>\n\t\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t\t<artifactId>maven-install-plugin</artifactId>\n\t\t\t\t\t<version>2.5.2</version>\n\t\t\t\t</plugin>\n\t\t\t\t<plugin>\n\t\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t\t<artifactId>maven-jar-plugin</artifactId>\n\t\t\t\t\t<version>3.3.0</version>\n\t\t\t\t</plugin>\n\t\t\t\t<plugin>\n\t\t\t\t\t<groupId>org.sonatype.plugins</groupId>\n\t\t\t\t\t<artifactId>nexus-staging-maven-plugin</artifactId>\n\t\t\t\t\t<version>1.6.13</version>\n\t\t\t\t\t<extensions>true</extensions>\n\t\t\t\t\t<configuration>\n\t\t\t\t\t\t<serverId>ossrh</serverId>\n\t\t\t\t\t\t<nexusUrl>https://s01.oss.sonatype.org/</nexusUrl>\n\t\t\t\t\t\t<autoReleaseAfterClose>true</autoReleaseAfterClose>\n\t\t\t\t\t</configuration>\n\t\t\t\t</plugin>\n\t\t\t\t<plugin>\n\t\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t\t<artifactId>maven-source-plugin</artifactId>\n\t\t\t\t\t<version>2.2.1</version>\n\t\t\t\t\t<executions>\n\t\t\t\t\t\t<execution>\n\t\t\t\t\t\t\t<id>attach-sources</id>\n\t\t\t\t\t\t\t<goals>\n\t\t\t\t\t\t\t\t<goal>jar-no-fork</goal>\n\t\t\t\t\t\t\t</goals>\n\t\t\t\t\t\t</execution>\n\t\t\t\t\t</executions>\n\t\t\t\t</plugin>\n\t\t\t\t<plugin>\n\t\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t\t<artifactId>maven-javadoc-plugin</artifactId>\n\t\t\t\t\t<configuration>\n\t\t\t\t\t\t<source>8</source>\n\t\t\t\t\t</configuration>\n\t\t\t\t\t<version>3.5.0</version>\n\t\t\t\t\t<executions>\n\t\t\t\t\t\t<execution>\n\t\t\t\t\t\t\t<id>attach-javadocs</id>\n\t\t\t\t\t\t\t<goals>\n\t\t\t\t\t\t\t\t<goal>jar</goal>\n\t\t\t\t\t\t\t</goals>\n\t\t\t\t\t\t</execution>\n\t\t\t\t\t</executions>\n\t\t\t\t</plugin>\n\t\t\t\t<plugin>\n\t\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t\t<artifactId>maven-gpg-plugin</artifactId>\n\t\t\t\t\t<executions>\n\t\t\t\t\t\t<execution>\n\t\t\t\t\t\t\t<id>sign-artifacts</id>\n\t\t\t\t\t\t\t<phase>verify</phase>\n\t\t\t\t\t\t\t<goals>\n\t\t\t\t\t\t\t\t<goal>sign</goal>\n\t\t\t\t\t\t\t</goals>\n\t\t\t\t\t\t</execution>\n\t\t\t\t\t</executions>\n\t\t\t\t\t<configuration>\n\t\t\t\t\t\t<!-- Prevent gpg from using pinentry programs -->\n\t\t\t\t\t\t<gpgArguments>\n\t\t\t\t\t\t\t<arg>--pinentry-mode</arg>\n\t\t\t\t\t\t\t<arg>loopback</arg>\n\t\t\t\t\t\t</gpgArguments>\n\t\t\t\t\t</configuration>\n\t\t\t\t</plugin>\n\t\t\t</plugins>\n\t\t</pluginManagement>\n\t</build>\n</project>\n"
  },
  {
    "path": "kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/DeserializeResult.java",
    "content": "package com.provectus.kafka.ui.serde.api;\n\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.Objects;\n\n/**\n * Result of {@code Deserializer} work.\n */\npublic final class DeserializeResult {\n\n  public enum Type {\n    STRING, JSON\n  }\n\n  // nullable\n  private final String result;\n  private final Type type;\n  private final Map<String, Object> additionalProperties;\n\n  /**\n   * @param result string representation of deserialized binary data\n   * @param type type of string - can it be converted to json or not\n   * @param additionalProperties additional information about deserialized value (will be shown in UI)\n   */\n  public DeserializeResult(String result, Type type, Map<String, Object> additionalProperties) {\n    this.result = result;\n    this.type = type != null ? type : Type.STRING;\n    this.additionalProperties = additionalProperties != null ? additionalProperties : Collections.emptyMap();\n  }\n\n  /**\n   * @return string representation of deserialized binary data, can be null\n   */\n  public String getResult() {\n    return result;\n  }\n\n  /**\n   * @return additional information about deserialized value.\n   * Will be show as json dictionary in UI (serialized with Jackson object mapper).\n   * It is recommended to use primitive types and strings for values.\n   */\n  public Map<String, Object> getAdditionalProperties() {\n    return additionalProperties;\n  }\n\n  /**\n   * @return type of deserialized result. Will be used as hint for some internal logic\n   * (ex. if type==STRING smart filters won't try to parse it as json for further usage)\n   */\n  public Type getType() {\n    return type;\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }\n    DeserializeResult that = (DeserializeResult) o;\n    return Objects.equals(result, that.result)\n        && type == that.type\n        && additionalProperties.equals(that.additionalProperties);\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(result, type, additionalProperties);\n  }\n\n  @Override\n  public String toString() {\n    return \"DeserializeResult{\"\n        + \"result='\" + result\n        + '\\''\n        + \", type=\" + type\n        + \", additionalProperties=\"\n        + additionalProperties\n        + '}';\n  }\n}\n"
  },
  {
    "path": "kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/PropertyResolver.java",
    "content": "package com.provectus.kafka.ui.serde.api;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\n\n/**\n * Provides access to configuration properties.\n *Actual implementation uses {@code org.springframework.boot.context.properties.bind.Binder} class\n * to bind values to target types. Target type params can be custom configs classes, not only simple types and strings.\n *\n */\npublic interface PropertyResolver {\n\n  /**\n   * Get property value by name.\n   *\n   * @param key property name\n   * @param targetType type of property value\n   * @return property value or empty {@code Optional} if property not found\n   */\n  <T> Optional<T> getProperty(String key, Class<T> targetType);\n\n\n  /**\n   * Get list-property value by name\n   *\n   * @param key list property name\n   * @param itemType type of list element\n   * @return list property value or empty {@code Optional} if property not found\n   */\n  <T> Optional<List<T>> getListProperty(String key, Class<T> itemType);\n\n  /**\n   * Get map-property value by name\n   *\n   * @param key  map-property name\n   * @param keyType type of map key\n   * @param valueType type of map value\n   * @return map-property value or empty {@code Optional} if property not found\n   */\n  <K, V> Optional<Map<K, V>> getMapProperty(String key, Class<K> keyType, Class<V> valueType);\n\n}"
  },
  {
    "path": "kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/RecordHeader.java",
    "content": "package com.provectus.kafka.ui.serde.api;\n\npublic interface RecordHeader {\n\n  String key();\n\n  byte[] value();\n\n}\n"
  },
  {
    "path": "kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/RecordHeaders.java",
    "content": "package com.provectus.kafka.ui.serde.api;\n\n\npublic interface RecordHeaders extends Iterable<RecordHeader> {\n}\n"
  },
  {
    "path": "kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/SchemaDescription.java",
    "content": "package com.provectus.kafka.ui.serde.api;\n\nimport java.util.Map;\n\n/**\n * Description of topic's key/value schema.\n */\npublic final class SchemaDescription {\n\n  private final String schema;\n  private final Map<String, Object> additionalProperties;\n\n  /**\n   *\n   * @param schema schema descriptions.\n   *              If contains json-schema (preferred) UI will use it for validation and sample data generation.\n   * @param additionalProperties additional properties about schema (may be rendered in UI in the future)\n   */\n  public SchemaDescription(String schema, Map<String, Object> additionalProperties) {\n    this.schema = schema;\n    this.additionalProperties = additionalProperties;\n  }\n\n  /**\n   * @return schema description text. Preferably contains json-schema. Can be null.\n   */\n  public String getSchema() {\n    return schema;\n  }\n\n  /**\n   * @return additional properties about schema\n   */\n  public Map<String, Object> getAdditionalProperties() {\n    return additionalProperties;\n  }\n}\n"
  },
  {
    "path": "kafka-ui-serde-api/src/main/java/com/provectus/kafka/ui/serde/api/Serde.java",
    "content": "package com.provectus.kafka.ui.serde.api;\n\nimport java.io.Closeable;\nimport java.util.Optional;\n\n/**\n * Main interface of  serialization/deserialization logic.\n * It provides ability to serialize, deserialize topic's keys and values, and optionally provides\n * information about data schema inside topic.\n * <p>\n * <b>Lifecycle:</b><br/>\n * 1. on application startup kafka-ui scans configs and finds all custom serde definitions<br/>\n * 2. for each custom serde its own separated child-first classloader is created<br/>\n * 3. kafka-ui loads class defined in configuration and instantiates instance of that class using default, non-arg constructor<br/>\n * 4. {@code configure(...)} method called<br/>\n * 5. various methods called during application runtime<br/>\n * 6. on application shutdown kafka-ui calls {@code close()} method on serde instance<br/>\n * <p>\n * <b>Implementation considerations:</b><br/>\n * 1. Implementation class should have default/non-arg contructor<br/>\n * 2. All methods except {@code configure(...)} and {@code close()} can be called from different threads. So, your code should be thread-safe.<br/>\n * 3. All methods will be executed in separate child-first classloader.<br/>\n */\npublic interface Serde extends Closeable {\n\n  /**\n   * Kafka record's part that Serde will be applied to.\n   */\n  enum Target {\n    KEY, VALUE\n  }\n\n  /**\n   * Reads configuration using property resolvers and sets up serde's internal state.\n   *\n   * @param serdeProperties        specific serde instance's properties\n   * @param kafkaClusterProperties properties of the custer for what serde is instantiated\n   * @param globalProperties       global application properties\n   */\n  void configure(\n      PropertyResolver serdeProperties,\n      PropertyResolver kafkaClusterProperties,\n      PropertyResolver globalProperties\n  );\n\n  /**\n   * @return Serde's description. Treated as Markdown text. Will be shown in UI.\n   */\n  Optional<String> getDescription();\n\n  /**\n   * @return SchemaDescription for specified topic's key/value.\n   * {@code Optional.empty} if there is not information about schema.\n   */\n  Optional<SchemaDescription> getSchema(String topic, Target type);\n\n  /**\n   * @return true if this Serde can be applied to specified topic's key/value deserialization\n   */\n  boolean canDeserialize(String topic, Target type);\n\n  /**\n   * @return true if this Serde can be applied to specified topic's key/value serialization\n   */\n  boolean canSerialize(String topic, Target type);\n\n  /**\n   * Closes resources opened by Serde.\n   */\n  @Override\n  default void close() {\n    //intentionally left blank\n  }\n\n  //----------------------------------------------------------------------------\n\n  /**\n   * Creates {@code Serializer} for specified topic's key/value.\n   * Kafka-ui doesn't cache  {@code Serializes} - new one will be created each time user's message needs to be serialized.\n   * (Unless kafka-ui supports batch inserts).\n   */\n  Serializer serializer(String topic, Target type);\n\n  /**\n   * Creates {@code Deserializer} for specified topic's key/value.\n   * {@code Deserializer} will be created for each kafka polling and will be used for all messages within that polling cycle.\n   */\n  Deserializer deserializer(String topic, Target type);\n\n  /**\n   * 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).\n   */\n  interface Serializer {\n\n    /**\n     * @param input string entered by user into UI text field.<br/> Note: this input is not formatted in any way.\n     */\n    byte[] serialize(String input);\n  }\n\n  /**\n   * Deserializes polled record's key/value (depending on what {@code Type} it was created for).\n   */\n  interface Deserializer {\n    DeserializeResult deserialize(RecordHeaders headers, byte[] data);\n  }\n\n}\n"
  },
  {
    "path": "mvnw",
    "content": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#    https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n# ----------------------------------------------------------------------------\n\n# ----------------------------------------------------------------------------\n# Maven Start Up Batch script\n#\n# Required ENV vars:\n# ------------------\n#   JAVA_HOME - location of a JDK home dir\n#\n# Optional ENV vars\n# -----------------\n#   M2_HOME - location of maven2's installed home dir\n#   MAVEN_OPTS - parameters passed to the Java VM when running Maven\n#     e.g. to debug Maven itself, use\n#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n# ----------------------------------------------------------------------------\n\nif [ -z \"$MAVEN_SKIP_RC\" ] ; then\n\n  if [ -f /etc/mavenrc ] ; then\n    . /etc/mavenrc\n  fi\n\n  if [ -f \"$HOME/.mavenrc\" ] ; then\n    . \"$HOME/.mavenrc\"\n  fi\n\nfi\n\n# OS specific support.  $var _must_ be set to either true or false.\ncygwin=false;\ndarwin=false;\nmingw=false\ncase \"`uname`\" in\n  CYGWIN*) cygwin=true ;;\n  MINGW*) mingw=true;;\n  Darwin*) darwin=true\n    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home\n    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html\n    if [ -z \"$JAVA_HOME\" ]; then\n      if [ -x \"/usr/libexec/java_home\" ]; then\n        export JAVA_HOME=\"`/usr/libexec/java_home`\"\n      else\n        export JAVA_HOME=\"/Library/Java/Home\"\n      fi\n    fi\n    ;;\nesac\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  if [ -r /etc/gentoo-release ] ; then\n    JAVA_HOME=`java-config --jre-home`\n  fi\nfi\n\nif [ -z \"$M2_HOME\" ] ; then\n  ## resolve links - $0 may be a link to maven's home\n  PRG=\"$0\"\n\n  # need this for relative symlinks\n  while [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n      PRG=\"$link\"\n    else\n      PRG=\"`dirname \"$PRG\"`/$link\"\n    fi\n  done\n\n  saveddir=`pwd`\n\n  M2_HOME=`dirname \"$PRG\"`/..\n\n  # make it fully qualified\n  M2_HOME=`cd \"$M2_HOME\" && pwd`\n\n  cd \"$saveddir\"\n  # echo Using m2 at $M2_HOME\nfi\n\n# For Cygwin, ensure paths are in UNIX format before anything is touched\nif $cygwin ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --unix \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --unix \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --unix \"$CLASSPATH\"`\nfi\n\n# For Mingw, ensure paths are in UNIX format before anything is touched\nif $mingw ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=\"`(cd \"$M2_HOME\"; pwd)`\"\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=\"`(cd \"$JAVA_HOME\"; pwd)`\"\nfi\n\nif [ -z \"$JAVA_HOME\" ]; then\n  javaExecutable=\"`which javac`\"\n  if [ -n \"$javaExecutable\" ] && ! [ \"`expr \\\"$javaExecutable\\\" : '\\([^ ]*\\)'`\" = \"no\" ]; then\n    # readlink(1) is not available as standard on Solaris 10.\n    readLink=`which readlink`\n    if [ ! `expr \"$readLink\" : '\\([^ ]*\\)'` = \"no\" ]; then\n      if $darwin ; then\n        javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n        javaExecutable=\"`cd \\\"$javaHome\\\" && pwd -P`/javac\"\n      else\n        javaExecutable=\"`readlink -f \\\"$javaExecutable\\\"`\"\n      fi\n      javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n      javaHome=`expr \"$javaHome\" : '\\(.*\\)/bin'`\n      JAVA_HOME=\"$javaHome\"\n      export JAVA_HOME\n    fi\n  fi\nfi\n\nif [ -z \"$JAVACMD\" ] ; then\n  if [ -n \"$JAVA_HOME\"  ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n      # IBM's JDK on AIX uses strange locations for the executables\n      JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n      JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n  else\n    JAVACMD=\"`which java`\"\n  fi\nfi\n\nif [ ! -x \"$JAVACMD\" ] ; then\n  echo \"Error: JAVA_HOME is not defined correctly.\" >&2\n  echo \"  We cannot execute $JAVACMD\" >&2\n  exit 1\nfi\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  echo \"Warning: JAVA_HOME environment variable is not set.\"\nfi\n\nCLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher\n\n# traverses directory structure from process work directory to filesystem root\n# first directory with .mvn subdirectory is considered project base directory\nfind_maven_basedir() {\n\n  if [ -z \"$1\" ]\n  then\n    echo \"Path not specified to find_maven_basedir\"\n    return 1\n  fi\n\n  basedir=\"$1\"\n  wdir=\"$1\"\n  while [ \"$wdir\" != '/' ] ; do\n    if [ -d \"$wdir\"/.mvn ] ; then\n      basedir=$wdir\n      break\n    fi\n    # workaround for JBEAP-8937 (on Solaris 10/Sparc)\n    if [ -d \"${wdir}\" ]; then\n      wdir=`cd \"$wdir/..\"; pwd`\n    fi\n    # end of workaround\n  done\n  echo \"${basedir}\"\n}\n\n# concatenates all lines of a file\nconcat_lines() {\n  if [ -f \"$1\" ]; then\n    echo \"$(tr -s '\\n' ' ' < \"$1\")\"\n  fi\n}\n\nBASE_DIR=`find_maven_basedir \"$(pwd)\"`\nif [ -z \"$BASE_DIR\" ]; then\n  exit 1;\nfi\n\n##########################################################################################\n# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n# This allows using the maven wrapper in projects that prohibit checking in binary data.\n##########################################################################################\nif [ -r \"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\" ]; then\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Found .mvn/wrapper/maven-wrapper.jar\"\n    fi\nelse\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ...\"\n    fi\n    if [ -n \"$MVNW_REPOURL\" ]; then\n      jarUrl=\"$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\"\n    else\n      jarUrl=\"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\"\n    fi\n    while IFS=\"=\" read key value; do\n      case \"$key\" in (wrapperUrl) jarUrl=\"$value\"; break ;;\n      esac\n    done < \"$BASE_DIR/.mvn/wrapper/maven-wrapper.properties\"\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Downloading from: $jarUrl\"\n    fi\n    wrapperJarPath=\"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\"\n    if $cygwin; then\n      wrapperJarPath=`cygpath --path --windows \"$wrapperJarPath\"`\n    fi\n\n    if command -v wget > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found wget ... using wget\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            wget \"$jarUrl\" -O \"$wrapperJarPath\"\n        else\n            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD \"$jarUrl\" -O \"$wrapperJarPath\"\n        fi\n    elif command -v curl > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found curl ... using curl\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            curl -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        else\n            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        fi\n\n    else\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Falling back to using Java to download\"\n        fi\n        javaClass=\"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java\"\n        # For Cygwin, switch paths to Windows format before running javac\n        if $cygwin; then\n          javaClass=`cygpath --path --windows \"$javaClass\"`\n        fi\n        if [ -e \"$javaClass\" ]; then\n            if [ ! -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Compiling MavenWrapperDownloader.java ...\"\n                fi\n                # Compiling the Java class\n                (\"$JAVA_HOME/bin/javac\" \"$javaClass\")\n            fi\n            if [ -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                # Running the downloader\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Running MavenWrapperDownloader.java ...\"\n                fi\n                (\"$JAVA_HOME/bin/java\" -cp .mvn/wrapper MavenWrapperDownloader \"$MAVEN_PROJECTBASEDIR\")\n            fi\n        fi\n    fi\nfi\n##########################################################################################\n# End of extension\n##########################################################################################\n\nexport MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-\"$BASE_DIR\"}\nif [ \"$MVNW_VERBOSE\" = true ]; then\n  echo $MAVEN_PROJECTBASEDIR\nfi\nMAVEN_OPTS=\"$(concat_lines \"$MAVEN_PROJECTBASEDIR/.mvn/jvm.config\") $MAVEN_OPTS\"\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --path --windows \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --path --windows \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --windows \"$CLASSPATH\"`\n  [ -n \"$MAVEN_PROJECTBASEDIR\" ] &&\n    MAVEN_PROJECTBASEDIR=`cygpath --path --windows \"$MAVEN_PROJECTBASEDIR\"`\nfi\n\n# Provide a \"standardized\" way to retrieve the CLI args that will\n# work with both Windows and non-Windows executions.\nMAVEN_CMD_LINE_ARGS=\"$MAVEN_CONFIG $@\"\nexport MAVEN_CMD_LINE_ARGS\n\nWRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nexec \"$JAVACMD\" \\\n  $MAVEN_OPTS \\\n  -classpath \"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar\" \\\n  \"-Dmaven.home=${M2_HOME}\" \"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}\" \\\n  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG \"$@\"\n"
  },
  {
    "path": "mvnw.cmd",
    "content": "@REM ----------------------------------------------------------------------------\n@REM Licensed to the Apache Software Foundation (ASF) under one\n@REM or more contributor license agreements.  See the NOTICE file\n@REM distributed with this work for additional information\n@REM regarding copyright ownership.  The ASF licenses this file\n@REM to you under the Apache License, Version 2.0 (the\n@REM \"License\"); you may not use this file except in compliance\n@REM with the License.  You may obtain a copy of the License at\n@REM\n@REM    https://www.apache.org/licenses/LICENSE-2.0\n@REM\n@REM Unless required by applicable law or agreed to in writing,\n@REM software distributed under the License is distributed on an\n@REM \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n@REM KIND, either express or implied.  See the License for the\n@REM specific language governing permissions and limitations\n@REM under the License.\n@REM ----------------------------------------------------------------------------\n\n@REM ----------------------------------------------------------------------------\n@REM Maven Start Up Batch script\n@REM\n@REM Required ENV vars:\n@REM JAVA_HOME - location of a JDK home dir\n@REM\n@REM Optional ENV vars\n@REM M2_HOME - location of maven2's installed home dir\n@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands\n@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending\n@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven\n@REM     e.g. to debug Maven itself, use\n@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n@REM ----------------------------------------------------------------------------\n\n@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'\n@echo off\n@REM set title of command window\ntitle %0\n@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'\n@if \"%MAVEN_BATCH_ECHO%\" == \"on\"  echo %MAVEN_BATCH_ECHO%\n\n@REM set %HOME% to equivalent of $HOME\nif \"%HOME%\" == \"\" (set \"HOME=%HOMEDRIVE%%HOMEPATH%\")\n\n@REM Execute a user defined script before this one\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPre\n@REM check for pre script, once with legacy .bat ending and once with .cmd ending\nif exist \"%HOME%\\mavenrc_pre.bat\" call \"%HOME%\\mavenrc_pre.bat\"\nif exist \"%HOME%\\mavenrc_pre.cmd\" call \"%HOME%\\mavenrc_pre.cmd\"\n:skipRcPre\n\n@setlocal\n\nset ERROR_CODE=0\n\n@REM To isolate internal variables from possible post scripts, we use another setlocal\n@setlocal\n\n@REM ==== START VALIDATION ====\nif not \"%JAVA_HOME%\" == \"\" goto OkJHome\n\necho.\necho Error: JAVA_HOME not found in your environment. >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n:OkJHome\nif exist \"%JAVA_HOME%\\bin\\java.exe\" goto init\n\necho.\necho Error: JAVA_HOME is set to an invalid directory. >&2\necho JAVA_HOME = \"%JAVA_HOME%\" >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n@REM ==== END VALIDATION ====\n\n:init\n\n@REM Find the project base dir, i.e. the directory that contains the folder \".mvn\".\n@REM Fallback to current working directory if not found.\n\nset MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%\nIF NOT \"%MAVEN_PROJECTBASEDIR%\"==\"\" goto endDetectBaseDir\n\nset EXEC_DIR=%CD%\nset WDIR=%EXEC_DIR%\n:findBaseDir\nIF EXIST \"%WDIR%\"\\.mvn goto baseDirFound\ncd ..\nIF \"%WDIR%\"==\"%CD%\" goto baseDirNotFound\nset WDIR=%CD%\ngoto findBaseDir\n\n:baseDirFound\nset MAVEN_PROJECTBASEDIR=%WDIR%\ncd \"%EXEC_DIR%\"\ngoto endDetectBaseDir\n\n:baseDirNotFound\nset MAVEN_PROJECTBASEDIR=%EXEC_DIR%\ncd \"%EXEC_DIR%\"\n\n:endDetectBaseDir\n\nIF NOT EXIST \"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\" goto endReadAdditionalConfig\n\n@setlocal EnableExtensions EnableDelayedExpansion\nfor /F \"usebackq delims=\" %%a in (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a\n@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%\n\n:endReadAdditionalConfig\n\nSET MAVEN_JAVA_EXE=\"%JAVA_HOME%\\bin\\java.exe\"\nset WRAPPER_JAR=\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.jar\"\nset WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nset DOWNLOAD_URL=\"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\"\n\nFOR /F \"tokens=1,2 delims==\" %%A IN (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.properties\") DO (\n    IF \"%%A\"==\"wrapperUrl\" SET DOWNLOAD_URL=%%B\n)\n\n@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n@REM This allows using the maven wrapper in projects that prohibit checking in binary data.\nif exist %WRAPPER_JAR% (\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Found %WRAPPER_JAR%\n    )\n) else (\n    if not \"%MVNW_REPOURL%\" == \"\" (\n        SET DOWNLOAD_URL=\"%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar\"\n    )\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Couldn't find %WRAPPER_JAR%, downloading it ...\n        echo Downloading from: %DOWNLOAD_URL%\n    )\n\n    powershell -Command \"&{\"^\n\t\t\"$webclient = new-object System.Net.WebClient;\"^\n\t\t\"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {\"^\n\t\t\"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');\"^\n\t\t\"}\"^\n\t\t\"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')\"^\n\t\t\"}\"\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Finished downloading %WRAPPER_JAR%\n    )\n)\n@REM End of extension\n\n@REM Provide a \"standardized\" way to retrieve the CLI args that will\n@REM work with both Windows and non-Windows executions.\nset MAVEN_CMD_LINE_ARGS=%*\n\n%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% \"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%\" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*\nif ERRORLEVEL 1 goto error\ngoto end\n\n:error\nset ERROR_CODE=1\n\n:end\n@endlocal & set ERROR_CODE=%ERROR_CODE%\n\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPost\n@REM check for post script, once with legacy .bat ending and once with .cmd ending\nif exist \"%HOME%\\mavenrc_post.bat\" call \"%HOME%\\mavenrc_post.bat\"\nif exist \"%HOME%\\mavenrc_post.cmd\" call \"%HOME%\\mavenrc_post.cmd\"\n:skipRcPost\n\n@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'\nif \"%MAVEN_BATCH_PAUSE%\" == \"on\" pause\n\nif \"%MAVEN_TERMINATE_CMD%\" == \"on\" exit %ERROR_CODE%\n\nexit /B %ERROR_CODE%\n"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <packaging>pom</packaging>\n    <modules>\n        <module>kafka-ui-contract</module>\n        <module>kafka-ui-api</module>\n        <module>kafka-ui-serde-api</module>\n        <module>kafka-ui-e2e-checks</module>\n    </modules>\n\n    <properties>\n        <maven.compiler.release>17</maven.compiler.release>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n\n        <frontend-generated-sources-directory>..//kafka-ui-react-app/src/generated-sources\n        </frontend-generated-sources-directory>\n        <sonar.organization>provectus</sonar.organization>\n        <sonar.host.url>https://sonarcloud.io</sonar.host.url>\n        <git.revision>latest</git.revision>\n\n        <!-- Dependency versions -->\n        <antlr4-maven-plugin.version>4.12.0</antlr4-maven-plugin.version>\n        <apache.commons.version>2.11.1</apache.commons.version>\n        <assertj.version>3.19.0</assertj.version>\n        <avro.version>1.11.1</avro.version>\n        <byte-buddy.version>1.12.19</byte-buddy.version>\n        <confluent.version>7.4.0</confluent.version>\n        <datasketches-java.version>3.1.0</datasketches-java.version>\n        <groovy.version>3.0.13</groovy.version>\n        <jackson.version>2.14.0</jackson.version>\n        <kafka-clients.version>3.5.0</kafka-clients.version>\n        <org.mapstruct.version>1.5.5.Final</org.mapstruct.version>\n        <org.projectlombok.version>1.18.24</org.projectlombok.version>\n        <protobuf-java.version>3.23.3</protobuf-java.version>\n        <scala-lang.library.version>2.13.9</scala-lang.library.version>\n        <snakeyaml.version>2.0</snakeyaml.version>\n        <spring-boot.version>3.1.3</spring-boot.version>\n        <kafka-ui-serde-api.version>1.0.0</kafka-ui-serde-api.version>\n        <odd-oddrn-generator.version>0.1.17</odd-oddrn-generator.version>\n        <odd-oddrn-client.version>0.1.26</odd-oddrn-client.version>\n        <org.json.version>20230227</org.json.version>\n\n        <!-- Test dependency versions -->\n        <junit.version>5.9.1</junit.version>\n        <mockito.version>5.3.1</mockito.version>\n        <okhttp3.mockwebserver.version>4.10.0</okhttp3.mockwebserver.version>\n        <testcontainers.version>1.17.5</testcontainers.version>\n\n        <!-- Frontend dependency versions -->\n        <node.version>v18.17.1</node.version>\n        <pnpm.version>v8.6.12</pnpm.version>\n\n        <!-- Plugin versions -->\n        <fabric8-maven-plugin.version>0.42.1</fabric8-maven-plugin.version>\n        <frontend-maven-plugin.version>1.12.1</frontend-maven-plugin.version>\n        <maven-clean-plugin.version>3.2.0</maven-clean-plugin.version>\n        <maven-compiler-plugin.version>3.10.1</maven-compiler-plugin.version>\n        <maven-resources-plugin.version>3.2.0</maven-resources-plugin.version>\n        <maven-surefire-plugin.version>3.1.2</maven-surefire-plugin.version>\n        <openapi-generator-maven-plugin.version>6.6.0</openapi-generator-maven-plugin.version>\n        <springdoc-openapi-webflux-ui.version>1.2.32</springdoc-openapi-webflux-ui.version>\n    </properties>\n\n    <repositories>\n        <repository>\n            <id>confluent</id>\n            <url>https://packages.confluent.io/maven/</url>\n        </repository>\n        <repository>\n            <id>central</id>\n            <name>Central Repository</name>\n            <url>https://repo.maven.apache.org/maven2</url>\n            <layout>default</layout>\n            <snapshots>\n                <enabled>false</enabled>\n            </snapshots>\n        </repository>\n    </repositories>\n\n    <pluginRepositories>\n        <pluginRepository>\n            <id>confluent</id>\n            <url>https://packages.confluent.io/maven/</url>\n        </pluginRepository>\n        <pluginRepository>\n            <id>central</id>\n            <name>Central Repository</name>\n            <url>https://repo.maven.apache.org/maven2</url>\n            <layout>default</layout>\n            <snapshots>\n                <enabled>false</enabled>\n            </snapshots>\n            <releases>\n                <updatePolicy>never</updatePolicy>\n            </releases>\n        </pluginRepository>\n    </pluginRepositories>\n\n    <dependencyManagement>\n        <dependencies>\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-dependencies</artifactId>\n                <version>${spring-boot.version}</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n            <dependency>\n                <groupId>com.fasterxml.jackson</groupId>\n                <artifactId>jackson-bom</artifactId>\n                <version>${jackson.version}</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n            <dependency>\n                <groupId>org.scala-lang</groupId>\n                <artifactId>scala-library</artifactId>\n                <version>${scala-lang.library.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.yaml</groupId>\n                <artifactId>snakeyaml</artifactId>\n                <version>${snakeyaml.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.google.protobuf</groupId>\n                <artifactId>protobuf-java</artifactId>\n                <version>${protobuf-java.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.junit</groupId>\n                <artifactId>junit-bom</artifactId>\n                <version>${junit.version}</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n            <dependency>\n                <groupId>org.testcontainers</groupId>\n                <artifactId>testcontainers-bom</artifactId>\n                <version>${testcontainers.version}</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n        </dependencies>\n    </dependencyManagement>\n\n    <build>\n        <pluginManagement>\n            <plugins>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-compiler-plugin</artifactId>\n                    <version>${maven-compiler-plugin.version}</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-resources-plugin</artifactId>\n                    <version>${maven-resources-plugin.version}</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-surefire-plugin</artifactId>\n                    <version>${maven-surefire-plugin.version}</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-clean-plugin</artifactId>\n                    <version>${maven-clean-plugin.version}</version>\n                </plugin>\n            </plugins>\n        </pluginManagement>\n    </build>\n\n    <groupId>com.provectus</groupId>\n    <artifactId>kafka-ui</artifactId>\n    <version>0.0.1-SNAPSHOT</version>\n    <name>kafka-ui</name>\n    <description>Web UI for Apache Kafka</description>\n</project>\n"
  },
  {
    "path": "settings.xml",
    "content": "<settings>\n  <servers>\n    <server>\n     <id>ossrh</id>\n     <username>${server.username}</username>\n     <password>${server.password}</password>\n    </server>\n  </servers>\n  <profiles>\n    <profile>\n      <id>ossrh</id>\n      <activation>\n        <activeByDefault>true</activeByDefault>\n      </activation>\n    </profile>\n  </profiles>\n</settings>"
  }
]